Skip to content

Commit

Permalink
feat(@leav/ui): explorer numeric, text and rich text filter (#662)
Browse files Browse the repository at this point in the history
Co-authored-by: emile <[email protected]>
  • Loading branch information
evoiron and emile authored Dec 13, 2024
1 parent e865e1b commit 8bd5a2d
Show file tree
Hide file tree
Showing 16 changed files with 683 additions and 206 deletions.
6 changes: 2 additions & 4 deletions libs/ui/src/components/Explorer/Explorer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Explorer library="campaigns" />);

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 () => {
Expand Down
16 changes: 12 additions & 4 deletions libs/ui/src/components/Explorer/Explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = <T extends unknown[]>(union: T): union is Exclude<T, []> => union.length > 0;

Expand Down Expand Up @@ -64,6 +66,8 @@ export const Explorer: FunctionComponent<IExplorerProps> = ({
defaultPrimaryActions = ['create'],
defaultViewSettings
}) => {
const {t} = useSharedTranslation();

const {panelElement} = useEditSettings();

const [view, dispatch] = useReducer(viewSettingsReducer, {
Expand All @@ -79,7 +83,8 @@ export const Explorer: FunctionComponent<IExplorerProps> = ({
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({
Expand Down Expand Up @@ -128,13 +133,16 @@ export const Explorer: FunctionComponent<IExplorerProps> = ({
{primaryButton}
</KitSpace>
</ExplorerHeaderDivStyled>
<ExplorerFilterBar />
{loading ? (
<Loading />
) : data === null ? (
<KitEmpty title={t('explorer.empty-data')} />
) : (
<DataView
dataGroupedFilteredSorted={data?.records ?? []}
dataGroupedFilteredSorted={data.records ?? []}
itemActions={[editAction, deactivateAction, ...(itemActions ?? [])].filter(Boolean)}
attributesProperties={data?.attributes ?? {}}
attributesProperties={data.attributes ?? {}}
attributesToDisplay={['whoAmI', ...view.attributesIds]}
{...dataViewAdditionalProps}
/>
Expand Down
23 changes: 18 additions & 5 deletions libs/ui/src/components/Explorer/_queries/useExplorerData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -44,7 +44,8 @@ export const useExplorerData = ({
attributeIds,
fulltextSearch,
sorts,
pagination
pagination,
filters
}: {
libraryId: string;
attributeIds: string[];
Expand All @@ -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({
Expand All @@ -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
};
Expand Down
23 changes: 22 additions & 1 deletion libs/ui/src/components/Explorer/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -37,3 +43,18 @@ export interface IPrimaryAction {
}

export type ActionHook<T = {}> = {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;
}
Original file line number Diff line number Diff line change
@@ -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 (
<ExplorerFilterBarStyledDiv>
<ExplorerBarItemsListDiv>
<KitSpace size="s">
{filters.map(filter => (
<FilterStyled
key={filter.id}
expandable
label={filter.attribute.label}
values={filter.value === null ? [] : [filter.value]}
dropDownProps={{
placement: 'bottomLeft',
dropdownRender: () => <FilterDropDown filter={filter} />
}}
/>
))}
</KitSpace>
<DividerStyled type="vertical" />
<FilterStyled as={KitButton} type="secondary" size="s" danger icon={<FaTrash />} disabled>
{t('explorer.delete-filters')}
</FilterStyled>
</ExplorerBarItemsListDiv>
</ExplorerFilterBarStyledDiv>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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: () => (
<SimpleFilterDropdown
filter={activeFilter}
attribute={attributeDetailsById[activeFilter.field]}
/>
)
dropdownRender: () => <FilterDropDown filter={activeFilter} />
}
}}
visibilityButtonProps={{
icon: <StyledFaEye />,
title: String(t('explorer.hide')),
onClick: _toggleColumnVisibility(activeFilter.field)
onClick: removeFilter(activeFilter.id)
}}
/>
))}
Expand Down Expand Up @@ -154,7 +150,7 @@ export const FilterItems: FunctionComponent<{libraryId: string}> = ({libraryId})
? {
icon: <StyledEyeSlash />,
title: String(t('explorer.show')),
onClick: _toggleColumnVisibility(attributeId)
onClick: addFilter(attributeId)
}
: undefined
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IFilterDropDownProps> = ({filter}) => {
switch (filter.attribute.format) {
case AttributeFormat.numeric:
return <NumericAttributeDropDown filter={filter} />;
case AttributeFormat.text:
case AttributeFormat.rich_text:
return <TextAttributeDropDown filter={filter} />;
default:
return <SimpleFilterDropdown filter={filter} />;
}
};
Loading

0 comments on commit 8bd5a2d

Please sign in to comment.