diff --git a/libs/ui/src/components/Explorer/Explorer.test.tsx b/libs/ui/src/components/Explorer/Explorer.test.tsx index 225d8302a..37f515cae 100644 --- a/libs/ui/src/components/Explorer/Explorer.test.tsx +++ b/libs/ui/src/components/Explorer/Explorer.test.tsx @@ -461,13 +461,11 @@ describe('Explorer', () => { expect(screen.getByText(record2.whoAmI.label)).toBeInTheDocument(); }); - test('Should hide table header when no data provided', async () => { + test('Should display message on empty data', async () => { spyUseExplorerQuery.mockReturnValueOnce(mockEmptyExplorerQueryResult); render(); - expect(screen.getByRole('table')).toBeVisible(); - expect(screen.getByRole('row')).toBeVisible(); - expect(within(screen.getByRole('row')).getByText('Aucune donnée')).toBeVisible(); + expect(screen.getByText(/empty-data/)).toBeVisible(); }); test('Should display the list of records in a table with attributes values', async () => { diff --git a/libs/ui/src/components/Explorer/Explorer.tsx b/libs/ui/src/components/Explorer/Explorer.tsx index 3d21d7a35..35b9ea99e 100644 --- a/libs/ui/src/components/Explorer/Explorer.tsx +++ b/libs/ui/src/components/Explorer/Explorer.tsx @@ -3,7 +3,7 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {ComponentProps, FunctionComponent, useReducer} from 'react'; import {createPortal} from 'react-dom'; -import {KitSpace, KitTypography} from 'aristid-ds'; +import {KitEmpty, KitSpace, KitTypography} from 'aristid-ds'; import styled from 'styled-components'; import {IItemAction, IPrimaryAction} from './_types'; import {useExplorerData} from './_queries/useExplorerData'; @@ -26,6 +26,8 @@ import { import {useSearchInput} from './useSearchInput'; import {usePagination} from './usePagination'; import {Loading} from '../Loading'; +import {ExplorerFilterBar} from './display-view-filters/ExplorerFilterBar'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; const isNotEmpty = (union: T): union is Exclude => union.length > 0; @@ -64,6 +66,8 @@ export const Explorer: FunctionComponent = ({ defaultPrimaryActions = ['create'], defaultViewSettings }) => { + const {t} = useSharedTranslation(); + const {panelElement} = useEditSettings(); const [view, dispatch] = useReducer(viewSettingsReducer, { @@ -79,7 +83,8 @@ export const Explorer: FunctionComponent = ({ attributeIds: view.attributesIds, fulltextSearch: view.fulltextSearch, pagination: noPagination ? null : {limit: view.pageSize, offset: view.pageSize * (currentPage - 1)}, - sorts: view.sort + sorts: view.sort, + filters: view.filters }); // TODO: refresh when go back on page const {deactivateAction} = useDeactivateAction({ @@ -128,13 +133,16 @@ export const Explorer: FunctionComponent = ({ {primaryButton} + {loading ? ( + ) : data === null ? ( + ) : ( diff --git a/libs/ui/src/components/Explorer/_queries/useExplorerData.ts b/libs/ui/src/components/Explorer/_queries/useExplorerData.ts index 74361b1c6..34caadf8c 100644 --- a/libs/ui/src/components/Explorer/_queries/useExplorerData.ts +++ b/libs/ui/src/components/Explorer/_queries/useExplorerData.ts @@ -2,8 +2,8 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {localizedTranslation} from '@leav/utils'; -import {IExplorerData} from '../_types'; -import {ExplorerQuery, SortOrder, useExplorerQuery} from '_ui/_gqlTypes'; +import {IExplorerData, IExplorerFilter} from '../_types'; +import {ExplorerQuery, RecordFilterCondition, SortOrder, useExplorerQuery} from '_ui/_gqlTypes'; import {useLang} from '_ui/hooks'; const _mapping = (data: ExplorerQuery, libraryId: string, availableLangs: string[]): IExplorerData => { @@ -44,7 +44,8 @@ export const useExplorerData = ({ attributeIds, fulltextSearch, sorts, - pagination + pagination, + filters }: { libraryId: string; attributeIds: string[]; @@ -54,6 +55,7 @@ export const useExplorerData = ({ order: SortOrder; }>; pagination: null | {limit: number; offset: number}; + filters: IExplorerFilter[]; }) => { const {lang: availableLangs} = useLang(); const {data, loading, refetch} = useExplorerQuery({ @@ -65,12 +67,23 @@ export const useExplorerData = ({ multipleSort: sorts.map(({order, attributeId}) => ({ field: attributeId, order - })) + })), + filters: filters + .filter( + ({value, condition}) => + value !== null || + [RecordFilterCondition.IS_EMPTY, RecordFilterCondition.IS_NOT_EMPTY].includes(condition) + ) + .map(({field, condition, value}) => ({ + field, + condition, + value + })) } }); return { - data: data !== undefined ? _mapping(data, libraryId, availableLangs) : null, + data: data !== undefined && data.records.list.length > 0 ? _mapping(data, libraryId, availableLangs) : null, loading, refetch }; diff --git a/libs/ui/src/components/Explorer/_types.ts b/libs/ui/src/components/Explorer/_types.ts index 3f2a4af1f..bfd415062 100644 --- a/libs/ui/src/components/Explorer/_types.ts +++ b/libs/ui/src/components/Explorer/_types.ts @@ -2,7 +2,13 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {Override} from '@leav/utils'; -import {AttributePropertiesFragment, PropertyValueFragment, RecordIdentityFragment} from '_ui/_gqlTypes'; +import { + AttributeFormat, + AttributePropertiesFragment, + PropertyValueFragment, + RecordFilterCondition, + RecordIdentityFragment +} from '_ui/_gqlTypes'; import {ReactElement} from 'react'; export interface IExplorerData { @@ -37,3 +43,18 @@ export interface IPrimaryAction { } export type ActionHook = {isEnabled: boolean} & T; + +export interface IExplorerFilter { + id: string; + attribute: { + format: AttributeFormat; + label: string; + }; + field: string; + condition: RecordFilterCondition; + value: string | null; +} + +export interface IFilterDropDownProps { + filter: IExplorerFilter; +} diff --git a/libs/ui/src/components/Explorer/display-view-filters/ExplorerFilterBar.tsx b/libs/ui/src/components/Explorer/display-view-filters/ExplorerFilterBar.tsx new file mode 100644 index 000000000..6f5f9407f --- /dev/null +++ b/libs/ui/src/components/Explorer/display-view-filters/ExplorerFilterBar.tsx @@ -0,0 +1,70 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import styled from 'styled-components'; +import {useViewSettingsContext} from '../manage-view-settings/store-view-settings/useViewSettingsContext'; +import {KitButton, KitDivider, KitFilter, KitSpace} from 'aristid-ds'; +import {FunctionComponent} from 'react'; +import {FilterDropDown} from '../manage-view-settings/filter-items/filter-type/FilterDropDown'; +import {FaTrash} from 'react-icons/fa'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; + +const FilterStyled = styled(KitFilter)` + flex: 0 0 auto; +`; + +const ExplorerFilterBarStyledDiv = styled.div` + overflow: auto; + padding: 0 calc(var(--general-spacing-xxs) * 1px); + padding-bottom: calc(var(--general-spacing-m) * 1px); +`; + +const ExplorerBarItemsListDiv = styled.div` + display: flex; + flex-wrap: nowrap; + align-items: center; + gap: 0; + white-space: nowrap; + padding-top: calc(var(--general-spacing-xs) * 1px); +`; + +const DividerStyled = styled(KitDivider)` + height: 2em; +`; + +export const ExplorerFilterBar: FunctionComponent = () => { + const {t} = useSharedTranslation(); + + const { + view: {filters} + } = useViewSettingsContext(); + + if (filters.length === 0) { + return null; + } + + return ( + + + + {filters.map(filter => ( + + }} + /> + ))} + + + } disabled> + {t('explorer.delete-filters')} + + + + ); +}; diff --git a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/FilterItems.tsx b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/FilterItems.tsx index 63cbf0aea..0fef4a810 100644 --- a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/FilterItems.tsx +++ b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/FilterItems.tsx @@ -6,7 +6,7 @@ import {FaEye, FaEyeSlash, FaSearch} from 'react-icons/fa'; import {KitInput, KitTypography} from 'aristid-ds'; import styled from 'styled-components'; import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -import {AttributeType, SortOrder} from '_ui/_gqlTypes'; +import {AttributeFormat, AttributeType, RecordFilterCondition} from '_ui/_gqlTypes'; import { closestCenter, DndContext, @@ -21,7 +21,7 @@ import {useAttributeDetailsData} from '../_shared/useAttributeDetailsData'; import {ViewSettingsActionTypes} from '../store-view-settings/viewSettingsReducer'; import {useViewSettingsContext} from '../store-view-settings/useViewSettingsContext'; import {FilterListItem} from './FilterListItem'; -import {SimpleFilterDropdown} from './filter-type/SimpleFilterDropDown'; +import {FilterDropDown} from './filter-type/FilterDropDown'; const StyledList = styled.ul` padding: calc(var(--general-spacing-s) * 1px) 0; @@ -54,27 +54,28 @@ export const FilterItems: FunctionComponent<{libraryId: string}> = ({libraryId}) }) ); - const _toggleColumnVisibility = (attributeId: string) => () => { - const isAttributeAlreadyFiltering = filters.find(filterItem => filterItem.field === attributeId); - if (isAttributeAlreadyFiltering) { - dispatch({ - type: ViewSettingsActionTypes.REMOVE_FILTER, - payload: { - id: isAttributeAlreadyFiltering.id - } - }); - } else { - if (canAddFilter) { - dispatch({ - type: ViewSettingsActionTypes.ADD_FILTER, - payload: { - field: attributeId, - condition: '', - values: [] - } - }); + const addFilter = (attributeId: string) => () => { + dispatch({ + type: ViewSettingsActionTypes.ADD_FILTER, + payload: { + field: attributeId, + attribute: { + label: attributeDetailsById[attributeId].label, + format: attributeDetailsById[attributeId].format ?? AttributeFormat.text + }, + condition: RecordFilterCondition.EQUAL, + value: null } - } + }); + }; + + const removeFilter = (filterId: string) => () => { + dispatch({ + type: ViewSettingsActionTypes.REMOVE_FILTER, + payload: { + id: filterId + } + }); }; const _handleDragEnd = ({active: draggedElement, over: dropTarget}: DragEndEvent) => { @@ -110,22 +111,17 @@ export const FilterItems: FunctionComponent<{libraryId: string}> = ({libraryId}) attributeId={activeFilter.field} isDraggable filterChipProps={{ - label: attributeDetailsById[activeFilter.field].label, + label: activeFilter.attribute.label, expandable: true, - values: activeFilter.values, + values: activeFilter.value === null ? [] : [activeFilter.value], dropDownProps: { - dropdownRender: () => ( - - ) + dropdownRender: () => } }} visibilityButtonProps={{ icon: , title: String(t('explorer.hide')), - onClick: _toggleColumnVisibility(activeFilter.field) + onClick: removeFilter(activeFilter.id) }} /> ))} @@ -154,7 +150,7 @@ export const FilterItems: FunctionComponent<{libraryId: string}> = ({libraryId}) ? { icon: , title: String(t('explorer.show')), - onClick: _toggleColumnVisibility(attributeId) + onClick: addFilter(attributeId) } : undefined } diff --git a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/FilterDropDown.tsx b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/FilterDropDown.tsx new file mode 100644 index 000000000..6c97367b7 --- /dev/null +++ b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/FilterDropDown.tsx @@ -0,0 +1,21 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {IFilterDropDownProps} from '_ui/components/Explorer/_types'; +import {AttributeFormat} from '_ui/_gqlTypes'; +import {FunctionComponent} from 'react'; +import {SimpleFilterDropdown} from './SimpleFilterDropDown'; +import {TextAttributeDropDown} from './TextAttributeDropDown'; +import {NumericAttributeDropDown} from './NumericAttributeDropDown'; + +export const FilterDropDown: FunctionComponent = ({filter}) => { + switch (filter.attribute.format) { + case AttributeFormat.numeric: + return ; + case AttributeFormat.text: + case AttributeFormat.rich_text: + return ; + default: + return ; + } +}; diff --git a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/FilterValueList.tsx b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/FilterValueList.tsx new file mode 100644 index 000000000..0f0efcfe0 --- /dev/null +++ b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/FilterValueList.tsx @@ -0,0 +1,120 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {useDebouncedValue} from '_ui/hooks/useDebouncedValue'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; +import {KitInput, KitTypography} from 'aristid-ds'; +import {ChangeEvent, FunctionComponent, useMemo, useState} from 'react'; +import {FaCheck, FaSearch} from 'react-icons/fa'; +import styled from 'styled-components'; + +interface IFilterValueListProps { + values: string[]; + selectedValues: string[]; + multiple: boolean; + freeEntry: boolean; + onSelectionChanged: (value: string[]) => void; +} + +const FilterValueListStyledUl = styled.ul` + padding: 0; + margin: 0; + list-style: none; +`; + +const ValueListItemValueLi = styled.li` + display: flex; + align-items: center; + min-height: 32px; + height: 32px; + border-radius: calc(var(--general-border-radius-s) * 1px); + cursor: pointer; + + &.selected, + &:hover { + color: var(--components-Icon-colors-icon-on, var(--general-utilities-main-default)); + background-color: var(--components-Icon-colors-background-on, var(--general-utilities-main-light)); + } + + .label { + flex: 1; + padding: 0 calc(var(--general-spacing-xs) * 1px); + } + + .check { + flex: 0; + padding: 0 calc(var(--general-spacing-xs) * 1px); + } +`; + +export const FilterValueList: FunctionComponent = ({ + values, + selectedValues, + multiple, + onSelectionChanged +}) => { + const {t} = useSharedTranslation(); + const [searchInput, setSearchInput] = useState(''); + const debouncedSearchInput = useDebouncedValue(searchInput, 300); + + const searchFilteredValues = useMemo(() => { + if (debouncedSearchInput === '' || debouncedSearchInput.length < 3) { + return values; + } + + return values.filter(value => value.includes(debouncedSearchInput)); + }, [debouncedSearchInput, values]); + + const onSearchChanged = (event: ChangeEvent) => { + const shouldIgnoreInputChange = event.target.value.length < 3 && debouncedSearchInput.length < 3; + if (shouldIgnoreInputChange) { + return; + } + setSearchInput(() => { + if (event.target.value.length > 2) { + return event.target.value; + } + return ''; + }); + }; + + const onClickItem = (value: string) => { + if (selectedValues.includes(value)) { + onSelectionChanged(selectedValues.filter(selectedValue => selectedValue !== value)); + } else { + onSelectionChanged(multiple ? [...selectedValues, value] : [value]); + } + }; + + return ( + <> + } + /> + + {searchFilteredValues.map(value => { + const isSelected = selectedValues.includes(value); + return ( + onClickItem(value)} + > + + {value} + + {isSelected && ( + + + + )} + + ); + })} + + + ); +}; diff --git a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/NumericAttributeDropDown.tsx b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/NumericAttributeDropDown.tsx new file mode 100644 index 000000000..691a3ad7b --- /dev/null +++ b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/NumericAttributeDropDown.tsx @@ -0,0 +1,66 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; +import {KitButton, KitDivider, KitInput, KitInputNumber, KitSelect, KitSpace} from 'aristid-ds'; +import {ComponentProps, FunctionComponent} from 'react'; +import {FaClock, FaTrash} from 'react-icons/fa'; +import {AttributeConditionFilter} from '_ui/types'; +import {IExplorerFilter, IFilterDropDownProps} from '_ui/components/Explorer/_types'; +import {useViewSettingsContext} from '../../store-view-settings/useViewSettingsContext'; +import {ViewSettingsActionTypes} from '../../store-view-settings/viewSettingsReducer'; +import {useConditionsOptionsByType} from './useConditionOptionsByType'; + +export const NumericAttributeDropDown: FunctionComponent = ({filter}) => { + const {t} = useSharedTranslation(); + const {dispatch} = useViewSettingsContext(); + + const {conditionOptionsByType} = useConditionsOptionsByType(filter); + + const _updateFilter = (filterData: IExplorerFilter) => { + dispatch({ + type: ViewSettingsActionTypes.CHANGE_FILTER_CONFIG, + payload: filterData + }); + }; + + const _onConditionChanged: ComponentProps['onChange'] = condition => + _updateFilter({...filter, condition}); + + const _onInputChanged: ComponentProps['onChange'] = value => + _updateFilter({...filter, value: value === null ? null : String(value)}); + + const _onResetFilter: ComponentProps['onClick'] = () => _updateFilter({...filter, value: null}); + + const _onDeleteFilter: ComponentProps['onClick'] = () => + dispatch({ + type: ViewSettingsActionTypes.REMOVE_FILTER, + payload: { + id: filter.id + } + }); + + const showInput = + filter.condition !== AttributeConditionFilter.IS_EMPTY && + filter.condition !== AttributeConditionFilter.IS_NOT_EMPTY; + + return ( + + + {showInput && ( + + )} + + } onClick={_onResetFilter}> + {t('explorer.reset-filter')} + + } onClick={_onDeleteFilter}> + {t('global.delete')} + + + ); +}; diff --git a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/SimpleFilterDropDown.tsx b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/SimpleFilterDropDown.tsx index 6c06c9793..a1b955e33 100644 --- a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/SimpleFilterDropDown.tsx +++ b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/SimpleFilterDropDown.tsx @@ -2,12 +2,13 @@ // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; -import {KitButton, KitDivider, KitInput, KitSelect, KitSpace} from 'aristid-ds'; -import {FunctionComponent} from 'react'; -import {FaCheck, FaClock, FaTrash} from 'react-icons/fa'; +import {KitButton, KitDivider, KitSelect, KitSpace} from 'aristid-ds'; +import {ComponentProps, FunctionComponent} from 'react'; +import {FaClock, FaTrash} from 'react-icons/fa'; import {useViewSettingsContext} from '../../store-view-settings/useViewSettingsContext'; -import {IExplorerFilter, ViewSettingsActionTypes} from '../../store-view-settings/viewSettingsReducer'; -import styled from 'styled-components'; +import {ViewSettingsActionTypes} from '../../store-view-settings/viewSettingsReducer'; +import {IFilterDropDownProps} from '_ui/components/Explorer/_types'; +import {FilterValueList} from './FilterValueList'; // TODO : This is an exemple file showing ho to customize dropdown Panel content. Don't mind the content of the file, missing types,... it's just an example. @@ -42,36 +43,9 @@ const conditions = [ } ]; -const attributeValuesList = ['Value 1', 'Value 2', 'Value 3', 'Value 4', 'Value 5']; +const attributeValuesList = ['toto', 'tata', 'Value 3', 'Value 4', 'Value 5']; -const ValueListItemValueLi = styled.li` - display: flex; - align-items: center; - min-height: 32px; - height: 32px; - border-radius: calc(var(--general-border-radius-s) * 1px); - cursor: pointer; - - &.selected { - color: var(--components-Icon-colors-icon-on, var(--general-utilities-main-default)); - background-color: var(--components-Icon-colors-background-on, var(--general-utilities-main-light)); - } - - .label { - flex: 1; - padding: 0 calc(var(--general-spacing-xs) * 1px); - } - - .check { - flex: 0; - padding: 0 calc(var(--general-spacing-xs) * 1px); - } -`; - -export const SimpleFilterDropdown: FunctionComponent<{ - filter: IExplorerFilter; - attribute: {multiple_values: boolean}; -}> = ({filter, attribute}) => { +export const SimpleFilterDropdown: FunctionComponent = ({filter}) => { const {t} = useSharedTranslation(); const {dispatch} = useViewSettingsContext(); @@ -80,57 +54,43 @@ export const SimpleFilterDropdown: FunctionComponent<{ type: ViewSettingsActionTypes.CHANGE_FILTER_CONFIG, payload: { id: filter.id, - field: filter.field, - condition: data.condition, - values: data.values + ...data } }); }; - const onconditionChanged = condition => { - updateFilter({...filter, condition}); + const onConditionChanged = operator => { + updateFilter({...filter, operator}); }; const onValueClick = value => { - let newValues = [...filter.values]; - if (filter.values.includes(value)) { - newValues = filter.values.filter(v => v !== value); - } else { - if (attribute.multiple_values) { - newValues = [...filter.values, value]; - } else { - newValues = [value]; - } - } - updateFilter({...filter, values: newValues}); + updateFilter({...filter, value}); }; + const _onDeleteFilter: ComponentProps['onClick'] = () => + dispatch({ + type: ViewSettingsActionTypes.REMOVE_FILTER, + payload: { + id: filter.id + } + }); + return ( - - -
    - {attributeValuesList.map(value => ( - onValueClick(value)} - className={filter.values.includes(value) ? 'selected' : ''} - > - {value} - {filter.values.includes(value) && ( - - - - )} - - ))} -
+ + - }> - Réinitialiser le filtre + } disabled> + {t('explorer.reset-filter')} - }> - Supprimer + } onClick={_onDeleteFilter}> + {t('global.delete')}
); diff --git a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/TextAttributeDropDown.tsx b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/TextAttributeDropDown.tsx new file mode 100644 index 000000000..fbf82f285 --- /dev/null +++ b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/TextAttributeDropDown.tsx @@ -0,0 +1,72 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; +import {KitButton, KitDivider, KitInput, KitSelect, KitSpace} from 'aristid-ds'; +import {ChangeEvent, ComponentProps, FunctionComponent} from 'react'; +import {FaClock, FaTrash} from 'react-icons/fa'; +import {useViewSettingsContext} from '../../store-view-settings/useViewSettingsContext'; +import {ViewSettingsActionTypes} from '../../store-view-settings/viewSettingsReducer'; +import {IExplorerFilter, IFilterDropDownProps} from '_ui/components/Explorer/_types'; +import {useConditionsOptionsByType} from './useConditionOptionsByType'; +import {AttributeConditionFilter} from '_ui/types'; + +export const TextAttributeDropDown: FunctionComponent = ({filter}) => { + const {t} = useSharedTranslation(); + const {dispatch} = useViewSettingsContext(); + + const {conditionOptionsByType} = useConditionsOptionsByType(filter); + + const _updateFilter = (filterData: IExplorerFilter) => { + dispatch({ + type: ViewSettingsActionTypes.CHANGE_FILTER_CONFIG, + payload: filterData + }); + }; + + const _onConditionChanged: ComponentProps['onChange'] = condition => { + _updateFilter({...filter, condition}); + }; + + // TODO debounce ? + const _onInputChanged: ComponentProps['onChange'] = event => { + const shouldIgnoreInputChange = + event.target.value.length < 3 && (filter.value?.length ?? 0) <= event.target.value.length; + if (shouldIgnoreInputChange) { + return; + } + _updateFilter({...filter, value: event.target.value.length === 0 ? null : event.target.value}); + }; + + const _onDeleteFilter: ComponentProps['onClick'] = () => + dispatch({ + type: ViewSettingsActionTypes.REMOVE_FILTER, + payload: { + id: filter.id + } + }); + + const showSearch = + filter.condition !== AttributeConditionFilter.IS_EMPTY && + filter.condition !== AttributeConditionFilter.IS_NOT_EMPTY; + + return ( + + + {showSearch && ( + + )} + + } disabled> + {t('explorer.reset-filter')} + + } onClick={_onDeleteFilter}> + {t('global.delete')} + + + ); +}; diff --git a/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/useConditionOptionsByType.ts b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/useConditionOptionsByType.ts new file mode 100644 index 000000000..6af0f0b29 --- /dev/null +++ b/libs/ui/src/components/Explorer/manage-view-settings/filter-items/filter-type/useConditionOptionsByType.ts @@ -0,0 +1,96 @@ +// Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 +// This file is released under LGPL V3 +// License text available at https://www.gnu.org/licenses/lgpl-3.0.txt +import {AttributeFormat} from '_ui/_gqlTypes'; +import {IExplorerFilter} from '_ui/components/Explorer/_types'; +import {AttributeConditionFilter, AttributeConditionType} from '_ui/types'; +import {TFunction} from 'i18next'; +import {useSharedTranslation} from '_ui/hooks/useSharedTranslation'; + +const conditionsByFormat: Record = { + [AttributeFormat.text]: [ + AttributeConditionFilter.CONTAINS, + AttributeConditionFilter.NOT_CONTAINS, + AttributeConditionFilter.EQUAL, + AttributeConditionFilter.NOT_EQUAL, + AttributeConditionFilter.BEGIN_WITH, + AttributeConditionFilter.END_WITH, + AttributeConditionFilter.IS_EMPTY, + AttributeConditionFilter.IS_NOT_EMPTY + ], + [AttributeFormat.rich_text]: [ + AttributeConditionFilter.CONTAINS, + AttributeConditionFilter.NOT_CONTAINS, + AttributeConditionFilter.IS_EMPTY, + AttributeConditionFilter.IS_NOT_EMPTY + ], + [AttributeFormat.boolean]: [], + [AttributeFormat.color]: [], + [AttributeFormat.date]: [], + [AttributeFormat.date_range]: [], + [AttributeFormat.encrypted]: [], + [AttributeFormat.extended]: [], + [AttributeFormat.numeric]: [ + AttributeConditionFilter.EQUAL, + AttributeConditionFilter.NOT_EQUAL, + AttributeConditionFilter.IS_EMPTY, + AttributeConditionFilter.IS_NOT_EMPTY, + AttributeConditionFilter.LESS_THAN, + AttributeConditionFilter.GREATER_THAN + ] +}; + +interface IExplorerFilterConditionOption { + label: string; + value: T; + textByFormat?: {[key in AttributeFormat]?: string}; +} + +const _getAttributeConditionOptions = (t: TFunction): Array> => [ + {label: t('filters.contains'), value: AttributeConditionFilter.CONTAINS}, + {label: t('filters.not-contains'), value: AttributeConditionFilter.NOT_CONTAINS}, + {label: t('filters.equal'), value: AttributeConditionFilter.EQUAL}, + {label: t('filters.not-equal'), value: AttributeConditionFilter.NOT_EQUAL}, + {label: t('filters.begin-with'), value: AttributeConditionFilter.BEGIN_WITH}, + {label: t('filters.end-with'), value: AttributeConditionFilter.END_WITH}, + { + label: t('filters.less-than'), + textByFormat: {[AttributeFormat.date]: String(t('filters.before'))}, + value: AttributeConditionFilter.LESS_THAN + }, + { + label: t('filters.greater-than'), + textByFormat: {[AttributeFormat.date]: String(t('filters.after'))}, + value: AttributeConditionFilter.GREATER_THAN + }, + {label: t('filters.today'), value: AttributeConditionFilter.TODAY}, + {label: t('filters.tomorrow'), value: AttributeConditionFilter.TOMORROW}, + {label: t('filters.yesterday'), value: AttributeConditionFilter.YESTERDAY}, + {label: t('filters.last-month'), value: AttributeConditionFilter.LAST_MONTH}, + {label: t('filters.next-month'), value: AttributeConditionFilter.NEXT_MONTH}, + {label: t('filters.between'), value: AttributeConditionFilter.BETWEEN}, + {label: t('filters.start-on'), value: AttributeConditionFilter.START_ON}, + {label: t('filters.start-after'), value: AttributeConditionFilter.START_AFTER}, + {label: t('filters.start-before'), value: AttributeConditionFilter.START_BEFORE}, + {label: t('filters.end-on'), value: AttributeConditionFilter.END_ON}, + {label: t('filters.end-after'), value: AttributeConditionFilter.END_AFTER}, + {label: t('filters.end-before'), value: AttributeConditionFilter.END_BEFORE}, + {label: t('filters.is-empty'), value: AttributeConditionFilter.IS_EMPTY}, + {label: t('filters.is-not-empty'), value: AttributeConditionFilter.IS_NOT_EMPTY}, + {label: t('filters.values-count-equal'), value: AttributeConditionFilter.VALUES_COUNT_EQUAL}, + {label: t('filters.values-count-greater-than'), value: AttributeConditionFilter.VALUES_COUNT_GREATER_THAN}, + {label: t('filters.values-count-lower-than'), value: AttributeConditionFilter.VALUES_COUNT_LOWER_THAN}, + {label: t('filters.through'), value: AttributeConditionFilter.THROUGH} +]; + +export const useConditionsOptionsByType = (filter: IExplorerFilter) => { + const {t} = useSharedTranslation(); + + const attributeConditionOptions = _getAttributeConditionOptions(t); + + return { + conditionOptionsByType: attributeConditionOptions.filter(({value}) => + conditionsByFormat[filter.attribute.format].includes(value) + ) + }; +}; diff --git a/libs/ui/src/components/Explorer/manage-view-settings/store-view-settings/viewSettingsReducer.test.ts b/libs/ui/src/components/Explorer/manage-view-settings/store-view-settings/viewSettingsReducer.test.ts index b5e1b2c8a..63c94b8c2 100644 --- a/libs/ui/src/components/Explorer/manage-view-settings/store-view-settings/viewSettingsReducer.test.ts +++ b/libs/ui/src/components/Explorer/manage-view-settings/store-view-settings/viewSettingsReducer.test.ts @@ -3,7 +3,12 @@ // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt import {IViewSettingsState, ViewSettingsActionTypes, viewSettingsReducer, ViewType} from './viewSettingsReducer'; import {defaultPageSizeOptions, viewSettingsInitialState} from './viewSettingsInitialState'; -import {SortOrder} from '_ui/_gqlTypes'; +import {AttributeFormat, RecordFilterCondition, SortOrder} from '_ui/_gqlTypes'; + +const attributeData = { + label: 'first', + format: AttributeFormat.text +}; describe('ViewSettings Reducer', () => { describe(`Action ${ViewSettingsActionTypes.CHANGE_PAGE_SIZE}`, () => { @@ -275,18 +280,20 @@ describe('ViewSettings Reducer', () => { filters: [ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ] }, { type: ViewSettingsActionTypes.ADD_FILTER, payload: { + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: 'test' } } ); @@ -294,15 +301,17 @@ describe('ViewSettings Reducer', () => { expect(state.filters).toEqual([ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: expect.any(String), + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: 'test' } ]); }); @@ -315,24 +324,27 @@ describe('ViewSettings Reducer', () => { filters: [ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'second-id', + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: 'test' } ] }, { type: ViewSettingsActionTypes.ADD_FILTER, payload: { + attribute: attributeData, field: 'third', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } } ); @@ -340,15 +352,17 @@ describe('ViewSettings Reducer', () => { expect(state.filters).toEqual([ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'second-id', + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: 'test' } ]); }); @@ -363,18 +377,20 @@ describe('ViewSettings Reducer', () => { filters: [ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ] }, { type: ViewSettingsActionTypes.ADD_FILTER, payload: { + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: 'test' } } ); @@ -390,15 +406,17 @@ describe('ViewSettings Reducer', () => { filters: [ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'second-id', + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: 'test' } ] }, @@ -421,21 +439,24 @@ describe('ViewSettings Reducer', () => { filters: [ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'second-id', + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'third-id', + attribute: attributeData, field: 'third', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ] }, @@ -450,15 +471,17 @@ describe('ViewSettings Reducer', () => { expect(state.filters).toEqual([ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'third-id', + attribute: attributeData, field: 'third', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ]); }); @@ -470,15 +493,17 @@ describe('ViewSettings Reducer', () => { filters: [ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'second-id', + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ] }, @@ -486,9 +511,10 @@ describe('ViewSettings Reducer', () => { type: ViewSettingsActionTypes.CHANGE_FILTER_CONFIG, payload: { id: 'id', + attribute: attributeData, field: 'first', - condition: 'less', - values: [] + condition: RecordFilterCondition.LESS_THAN, + value: null } } ); @@ -496,15 +522,17 @@ describe('ViewSettings Reducer', () => { expect(state.filters).toEqual([ { id: 'id', + attribute: attributeData, field: 'first', - condition: 'less', - values: [] + condition: RecordFilterCondition.LESS_THAN, + value: null }, { id: 'second-id', + attribute: attributeData, field: 'second', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ]); }); @@ -515,25 +543,25 @@ describe('ViewSettings Reducer', () => { filters: [ { id: 'id', + attribute: attributeData, field: 'test', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'active-id', + attribute: attributeData, field: 'active', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'created_at-id', + attribute: attributeData, field: 'created_at', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } - // {order: SortOrder.desc, attributeId: 'test'}, - // {order: SortOrder.asc, attributeId: 'active'}, - // {order: SortOrder.asc, attributeId: 'created_at'} ] }; @@ -544,21 +572,24 @@ describe('ViewSettings Reducer', () => { expected: [ { id: 'active-id', + attribute: attributeData, field: 'active', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'created_at-id', + attribute: attributeData, field: 'created_at', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'id', + attribute: attributeData, field: 'test', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ] }, @@ -568,21 +599,24 @@ describe('ViewSettings Reducer', () => { expected: [ { id: 'created_at-id', + attribute: attributeData, field: 'created_at', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'id', + attribute: attributeData, field: 'test', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'active-id', + attribute: attributeData, field: 'active', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ] }, @@ -592,21 +626,24 @@ describe('ViewSettings Reducer', () => { expected: [ { id: 'id', + attribute: attributeData, field: 'test', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'created_at-id', + attribute: attributeData, field: 'created_at', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null }, { id: 'active-id', + attribute: attributeData, field: 'active', - condition: 'eq', - values: [] + condition: RecordFilterCondition.EQUAL, + value: null } ] }, diff --git a/libs/ui/src/components/Explorer/manage-view-settings/store-view-settings/viewSettingsReducer.ts b/libs/ui/src/components/Explorer/manage-view-settings/store-view-settings/viewSettingsReducer.ts index e922bbac9..3ce0b3578 100644 --- a/libs/ui/src/components/Explorer/manage-view-settings/store-view-settings/viewSettingsReducer.ts +++ b/libs/ui/src/components/Explorer/manage-view-settings/store-view-settings/viewSettingsReducer.ts @@ -1,7 +1,8 @@ // Copyright LEAV Solutions 2017 until 2023/11/05, Copyright Aristid from 2023/11/06 // This file is released under LGPL V3 // License text available at https://www.gnu.org/licenses/lgpl-3.0.txt -import {SortOrder} from '_ui/_gqlTypes'; +import {AttributeDetailsFragment, SortOrder} from '_ui/_gqlTypes'; +import {IExplorerFilter} from '../../_types'; export type ViewType = 'table' | 'list' | 'timeline' | 'mosaic'; @@ -24,13 +25,6 @@ export const ViewSettingsActionTypes = { CHANGE_FILTER_CONFIG: 'CHANGE_FILTER_CONFIG' } as const; -export interface IExplorerFilter { - id: string; - field: string; - condition: string; - values: string[]; -} - export interface IViewSettingsState { viewType: ViewType; attributesIds: string[]; @@ -231,9 +225,7 @@ const removeFilter: Reducer = (state const changeFilterConfig: Reducer = (state, payload) => ({ ...state, - filters: state.filters.map(filter => - filter.id === payload.id ? {...filter, condition: payload.condition, values: payload.values} : filter - ) + filters: state.filters.map(filter => (filter.id === payload.id ? {...filter, ...payload} : filter)) }); const moveFilter: Reducer = (state, payload) => { @@ -259,7 +251,6 @@ export type IViewSettingsAction = | IViewSettingsActionChangePageSize | IViewSettingsActionChangeFulltextSearch | IViewSettingsActionClearFulltextSearch - | IViewSettingsActionMoveSort | IViewSettingsActionAddFilter | IViewSettingsActionRemoveFilter | IViewSettingsActionChangeFilterConfig diff --git a/libs/ui/src/locales/en/shared.json b/libs/ui/src/locales/en/shared.json index 71b32a4d9..d5d769c02 100644 --- a/libs/ui/src/locales/en/shared.json +++ b/libs/ui/src/locales/en/shared.json @@ -639,6 +639,9 @@ "active": "Active filters", "inactive": "Inactive filters" }, + "reset-filter": "Reset filter", + "delete-filters": "Delete all filters", + "empty-data": "No items found.", "available-attributes": "Other available attributes", "coming-soon": "Coming soon", "show": "Show", @@ -649,6 +652,7 @@ "pagination-total-number": "{{from, number}} - {{to, number}} of {{total, number}} items", "active-items-number": "{{count, number}} active", "active-items-number_zero": "None", - "invalid-value": "Invalid value" + "invalid-value": "Invalid value", + "type-a-value": "Type a value" } } diff --git a/libs/ui/src/locales/fr/shared.json b/libs/ui/src/locales/fr/shared.json index 81d8382ce..16f577945 100644 --- a/libs/ui/src/locales/fr/shared.json +++ b/libs/ui/src/locales/fr/shared.json @@ -637,6 +637,9 @@ "active": "Filtres actifs", "inactive": "Filtres inactifs" }, + "reset-filter": "Réinitialiser le filtre", + "delete-filters": "Supprimer tous les filtres", + "empty-data": "Aucun élément trouvé.", "sort-ascending": "Ascendant", "sort-descending": "Descendant", "available-attributes": "Autres propriétés disponibles", @@ -649,6 +652,7 @@ "invisible-columns": "Colonnes invisibles", "active-items-number": "{{count, number}} actifs", "active-items-number_zero": "Aucun", - "invalid-value": "Valeur invalide" + "invalid-value": "Valeur invalide", + "type-a-value": "Saisissez une valeur" } }