Refactor Filters and Search (#119)

This commit is contained in:
Charles Bochet
2023-05-17 13:25:33 +02:00
committed by GitHub
parent 96e3f2c7ea
commit 499752ed6b
25 changed files with 466 additions and 804 deletions

View File

@ -67,7 +67,7 @@
"coverageThreshold": { "coverageThreshold": {
"global": { "global": {
"branches": 70, "branches": 70,
"functions": 80, "functions": 75,
"lines": 80, "lines": 80,
"statements": 80 "statements": 80
} }

View File

@ -9,7 +9,9 @@ import {
import TableHeader from './table-header/TableHeader'; import TableHeader from './table-header/TableHeader';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { import {
FilterType, FilterConfigType,
SearchConfigType,
SearchableType,
SelectedFilterType, SelectedFilterType,
SelectedSortType, SelectedSortType,
SortType, SortType,
@ -21,23 +23,24 @@ declare module 'react' {
): (props: P & React.RefAttributes<T>) => React.ReactElement | null; ): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
} }
type OwnProps<TData, SortField, FilterProperties> = { type OwnProps<TData, SortField> = {
data: Array<TData>; data: Array<TData>;
columns: Array<ColumnDef<TData, any>>; columns: Array<ColumnDef<TData, any>>;
viewName: string; viewName: string;
viewIcon?: React.ReactNode; viewIcon?: React.ReactNode;
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<SortField>>;
availableFilters?: FilterType<FilterProperties>[]; availableFilters?: FilterConfigType<TData>[];
filterSearchResults?: { filterSearchResults?: {
results: { displayValue: string; value: any }[]; results: {
render: (value: SearchableType) => string;
value: SearchableType;
}[];
loading: boolean; loading: boolean;
}; };
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: ( onFiltersUpdate?: (sorts: Array<SelectedFilterType>) => void;
sorts: Array<SelectedFilterType<FilterProperties>>,
) => void;
onFilterSearch?: ( onFilterSearch?: (
filter: FilterType<FilterProperties> | null, filter: SearchConfigType<any> | null,
searchValue: string, searchValue: string,
) => void; ) => void;
onRowSelectionChange?: (rowSelection: string[]) => void; onRowSelectionChange?: (rowSelection: string[]) => void;
@ -94,7 +97,7 @@ const StyledTableScrollableContainer = styled.div`
flex: 1; flex: 1;
`; `;
const Table = <TData extends { id: string }, SortField, FilterProperies>( const Table = <TData extends { id: string }, SortField>(
{ {
data, data,
columns, columns,
@ -107,7 +110,7 @@ const Table = <TData extends { id: string }, SortField, FilterProperies>(
onFiltersUpdate, onFiltersUpdate,
onFilterSearch, onFilterSearch,
onRowSelectionChange, onRowSelectionChange,
}: OwnProps<TData, SortField, FilterProperies>, }: OwnProps<TData, SortField>,
ref: React.ForwardedRef<{ resetRowSelection: () => void } | undefined>, ref: React.ForwardedRef<{ resetRowSelection: () => void } | undefined>,
) => { ) => {
const [internalRowSelection, setInternalRowSelection] = React.useState({}); const [internalRowSelection, setInternalRowSelection] = React.useState({});
@ -144,7 +147,7 @@ const Table = <TData extends { id: string }, SortField, FilterProperies>(
viewName={viewName} viewName={viewName}
viewIcon={viewIcon} viewIcon={viewIcon}
availableSorts={availableSorts} availableSorts={availableSorts}
availableFilters={availableFilters} availableFilters={availableFilters as FilterConfigType<any>[]}
filterSearchResults={filterSearchResults} filterSearchResults={filterSearchResults}
onSortsUpdate={onSortsUpdate} onSortsUpdate={onSortsUpdate}
onFiltersUpdate={onFiltersUpdate} onFiltersUpdate={onFiltersUpdate}

View File

@ -2,8 +2,7 @@ import { ChangeEvent, ComponentType, useState } from 'react';
import EditableCellWrapper from './EditableCellWrapper'; import EditableCellWrapper from './EditableCellWrapper';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useSearch } from '../../../services/search/search'; import { useSearch } from '../../../services/search/search';
import { FilterType } from '../table-header/interface'; import { SearchConfigType, SearchableType } from '../table-header/interface';
import { People_Bool_Exp } from '../../../generated/graphql';
const StyledEditModeContainer = styled.div` const StyledEditModeContainer = styled.div`
width: 200px; width: 200px;
@ -48,10 +47,13 @@ const StyledEditModeResultItem = styled.div`
cursor: pointer; cursor: pointer;
`; `;
export type EditableRelationProps<RelationType, ChipComponentPropsType> = { export type EditableRelationProps<
RelationType extends SearchableType,
ChipComponentPropsType,
> = {
relation?: RelationType | null; relation?: RelationType | null;
searchPlaceholder: string; searchPlaceholder: string;
searchFilter: FilterType<People_Bool_Exp>; searchConfig: SearchConfigType<RelationType>;
changeHandler: (relation: RelationType) => void; changeHandler: (relation: RelationType) => void;
editModeHorizontalAlign?: 'left' | 'right'; editModeHorizontalAlign?: 'left' | 'right';
ChipComponent: ComponentType<ChipComponentPropsType>; ChipComponent: ComponentType<ChipComponentPropsType>;
@ -60,10 +62,13 @@ export type EditableRelationProps<RelationType, ChipComponentPropsType> = {
) => ChipComponentPropsType & JSX.IntrinsicAttributes; ) => ChipComponentPropsType & JSX.IntrinsicAttributes;
}; };
function EditableRelation<RelationType, ChipComponentPropsType>({ function EditableRelation<
RelationType extends SearchableType,
ChipComponentPropsType,
>({
relation, relation,
searchPlaceholder, searchPlaceholder,
searchFilter, searchConfig,
changeHandler, changeHandler,
editModeHorizontalAlign, editModeHorizontalAlign,
ChipComponent, ChipComponent,
@ -72,7 +77,8 @@ function EditableRelation<RelationType, ChipComponentPropsType>({
const [selectedRelation, setSelectedRelation] = useState(relation); const [selectedRelation, setSelectedRelation] = useState(relation);
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch(); const [filterSearchResults, setSearchInput, setFilterSearch] =
useSearch<RelationType>();
return ( return (
<EditableCellWrapper <EditableCellWrapper
@ -97,16 +103,16 @@ function EditableRelation<RelationType, ChipComponentPropsType>({
<StyledEditModeSearchInput <StyledEditModeSearchInput
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterSearch(searchFilter); setFilterSearch(searchConfig);
setSearchInput(event.target.value); setSearchInput(event.target.value);
}} }}
/> />
</StyledEditModeSearchContainer> </StyledEditModeSearchContainer>
<StyledEditModeResults> <StyledEditModeResults>
{filterSearchResults.results && {filterSearchResults.results &&
filterSearchResults.results.map((result) => ( filterSearchResults.results.map((result, index) => (
<StyledEditModeResultItem <StyledEditModeResultItem
key={result.value.id} key={index}
onClick={() => { onClick={() => {
setSelectedRelation(result.value); setSelectedRelation(result.value);
changeHandler(result.value); changeHandler(result.value);

View File

@ -3,16 +3,11 @@ import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes'; import { lightTheme } from '../../../../layout/styles/themes';
import { StoryFn } from '@storybook/react'; import { StoryFn } from '@storybook/react';
import CompanyChip, { CompanyChipPropsType } from '../../../chips/CompanyChip'; import CompanyChip, { CompanyChipPropsType } from '../../../chips/CompanyChip';
import { import { Company, mapCompany } from '../../../../interfaces/company.interface';
GraphqlQueryCompany,
PartialCompany,
} from '../../../../interfaces/company.interface';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { SEARCH_COMPANY_QUERY } from '../../../../services/search/search'; import { SEARCH_COMPANY_QUERY } from '../../../../services/search/search';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { People_Bool_Exp } from '../../../../generated/graphql'; import { SearchConfigType } from '../../table-header/interface';
import { FilterType } from '../../table-header/interface';
import { FaBuilding } from 'react-icons/fa';
const component = { const component = {
title: 'editable-cell/EditableRelation', title: 'editable-cell/EditableRelation',
@ -58,13 +53,13 @@ const mocks = [
]; ];
const Template: StoryFn< const Template: StoryFn<
typeof EditableRelation<PartialCompany, CompanyChipPropsType> typeof EditableRelation<Company, CompanyChipPropsType>
> = (args: EditableRelationProps<PartialCompany, CompanyChipPropsType>) => { > = (args: EditableRelationProps<Company, CompanyChipPropsType>) => {
return ( return (
<MockedProvider mocks={mocks}> <MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<StyledParent data-testid="content-editable-parent"> <StyledParent data-testid="content-editable-parent">
<EditableRelation<PartialCompany, CompanyChipPropsType> {...args} /> <EditableRelation<Company, CompanyChipPropsType> {...args} />
</StyledParent> </StyledParent>
</ThemeProvider> </ThemeProvider>
</MockedProvider> </MockedProvider>
@ -77,36 +72,25 @@ EditableRelationStory.args = {
id: '123', id: '123',
name: 'Heroku', name: 'Heroku',
domain_name: 'heroku.com', domain_name: 'heroku.com',
} as PartialCompany, } as Company,
ChipComponent: CompanyChip, ChipComponent: CompanyChip,
chipComponentPropsMapper: (company: PartialCompany): CompanyChipPropsType => { chipComponentPropsMapper: (company: Company): CompanyChipPropsType => {
return { return {
name: company.name, name: company.name,
picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`, picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`,
}; };
}, },
changeHandler: (relation: PartialCompany) => { changeHandler: (relation: Company) => {
console.log('changed', relation); console.log('changed', relation);
}, },
searchFilter: { searchConfig: {
key: 'company_name', query: SEARCH_COMPANY_QUERY,
label: 'Company', template: (searchInput: string) => ({
icon: <FaBuilding />,
whereTemplate: () => {
return {};
},
searchQuery: SEARCH_COMPANY_QUERY,
searchTemplate: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` }, name: { _ilike: `%${searchInput}%` },
}), }),
searchResultMapper: (company: GraphqlQueryCompany) => ({ resultMapper: (company) => ({
displayValue: company.name, render: (company) => company.name,
value: { value: mapCompany(company),
id: company.id,
name: company.name,
domain_name: company.domain_name,
},
}), }),
operands: [], } satisfies SearchConfigType<Company>,
} satisfies FilterType<People_Bool_Exp>,
}; };

View File

@ -2,17 +2,17 @@ import { fireEvent, render, waitFor } from '@testing-library/react';
import { EditableRelationStory } from '../__stories__/EditableRelation.stories'; import { EditableRelationStory } from '../__stories__/EditableRelation.stories';
import { CompanyChipPropsType } from '../../../chips/CompanyChip'; import { CompanyChipPropsType } from '../../../chips/CompanyChip';
import { PartialCompany } from '../../../../interfaces/company.interface';
import { EditableRelationProps } from '../EditableRelation'; import { EditableRelationProps } from '../EditableRelation';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { Company } from '../../../../interfaces/company.interface';
it('Checks the EditableRelation editing event bubbles up', async () => { it('Checks the EditableRelation editing event bubbles up', async () => {
const func = jest.fn(() => null); const func = jest.fn(() => null);
const { getByTestId, getByText } = render( const { getByTestId, getByText } = render(
<EditableRelationStory <EditableRelationStory
{...(EditableRelationStory.args as EditableRelationProps< {...(EditableRelationStory.args as EditableRelationProps<
PartialCompany, Company,
CompanyChipPropsType CompanyChipPropsType
>)} >)}
changeHandler={func} changeHandler={func}
@ -49,10 +49,16 @@ it('Checks the EditableRelation editing event bubbles up', async () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(func).toBeCalledWith({ expect(func).toBeCalledWith(
domain_name: 'abnb.com', expect.objectContaining({
id: 'abnb', accountOwner: null,
name: 'Airbnb', address: undefined,
}); domain_name: 'abnb.com',
employees: undefined,
id: 'abnb',
name: 'Airbnb',
opportunities: [],
}),
);
}); });
}); });

View File

@ -1,34 +1,43 @@
import { ChangeEvent, useCallback, useState } from 'react'; import { ChangeEvent, useCallback, useState } from 'react';
import DropdownButton from './DropdownButton'; import DropdownButton from './DropdownButton';
import { FilterOperandType, FilterType, SelectedFilterType } from './interface'; import {
FilterConfigType,
FilterOperandType,
SearchConfigType,
SearchableType,
SelectedFilterType,
} from './interface';
type OwnProps<FilterProperties> = { type OwnProps = {
isFilterSelected: boolean; isFilterSelected: boolean;
availableFilters: FilterType<FilterProperties>[]; availableFilters: FilterConfigType[];
filterSearchResults?: { filterSearchResults?: {
results: { displayValue: string; value: any }[]; results: {
render: (value: SearchableType) => string;
value: SearchableType;
}[];
loading: boolean; loading: boolean;
}; };
onFilterSelect: (filter: SelectedFilterType<FilterProperties>) => void; onFilterSelect: (filter: SelectedFilterType) => void;
onFilterSearch: ( onFilterSearch: (
filter: FilterType<FilterProperties> | null, filter: SearchConfigType<any> | null,
searchValue: string, searchValue: string,
) => void; ) => void;
}; };
export function FilterDropdownButton<FilterProperties>({ export function FilterDropdownButton({
availableFilters, availableFilters,
filterSearchResults, filterSearchResults,
onFilterSearch, onFilterSearch,
onFilterSelect, onFilterSelect,
isFilterSelected, isFilterSelected,
}: OwnProps<FilterProperties>) { }: OwnProps) {
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false); const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedFilter, setSelectedFilter] = useState< const [selectedFilter, setSelectedFilter] = useState<
FilterType<FilterProperties> | undefined FilterConfigType | undefined
>(undefined); >(undefined);
const [selectedFilterOperand, setSelectedFilterOperand] = useState< const [selectedFilterOperand, setSelectedFilterOperand] = useState<
@ -57,10 +66,8 @@ export function FilterDropdownButton<FilterProperties>({
); );
const renderSearchResults = ( const renderSearchResults = (
filterSearchResults: NonNullable< filterSearchResults: NonNullable<OwnProps['filterSearchResults']>,
OwnProps<FilterProperties>['filterSearchResults'] selectedFilter: FilterConfigType,
>,
selectedFilter: FilterType<FilterProperties>,
selectedFilterOperand: FilterOperandType, selectedFilterOperand: FilterOperandType,
) => { ) => {
if (filterSearchResults.loading) { if (filterSearchResults.loading) {
@ -70,32 +77,24 @@ export function FilterDropdownButton<FilterProperties>({
</DropdownButton.StyledDropdownItem> </DropdownButton.StyledDropdownItem>
); );
} }
return filterSearchResults.results.map((value, index) => (
return filterSearchResults.results.map((result, index) => (
<DropdownButton.StyledDropdownItem <DropdownButton.StyledDropdownItem
key={`fields-value-${index}`} key={`fields-value-${index}`}
onClick={() => { onClick={() => {
onFilterSelect({ onFilterSelect({
...selectedFilter, key: selectedFilter.key,
key: value.displayValue,
operand: selectedFilterOperand,
searchQuery: selectedFilter.searchQuery,
searchTemplate: selectedFilter.searchTemplate,
whereTemplate: selectedFilter.whereTemplate,
label: selectedFilter.label, label: selectedFilter.label,
value: value.displayValue, value: result.value,
displayValue: result.render(result.value),
icon: selectedFilter.icon, icon: selectedFilter.icon,
where: operand: selectedFilterOperand,
selectedFilter.whereTemplate(
selectedFilterOperand,
value.value,
) || ({} as FilterProperties),
searchResultMapper: selectedFilter.searchResultMapper,
}); });
setIsUnfolded(false); setIsUnfolded(false);
setSelectedFilter(undefined); setSelectedFilter(undefined);
}} }}
> >
{value.displayValue} {result.render(result.value)}
</DropdownButton.StyledDropdownItem> </DropdownButton.StyledDropdownItem>
)); ));
}; };
@ -106,7 +105,7 @@ export function FilterDropdownButton<FilterProperties>({
onClick={() => { onClick={() => {
setSelectedFilter(filter); setSelectedFilter(filter);
setSelectedFilterOperand(filter.operands[0]); setSelectedFilterOperand(filter.operands[0]);
onFilterSearch(filter, ''); onFilterSearch(filter.searchConfig, '');
}} }}
> >
<DropdownButton.StyledIcon>{filter.icon}</DropdownButton.StyledIcon> <DropdownButton.StyledIcon>{filter.icon}</DropdownButton.StyledIcon>
@ -115,7 +114,7 @@ export function FilterDropdownButton<FilterProperties>({
)); ));
function renderFilterDropdown( function renderFilterDropdown(
selectedFilter: FilterType<FilterProperties>, selectedFilter: FilterConfigType,
selectedFilterOperand: FilterOperandType, selectedFilterOperand: FilterOperandType,
) { ) {
return ( return (
@ -133,7 +132,7 @@ export function FilterDropdownButton<FilterProperties>({
type="text" type="text"
placeholder={selectedFilter.label} placeholder={selectedFilter.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange={(event: ChangeEvent<HTMLInputElement>) =>
onFilterSearch(selectedFilter, event.target.value) onFilterSearch(selectedFilter.searchConfig, event.target.value)
} }
/> />
</DropdownButton.StyledSearchField> </DropdownButton.StyledSearchField>

View File

@ -3,13 +3,11 @@ import SortOrFilterChip from './SortOrFilterChip';
import { FaArrowDown, FaArrowUp } from 'react-icons/fa'; import { FaArrowDown, FaArrowUp } from 'react-icons/fa';
import { SelectedFilterType, SelectedSortType } from './interface'; import { SelectedFilterType, SelectedSortType } from './interface';
type OwnProps<SortField, FilterProperties> = { type OwnProps<SortField> = {
sorts: Array<SelectedSortType<SortField>>; sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void; onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
filters: Array<SelectedFilterType<FilterProperties>>; filters: Array<SelectedFilterType>;
onRemoveFilter: ( onRemoveFilter: (filterId: SelectedFilterType['key']) => void;
filterId: SelectedFilterType<FilterProperties>['key'],
) => void;
onCancelClick: () => void; onCancelClick: () => void;
}; };
@ -42,13 +40,13 @@ const StyledCancelButton = styled.button`
} }
`; `;
function SortAndFilterBar<SortField, FilterProperties>({ function SortAndFilterBar<SortField>({
sorts, sorts,
onRemoveSort, onRemoveSort,
filters, filters,
onRemoveFilter, onRemoveFilter,
onCancelClick, onCancelClick,
}: OwnProps<SortField, FilterProperties>) { }: OwnProps<SortField>) {
return ( return (
<StyledBar> <StyledBar>
{sorts.map((sort) => { {sorts.map((sort) => {
@ -67,7 +65,7 @@ function SortAndFilterBar<SortField, FilterProperties>({
<SortOrFilterChip <SortOrFilterChip
key={filter.key} key={filter.key}
labelKey={filter.label} labelKey={filter.label}
labelValue={`${filter.operand.label} ${filter.value}`} labelValue={`${filter.operand.label} ${filter.displayValue}`}
id={filter.key} id={filter.key}
icon={filter.icon} icon={filter.icon}
onRemove={() => onRemoveFilter(filter.key)} onRemove={() => onRemoveFilter(filter.key)}

View File

@ -1,6 +1,8 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { import {
FilterType, FilterConfigType,
SearchConfigType,
SearchableType,
SelectedFilterType, SelectedFilterType,
SelectedSortType, SelectedSortType,
SortType, SortType,
@ -10,21 +12,22 @@ import { SortDropdownButton } from './SortDropdownButton';
import { FilterDropdownButton } from './FilterDropdownButton'; import { FilterDropdownButton } from './FilterDropdownButton';
import SortAndFilterBar from './SortAndFilterBar'; import SortAndFilterBar from './SortAndFilterBar';
type OwnProps<SortField, FilterProperties> = { type OwnProps<SortField> = {
viewName: string; viewName: string;
viewIcon?: ReactNode; viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<SortField>>;
availableFilters?: FilterType<FilterProperties>[]; availableFilters?: FilterConfigType[];
filterSearchResults?: { filterSearchResults?: {
results: { displayValue: string; value: any }[]; results: {
render: (value: SearchableType) => string;
value: SearchableType;
}[];
loading: boolean; loading: boolean;
}; };
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: ( onFiltersUpdate?: (sorts: Array<SelectedFilterType>) => void;
sorts: Array<SelectedFilterType<FilterProperties>>,
) => void;
onFilterSearch?: ( onFilterSearch?: (
filter: FilterType<FilterProperties> | null, filter: SearchConfigType<any> | null,
searchValue: string, searchValue: string,
) => void; ) => void;
}; };
@ -65,7 +68,7 @@ const StyledFilters = styled.div`
margin-right: ${(props) => props.theme.spacing(2)}; margin-right: ${(props) => props.theme.spacing(2)};
`; `;
function TableHeader<SortField, FilterProperties>({ function TableHeader<SortField>({
viewName, viewName,
viewIcon, viewIcon,
availableSorts, availableSorts,
@ -74,13 +77,11 @@ function TableHeader<SortField, FilterProperties>({
onSortsUpdate, onSortsUpdate,
onFiltersUpdate, onFiltersUpdate,
onFilterSearch, onFilterSearch,
}: OwnProps<SortField, FilterProperties>) { }: OwnProps<SortField>) {
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>( const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
[], [],
); );
const [filters, innerSetFilters] = useState< const [filters, innerSetFilters] = useState<Array<SelectedFilterType>>([]);
Array<SelectedFilterType<FilterProperties>>
>([]);
const sortSelect = useCallback( const sortSelect = useCallback(
(newSort: SelectedSortType<SortField>) => { (newSort: SelectedSortType<SortField>) => {
@ -101,7 +102,7 @@ function TableHeader<SortField, FilterProperties>({
); );
const filterSelect = useCallback( const filterSelect = useCallback(
(filter: SelectedFilterType<FilterProperties>) => { (filter: SelectedFilterType) => {
const newFilters = updateSortOrFilterByKey(filters, filter); const newFilters = updateSortOrFilterByKey(filters, filter);
innerSetFilters(newFilters); innerSetFilters(newFilters);
@ -111,7 +112,7 @@ function TableHeader<SortField, FilterProperties>({
); );
const filterUnselect = useCallback( const filterUnselect = useCallback(
(filterId: SelectedFilterType<FilterProperties>['key']) => { (filterId: SelectedFilterType['key']) => {
const newFilters = filters.filter((filter) => filter.key !== filterId); const newFilters = filters.filter((filter) => filter.key !== filterId);
innerSetFilters(newFilters); innerSetFilters(newFilters);
onFiltersUpdate && onFiltersUpdate(newFilters); onFiltersUpdate && onFiltersUpdate(newFilters);
@ -120,7 +121,7 @@ function TableHeader<SortField, FilterProperties>({
); );
const filterSearch = useCallback( const filterSearch = useCallback(
(filter: FilterType<FilterProperties> | null, searchValue: string) => { (filter: SearchConfigType<any> | null, searchValue: string) => {
onFilterSearch && onFilterSearch(filter, searchValue); onFilterSearch && onFilterSearch(filter, searchValue);
}, },
[onFilterSearch], [onFilterSearch],

View File

@ -2,16 +2,16 @@ import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes'; import { lightTheme } from '../../../../layout/styles/themes';
import { FilterDropdownButton } from '../FilterDropdownButton'; import { FilterDropdownButton } from '../FilterDropdownButton';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { FilterType, SelectedFilterType } from '../interface'; import { FilterConfigType, SelectedFilterType } from '../interface';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { People_Bool_Exp } from '../../../../generated/graphql';
import { FaUsers } from 'react-icons/fa';
import { import {
SEARCH_PEOPLE_QUERY, SEARCH_PEOPLE_QUERY,
useSearch, useSearch,
} from '../../../../services/search/search'; } from '../../../../services/search/search';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mockData } from '../../../../pages/people/__tests__/__data__/mock-data'; import { mockData } from '../../../../pages/people/__tests__/__data__/mock-data';
import { availableFilters } from '../../../../pages/people/people-table';
import { Person } from '../../../../interfaces/person.interface';
const component = { const component = {
title: 'FilterDropdownButton', title: 'FilterDropdownButton',
@ -78,35 +78,6 @@ const mocks = [
}, },
]; ];
const availableFilters = [
{
key: 'fullname',
label: 'People',
icon: <FaUsers />,
searchQuery: SEARCH_PEOPLE_QUERY,
searchTemplate: (searchInput: string) => ({
_or: [
{ firstname: { _ilike: `%${searchInput}%` } },
{ lastname: { _ilike: `%${searchInput}%` } },
],
}),
whereTemplate: () => ({
_or: [
{ firstname: { _ilike: 'value' } },
{ lastname: { _ilike: 'value' } },
],
}),
searchResultMapper: (data) => ({
displayValue: data.firstname + ' ' + data.lastname,
value: data.firstname,
}),
operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
],
},
] satisfies FilterType<People_Bool_Exp>[];
const StyleDiv = styled.div` const StyleDiv = styled.div`
height: 200px; height: 200px;
width: 200px; width: 200px;
@ -114,12 +85,12 @@ const StyleDiv = styled.div`
const InnerRegularFilterDropdownButton = ({ const InnerRegularFilterDropdownButton = ({
setFilter: setFilters, setFilter: setFilters,
}: OwnProps<People_Bool_Exp>) => { }: OwnProps<Person>) => {
const [, innerSetFilters] = useState<SelectedFilterType<People_Bool_Exp>>(); const [, innerSetFilters] = useState<SelectedFilterType<Person>>();
const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch(); const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch();
const outerSetFilters = useCallback( const outerSetFilters = useCallback(
(filter: SelectedFilterType<People_Bool_Exp>) => { (filter: SelectedFilterType<Person>) => {
innerSetFilters(filter); innerSetFilters(filter);
setFilters(filter); setFilters(filter);
}, },
@ -128,7 +99,7 @@ const InnerRegularFilterDropdownButton = ({
return ( return (
<StyleDiv> <StyleDiv>
<FilterDropdownButton <FilterDropdownButton
availableFilters={availableFilters} availableFilters={availableFilters as FilterConfigType[]}
isFilterSelected={true} isFilterSelected={true}
onFilterSelect={outerSetFilters} onFilterSelect={outerSetFilters}
filterSearchResults={filterSearchResults} filterSearchResults={filterSearchResults}
@ -143,7 +114,7 @@ const InnerRegularFilterDropdownButton = ({
export const RegularFilterDropdownButton = ({ export const RegularFilterDropdownButton = ({
setFilter: setFilters, setFilter: setFilters,
}: OwnProps<People_Bool_Exp>) => { }: OwnProps<Person>) => {
return ( return (
<MockedProvider mocks={mocks}> <MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>

View File

@ -1,8 +1,8 @@
import SortAndFilterBar from '../SortAndFilterBar'; import SortAndFilterBar from '../SortAndFilterBar';
import { ThemeProvider } from '@emotion/react'; import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes'; import { lightTheme } from '../../../../layout/styles/themes';
import { GET_PEOPLE } from '../../../../services/people';
import { FaArrowDown } from 'react-icons/fa'; import { FaArrowDown } from 'react-icons/fa';
import { SelectedFilterType } from '../interface';
const component = { const component = {
title: 'SortAndFilterBar', title: 'SortAndFilterBar',
@ -45,26 +45,28 @@ export const RegularSortAndFilterBar = ({
filters={[ filters={[
{ {
label: 'People', label: 'People',
operand: { label: 'Include', id: 'include', keyWord: 'ilike' }, operand: {
label: 'Include',
id: 'include',
whereTemplate: (person) => {
return { email: { _eq: person.email } };
},
},
key: 'test_filter', key: 'test_filter',
icon: <FaArrowDown />, icon: <FaArrowDown />,
value: 'John Doe', displayValue: 'john@doedoe.com',
where: { value: {
firstname: { _ilike: 'John Doe' }, id: 'test',
email: 'john@doedoe.com',
firstname: 'John',
lastname: 'Doe',
phone: '123456789',
company: null,
creationDate: new Date(),
pipe: null,
city: 'Paris',
}, },
searchQuery: GET_PEOPLE, } satisfies SelectedFilterType,
searchTemplate: () => ({
firstname: { _ilike: 'John Doe' },
}),
whereTemplate: () => {
return { firstname: { _ilike: 'John Doe' } };
},
searchResultMapper: (data) => ({
displayValue: 'John Doe',
value: data.firstname,
}),
operands: [],
},
]} ]}
/> />
</ThemeProvider> </ThemeProvider>

View File

@ -1,6 +1,5 @@
import { fireEvent, render, waitFor } from '@testing-library/react'; import { fireEvent, render, waitFor } from '@testing-library/react';
import { RegularFilterDropdownButton } from '../__stories__/FilterDropdownButton.stories'; import { RegularFilterDropdownButton } from '../__stories__/FilterDropdownButton.stories';
import { FaUsers } from 'react-icons/fa';
it('Checks the default top option is Include', async () => { it('Checks the default top option is Include', async () => {
const setFilters = jest.fn(); const setFilters = jest.fn();
@ -24,15 +23,9 @@ it('Checks the default top option is Include', async () => {
expect(setFilters).toHaveBeenCalledWith( expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
key: 'Alexandre Prot', displayValue: 'Alexandre Prot',
value: 'Alexandre Prot', key: 'fullname',
label: 'People', label: 'People',
operand: {
id: 'equal',
keyWord: 'equal',
label: 'Equal',
},
icon: <FaUsers />,
}), }),
); );
}); });
@ -65,15 +58,9 @@ it('Checks the selection of top option for Not Equal', async () => {
expect(setFilters).toHaveBeenCalledWith( expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
key: 'Alexandre Prot', key: 'fullname',
value: 'Alexandre Prot', displayValue: 'Alexandre Prot',
label: 'People', label: 'People',
operand: {
id: 'not-equal',
keyWord: 'not_equal',
label: 'Not equal',
},
icon: <FaUsers />,
}), }),
); );
const blueSortDropdownButton = getByText('Filter'); const blueSortDropdownButton = getByText('Filter');
@ -118,15 +105,9 @@ it('Calls the filters when typing a new name', async () => {
expect(setFilters).toHaveBeenCalledWith( expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
key: 'Jane Doe', key: 'fullname',
value: 'Jane Doe', displayValue: 'Jane Doe',
label: 'People', label: 'People',
operand: {
id: 'equal',
keyWord: 'equal',
label: 'Equal',
},
icon: <FaUsers />,
}), }),
); );
const blueSortDropdownButton = getByText('Filter'); const blueSortDropdownButton = getByText('Filter');

View File

@ -1,13 +1,12 @@
import { Order_By } from '../../../generated/graphql'; import { Order_By } from '../../../generated/graphql';
import { SelectedFilterType, SelectedSortType } from './interface'; import { BoolExpType, SelectedFilterType, SelectedSortType } from './interface';
export const reduceFiltersToWhere = <T>( export const reduceFiltersToWhere = <ValueType, WhereTemplateType>(
filters: Array<SelectedFilterType<T>>, filters: Array<SelectedFilterType<ValueType, WhereTemplateType>>,
): T => { ): BoolExpType<WhereTemplateType> => {
const where = filters.reduce((acc, filter) => { const where = filters.reduce((acc, filter) => {
const { where } = filter; return { ...acc, ...filter.operand.whereTemplate(filter.value) };
return { ...acc, ...where }; }, {} as BoolExpType<WhereTemplateType>);
}, {} as T);
return where; return where;
}; };

View File

@ -6,6 +6,15 @@ import {
People_Bool_Exp, People_Bool_Exp,
Users_Bool_Exp, Users_Bool_Exp,
} from '../../../generated/graphql'; } from '../../../generated/graphql';
import {
Company,
GraphqlQueryCompany,
} from '../../../interfaces/company.interface';
import {
GraphqlQueryPerson,
Person,
} from '../../../interfaces/person.interface';
import { GraphqlQueryUser, User } from '../../../interfaces/user.interface';
export type SortType<OrderByTemplate> = export type SortType<OrderByTemplate> =
| { | {
@ -26,33 +35,64 @@ export type SelectedSortType<OrderByTemplate> = SortType<OrderByTemplate> & {
order: 'asc' | 'desc'; order: 'asc' | 'desc';
}; };
export type FilterType<WhereTemplate, FilterValue = Record<string, any>> = { export type FilterableFieldsType = Person | Company;
operands: FilterOperandType[]; export type FilterWhereType = Person | Company | User;
label: string;
type FilterConfigGqlType<WhereType> = WhereType extends Company
? GraphqlQueryCompany
: WhereType extends Person
? GraphqlQueryPerson
: WhereType extends User
? GraphqlQueryUser
: never;
export type BoolExpType<T> = T extends Company
? Companies_Bool_Exp
: T extends Person
? People_Bool_Exp
: never;
export type FilterConfigType<FilteredType = any, WhereType = any> = {
key: string; key: string;
label: string;
icon: ReactNode; icon: ReactNode;
whereTemplate: ( operands: FilterOperandType<FilteredType, WhereType>[];
operand: FilterOperandType, searchConfig: WhereType extends SearchableType
value: FilterValue, ? SearchConfigType<WhereType>
) => WhereTemplate | undefined; : null;
searchQuery: DocumentNode; selectedValueRender: (selected: WhereType) => string;
searchTemplate: ( };
export type SearchableType = Person | Company | User;
export type SearchConfigType<SearchType extends SearchableType> = {
query: DocumentNode;
template: (
searchInput: string, searchInput: string,
) => People_Bool_Exp | Companies_Bool_Exp | Users_Bool_Exp; ) => People_Bool_Exp | Companies_Bool_Exp | Users_Bool_Exp;
searchResultMapper: (data: any) => { resultMapper: (data: FilterConfigGqlType<SearchType>) => {
displayValue: string; value: SearchType;
value: FilterValue; render: (value: SearchType) => ReactNode;
}; };
}; };
export type FilterOperandType = { export type FilterOperandType<
FilteredType = FilterableFieldsType,
WhereType = any,
> = {
label: string; label: string;
id: string; id: string;
keyWord: 'ilike' | 'not_ilike' | 'equal' | 'not_equal'; whereTemplate: (value: WhereType) => BoolExpType<FilteredType>;
}; };
export type SelectedFilterType<WhereTemplate> = FilterType<WhereTemplate> & { export type SelectedFilterType<
value: string; FilteredType = FilterableFieldsType,
operand: FilterOperandType; WhereType = any,
where: WhereTemplate; > = {
key: string;
value: WhereType;
displayValue: string;
label: string;
icon: ReactNode;
operand: FilterOperandType<FilteredType, WhereType>;
}; };

View File

@ -17,9 +17,6 @@ export type Company = {
creationDate: Date; creationDate: Date;
}; };
export type PartialCompany = Partial<Company> &
Pick<Company, 'id' | 'name' | 'domain_name'>;
export type GraphqlQueryCompany = { export type GraphqlQueryCompany = {
id: string; id: string;
name: string; name: string;

View File

@ -15,6 +15,10 @@ describe('mapPerson', () => {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
name: '', name: '',
domain_name: '', domain_name: '',
employees: 0,
address: '',
created_at: '',
account_owner: null,
}, },
__typename: '', __typename: '',
}); });

View File

@ -1,4 +1,4 @@
import { PartialCompany } from './company.interface'; import { Company, GraphqlQueryCompany, mapCompany } from './company.interface';
import { Pipe } from './pipe.interface'; import { Pipe } from './pipe.interface';
export type Person = { export type Person = {
@ -7,7 +7,7 @@ export type Person = {
lastname: string; lastname: string;
picture?: string; picture?: string;
email: string; email: string;
company: PartialCompany | null; company: Company | null;
phone: string; phone: string;
creationDate: Date; creationDate: Date;
pipe: Pipe | null; pipe: Pipe | null;
@ -16,12 +16,7 @@ export type Person = {
export type GraphqlQueryPerson = { export type GraphqlQueryPerson = {
city: string; city: string;
company: { company: GraphqlQueryCompany | null;
__typename: string;
id: string;
name: string;
domain_name: string;
};
created_at: string; created_at: string;
email: string; email: string;
firstname: string; firstname: string;
@ -56,13 +51,7 @@ export const mapPerson = (person: GraphqlQueryPerson): Person => ({
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
icon: '💰', icon: '💰',
}, },
company: person.company company: person.company ? mapCompany(person.company) : null,
? {
id: person.company.id,
name: person.company.name,
domain_name: person.company.domain_name,
}
: null,
}); });
export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({ export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({

View File

@ -25,7 +25,10 @@ import {
Companies_Bool_Exp, Companies_Bool_Exp,
Companies_Order_By, Companies_Order_By,
} from '../../generated/graphql'; } from '../../generated/graphql';
import { SelectedFilterType } from '../../components/table/table-header/interface'; import {
FilterConfigType,
SelectedFilterType,
} from '../../components/table/table-header/interface';
import { useSearch } from '../../services/search/search'; import { useSearch } from '../../services/search/search';
import ActionBar from '../../components/table/action-bar/ActionBar'; import ActionBar from '../../components/table/action-bar/ActionBar';
@ -47,7 +50,7 @@ function Companies() {
}, []); }, []);
const updateFilters = useCallback( const updateFilters = useCallback(
(filters: Array<SelectedFilterType<Companies_Bool_Exp>>) => { (filters: Array<SelectedFilterType<Company>>) => {
setWhere(reduceFiltersToWhere(filters)); setWhere(reduceFiltersToWhere(filters));
}, },
[], [],
@ -108,7 +111,7 @@ function Companies() {
viewName="All Companies" viewName="All Companies"
viewIcon={<FaList />} viewIcon={<FaList />}
availableSorts={availableSorts} availableSorts={availableSorts}
availableFilters={availableFilters} availableFilters={availableFilters as Array<FilterConfigType>}
filterSearchResults={filterSearchResults} filterSearchResults={filterSearchResults}
onSortsUpdate={updateSorts} onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters} onFiltersUpdate={updateFilters}

View File

@ -1,53 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CompaniesFilter should render the filter company_name 1`] = `
Object {
"name": Object {
"_eq": "Airbnb",
},
}
`;
exports[`CompaniesFilter should render the filter company_name 2`] = `
Object {
"_not": Object {
"name": Object {
"_eq": "Airbnb",
},
},
}
`;
exports[`CompaniesFilter should render the filter domainName 1`] = `
Object {
"domain_name": Object {
"_eq": "airbnb.com",
},
}
`;
exports[`CompaniesFilter should render the filter domainName 2`] = `
Object {
"_not": Object {
"domain_name": Object {
"_eq": "airbnb.com",
},
},
}
`;
exports[`CompaniesFilter should render the serch company_name with the searchValue 1`] = `
Object {
"name": Object {
"_ilike": "%Search value%",
},
}
`;
exports[`CompaniesFilter should render the serch domainName with the searchValue 1`] = `
Object {
"domain_name": Object {
"_ilike": "%Search value%",
},
}
`;

View File

@ -1,62 +0,0 @@
import { FilterType } from '../../../components/table/table-header/interface';
import { Companies_Bool_Exp } from '../../../generated/graphql';
import { GraphqlQueryCompany } from '../../../interfaces/company.interface';
import { GraphqlQueryPerson } from '../../../interfaces/person.interface';
import {
SEARCH_COMPANY_QUERY,
SEARCH_PEOPLE_QUERY,
} from '../../../services/search/search';
import { mockData } from './__data__/mock-data';
import { availableFilters } from '../companies-table';
function assertFilterUseCompanySearch<FilterValue>(
filter: FilterType<Companies_Bool_Exp>,
): filter is FilterType<Companies_Bool_Exp> & {
searchResultMapper: (data: GraphqlQueryCompany) => {
displayValue: string;
value: FilterValue;
};
} {
return filter.searchQuery === SEARCH_COMPANY_QUERY;
}
function assertFilterUsePeopleSearch<FilterValue>(
filter: FilterType<Companies_Bool_Exp>,
): filter is FilterType<Companies_Bool_Exp> & {
searchResultMapper: (data: GraphqlQueryPerson) => {
displayValue: string;
value: FilterValue;
};
} {
return filter.searchQuery === SEARCH_PEOPLE_QUERY;
}
const AirbnbCompany = mockData.find(
(user) => user.name === 'Airbnb',
) as GraphqlQueryCompany;
describe('CompaniesFilter', () => {
for (const filter of availableFilters) {
it(`should render the filter ${filter.key}`, () => {
if (assertFilterUseCompanySearch(filter)) {
const filterSelectedValue = filter.searchResultMapper(mockData[0]);
for (const operand of filter.operands) {
expect(
filter.whereTemplate(operand, filterSelectedValue.value),
).toMatchSnapshot();
}
}
if (assertFilterUsePeopleSearch(filter)) {
const filterSelectedValue = filter.searchResultMapper(AirbnbCompany);
for (const operand of filter.operands) {
expect(
filter.whereTemplate(operand, filterSelectedValue.value),
).toMatchSnapshot();
}
}
});
it(`should render the serch ${filter.key} with the searchValue`, () => {
expect(filter.searchTemplate('Search value')).toMatchSnapshot();
});
}
});

View File

@ -1,8 +1,5 @@
import { CellContext, createColumnHelper } from '@tanstack/react-table'; import { CellContext, createColumnHelper } from '@tanstack/react-table';
import { import { Company, mapCompany } from '../../interfaces/company.interface';
Company,
GraphqlQueryCompany,
} from '../../interfaces/company.interface';
import { updateCompany } from '../../services/companies'; import { updateCompany } from '../../services/companies';
import ColumnHead from '../../components/table/ColumnHead'; import ColumnHead from '../../components/table/ColumnHead';
import CompanyChip from '../../components/chips/CompanyChip'; import CompanyChip from '../../components/chips/CompanyChip';
@ -15,28 +12,24 @@ import {
FaRegUser, FaRegUser,
FaUsers, FaUsers,
FaBuilding, FaBuilding,
FaUser,
} from 'react-icons/fa'; } from 'react-icons/fa';
import PersonChip, { import PersonChip, {
PersonChipPropsType, PersonChipPropsType,
} from '../../components/chips/PersonChip'; } from '../../components/chips/PersonChip';
import EditableChip from '../../components/table/editable-cell/EditableChip'; import EditableChip from '../../components/table/editable-cell/EditableChip';
import { import {
FilterType, FilterConfigType,
SearchConfigType,
SortType, SortType,
} from '../../components/table/table-header/interface'; } from '../../components/table/table-header/interface';
import { import { Companies_Order_By } from '../../generated/graphql';
Companies_Bool_Exp,
Companies_Order_By,
Users_Bool_Exp,
} from '../../generated/graphql';
import { import {
SEARCH_COMPANY_QUERY, SEARCH_COMPANY_QUERY,
SEARCH_USER_QUERY, SEARCH_USER_QUERY,
} from '../../services/search/search'; } from '../../services/search/search';
import EditableDate from '../../components/table/editable-cell/EditableDate'; import EditableDate from '../../components/table/editable-cell/EditableDate';
import EditableRelation from '../../components/table/editable-cell/EditableRelation'; import EditableRelation from '../../components/table/editable-cell/EditableRelation';
import { GraphqlQueryUser, PartialUser } from '../../interfaces/user.interface'; import { User, mapUser } from '../../interfaces/user.interface';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox'; import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
import Checkbox from '../../components/form/Checkbox'; import Checkbox from '../../components/form/Checkbox';
@ -79,63 +72,67 @@ export const availableFilters = [
key: 'company_name', key: 'company_name',
label: 'Company', label: 'Company',
icon: <FaBuilding />, icon: <FaBuilding />,
whereTemplate: (operand, { companyName }) => { searchConfig: {
if (operand.keyWord === 'equal') { query: SEARCH_COMPANY_QUERY,
return { template: (searchInput) => ({
name: { _eq: companyName }, name: { _ilike: `%${searchInput}%` },
}; }),
} resultMapper: (company) => ({
render: (company) => company.name,
if (operand.keyWord === 'not_equal') { value: mapCompany(company),
return { }),
_not: { name: { _eq: companyName } },
};
}
}, },
searchQuery: SEARCH_COMPANY_QUERY, selectedValueRender: (company) => company.name,
searchTemplate: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (company: GraphqlQueryCompany) => ({
displayValue: company.name,
value: { companyName: company.name },
}),
operands: [ operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' }, {
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' }, label: 'Equal',
id: 'equal',
whereTemplate: (company) => ({
name: { _eq: company.name },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (company) => ({
_not: { name: { _eq: company.name } },
}),
},
], ],
}, } as FilterConfigType<Company, Company>,
{ {
key: 'domainName', key: 'company_domain_name',
label: 'Url', label: 'Url',
icon: <FaLink />, icon: <FaLink />,
whereTemplate: (operand, { domainName }) => { searchConfig: {
if (operand.keyWord === 'equal') { query: SEARCH_COMPANY_QUERY,
return { template: (searchInput) => ({
domain_name: { _eq: domainName }, name: { _ilike: `%${searchInput}%` },
}; }),
} resultMapper: (company) => ({
render: (company) => company.domain_name,
if (operand.keyWord === 'not_equal') { value: mapCompany(company),
return { }),
_not: { domain_name: { _eq: domainName } },
};
}
}, },
searchQuery: SEARCH_COMPANY_QUERY, selectedValueRender: (company) => company.domain_name,
searchTemplate: (searchInput: string) => ({
domain_name: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (company: GraphqlQueryCompany) => ({
displayValue: company.domain_name,
value: { domainName: company.domain_name },
}),
operands: [ operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' }, {
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' }, label: 'Equal',
id: 'equal',
whereTemplate: (company) => ({
domain_name: { _eq: company.domain_name },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (company) => ({
_not: { domain_name: { _eq: company.domain_name } },
}),
},
], ],
}, } as FilterConfigType<Company, Company>,
] satisfies Array<FilterType<Companies_Bool_Exp>>; ];
const columnHelper = createColumnHelper<Company>(); const columnHelper = createColumnHelper<Company>();
@ -239,18 +236,18 @@ export const useCompaniesColumns = () => {
<ColumnHead viewName="Account Owner" viewIcon={<FaRegUser />} /> <ColumnHead viewName="Account Owner" viewIcon={<FaRegUser />} />
), ),
cell: (props) => ( cell: (props) => (
<EditableRelation<PartialUser, PersonChipPropsType> <EditableRelation<User, PersonChipPropsType>
relation={props.row.original.accountOwner} relation={props.row.original.accountOwner}
searchPlaceholder="Account Owner" searchPlaceholder="Account Owner"
ChipComponent={PersonChip} ChipComponent={PersonChip}
chipComponentPropsMapper={( chipComponentPropsMapper={(
accountOwner: PartialUser, accountOwner: User,
): PersonChipPropsType => { ): PersonChipPropsType => {
return { return {
name: accountOwner.displayName, name: accountOwner.displayName,
}; };
}} }}
changeHandler={(relation: PartialUser) => { changeHandler={(relation: User) => {
const company = props.row.original; const company = props.row.original;
if (company.accountOwner) { if (company.accountOwner) {
company.accountOwner.id = relation.id; company.accountOwner.id = relation.id;
@ -263,28 +260,17 @@ export const useCompaniesColumns = () => {
} }
updateCompany(company); updateCompany(company);
}} }}
searchFilter={ searchConfig={
{ {
key: 'account_owner_name', query: SEARCH_USER_QUERY,
label: 'Account Owner', template: (searchInput: string) => ({
icon: <FaUser />,
whereTemplate: () => {
return {};
},
searchQuery: SEARCH_USER_QUERY,
searchTemplate: (searchInput: string) => ({
displayName: { _ilike: `%${searchInput}%` }, displayName: { _ilike: `%${searchInput}%` },
}), }),
searchResultMapper: (accountOwner: GraphqlQueryUser) => ({ resultMapper: (accountOwner) => ({
displayValue: accountOwner.displayName, render: (accountOwner) => accountOwner.displayName,
value: { value: mapUser(accountOwner),
id: accountOwner.id,
email: accountOwner.email,
displayName: accountOwner.displayName,
},
}), }),
operands: [], } satisfies SearchConfigType<User>
} satisfies FilterType<Users_Bool_Exp>
} }
/> />
), ),

View File

@ -19,7 +19,10 @@ import {
} from '../../services/people'; } from '../../services/people';
import { useSearch } from '../../services/search/search'; import { useSearch } from '../../services/search/search';
import { People_Bool_Exp } from '../../generated/graphql'; import { People_Bool_Exp } from '../../generated/graphql';
import { SelectedFilterType } from '../../components/table/table-header/interface'; import {
FilterConfigType,
SelectedFilterType,
} from '../../components/table/table-header/interface';
import { import {
reduceFiltersToWhere, reduceFiltersToWhere,
reduceSortsToOrderBy, reduceSortsToOrderBy,
@ -44,7 +47,7 @@ function People() {
}, []); }, []);
const updateFilters = useCallback( const updateFilters = useCallback(
(filters: Array<SelectedFilterType<People_Bool_Exp>>) => { (filters: Array<SelectedFilterType<Person>>) => {
setWhere(reduceFiltersToWhere(filters)); setWhere(reduceFiltersToWhere(filters));
}, },
[], [],
@ -106,7 +109,7 @@ function People() {
viewName="All People" viewName="All People"
viewIcon={<FaList />} viewIcon={<FaList />}
availableSorts={availableSorts} availableSorts={availableSorts}
availableFilters={availableFilters} availableFilters={availableFilters as Array<FilterConfigType>}
filterSearchResults={filterSearchResults} filterSearchResults={filterSearchResults}
onSortsUpdate={updateSorts} onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters} onFiltersUpdate={updateFilters}

View File

@ -7,130 +7,3 @@ Object {
}, },
} }
`; `;
exports[`PeopleFilter should render the filter city 2`] = `
Object {
"_not": Object {
"city": Object {
"_eq": "Paris",
},
},
}
`;
exports[`PeopleFilter should render the filter company_name 1`] = `
Object {
"company": Object {
"name": Object {
"_eq": "Airbnb",
},
},
}
`;
exports[`PeopleFilter should render the filter company_name 2`] = `
Object {
"_not": Object {
"company": Object {
"name": Object {
"_eq": "Airbnb",
},
},
},
}
`;
exports[`PeopleFilter should render the filter email 1`] = `
Object {
"email": Object {
"_eq": "john@linkedin.com",
},
}
`;
exports[`PeopleFilter should render the filter email 2`] = `
Object {
"_not": Object {
"email": Object {
"_eq": "john@linkedin.com",
},
},
}
`;
exports[`PeopleFilter should render the filter fullname 1`] = `
Object {
"_and": Array [
Object {
"firstname": Object {
"_eq": "John",
},
},
Object {
"lastname": Object {
"_eq": "Doe",
},
},
],
}
`;
exports[`PeopleFilter should render the filter fullname 2`] = `
Object {
"_not": Object {
"_and": Array [
Object {
"firstname": Object {
"_eq": "John",
},
},
Object {
"lastname": Object {
"_eq": "Doe",
},
},
],
},
}
`;
exports[`PeopleFilter should render the serch city with the searchValue 1`] = `
Object {
"city": Object {
"_ilike": "%Search value%",
},
}
`;
exports[`PeopleFilter should render the serch company_name with the searchValue 1`] = `
Object {
"name": Object {
"_ilike": "%Search value%",
},
}
`;
exports[`PeopleFilter should render the serch email with the searchValue 1`] = `
Object {
"email": Object {
"_ilike": "%Search value%",
},
}
`;
exports[`PeopleFilter should render the serch fullname with the searchValue 1`] = `
Object {
"_or": Array [
Object {
"firstname": Object {
"_ilike": "%Search value%",
},
},
Object {
"lastname": Object {
"_ilike": "%Search value%",
},
},
],
}
`;

View File

@ -1,65 +1,19 @@
import { FilterType } from '../../../components/table/table-header/interface'; import { cityFilter } from '../people-table';
import { People_Bool_Exp } from '../../../generated/graphql';
import { GraphqlQueryCompany } from '../../../interfaces/company.interface';
import { GraphqlQueryPerson } from '../../../interfaces/person.interface';
import {
SEARCH_COMPANY_QUERY,
SEARCH_PEOPLE_QUERY,
} from '../../../services/search/search';
import { mockData as mockCompanyData } from '../../companies/__tests__/__data__/mock-data';
import { mockData as mockPeopleData } from './__data__/mock-data';
import { availableFilters } from '../people-table';
function assertFilterUseCompanySearch<FilterValue>(
filter: FilterType<People_Bool_Exp>,
): filter is FilterType<People_Bool_Exp> & {
searchResultMapper: (data: GraphqlQueryCompany) => {
displayValue: string;
value: FilterValue;
};
} {
return filter.searchQuery === SEARCH_COMPANY_QUERY;
}
function assertFilterUsePeopleSearch<FilterValue>(
filter: FilterType<People_Bool_Exp>,
): filter is FilterType<People_Bool_Exp> & {
searchResultMapper: (data: GraphqlQueryPerson) => {
displayValue: string;
value: FilterValue;
};
} {
return filter.searchQuery === SEARCH_PEOPLE_QUERY;
}
const JohnDoeUser = mockPeopleData.find(
(user) => user.email === 'john@linkedin.com',
) as GraphqlQueryPerson;
describe('PeopleFilter', () => { describe('PeopleFilter', () => {
for (const filter of availableFilters) { it(`should render the filter ${cityFilter.key}`, () => {
it(`should render the filter ${filter.key}`, () => { expect(
if (assertFilterUseCompanySearch(filter)) { cityFilter.operands[0].whereTemplate({
const filterSelectedValue = filter.searchResultMapper( id: 'test-id',
mockCompanyData[0], city: 'Paris',
); email: 'john@doe.com',
for (const operand of filter.operands) { firstname: 'John',
expect( lastname: 'Doe',
filter.whereTemplate(operand, filterSelectedValue.value), phone: '0123456789',
).toMatchSnapshot(); creationDate: new Date(),
} pipe: null,
} company: null,
if (assertFilterUsePeopleSearch(filter)) { }),
const filterSelectedValue = filter.searchResultMapper(JohnDoeUser); ).toMatchSnapshot();
for (const operand of filter.operands) { });
expect(
filter.whereTemplate(operand, filterSelectedValue.value),
).toMatchSnapshot();
}
}
});
it(`should render the serch ${filter.key} with the searchValue`, () => {
expect(filter.searchTemplate('Search value')).toMatchSnapshot();
});
}
}); });

View File

@ -14,25 +14,19 @@ import Checkbox from '../../components/form/Checkbox';
import CompanyChip, { import CompanyChip, {
CompanyChipPropsType, CompanyChipPropsType,
} from '../../components/chips/CompanyChip'; } from '../../components/chips/CompanyChip';
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface'; import { Person, mapPerson } from '../../interfaces/person.interface';
import EditableText from '../../components/table/editable-cell/EditableText'; import EditableText from '../../components/table/editable-cell/EditableText';
import { import {
FilterType, FilterConfigType,
SearchConfigType,
SortType, SortType,
} from '../../components/table/table-header/interface'; } from '../../components/table/table-header/interface';
import { import { Order_By, People_Order_By } from '../../generated/graphql';
Order_By,
People_Bool_Exp,
People_Order_By,
} from '../../generated/graphql';
import { import {
SEARCH_COMPANY_QUERY, SEARCH_COMPANY_QUERY,
SEARCH_PEOPLE_QUERY, SEARCH_PEOPLE_QUERY,
} from '../../services/search/search'; } from '../../services/search/search';
import { import { Company, mapCompany } from '../../interfaces/company.interface';
GraphqlQueryCompany,
PartialCompany,
} from '../../interfaces/company.interface';
import EditablePhone from '../../components/table/editable-cell/EditablePhone'; import EditablePhone from '../../components/table/editable-cell/EditablePhone';
import EditableFullName from '../../components/table/editable-cell/EditableFullName'; import EditableFullName from '../../components/table/editable-cell/EditableFullName';
import EditableDate from '../../components/table/editable-cell/EditableDate'; import EditableDate from '../../components/table/editable-cell/EditableDate';
@ -85,163 +79,155 @@ export const availableSorts = [
}, },
] satisfies Array<SortType<People_Order_By>>; ] satisfies Array<SortType<People_Order_By>>;
const fullnameFilter = { export const fullnameFilter = {
key: 'fullname', key: 'fullname',
label: 'People', label: 'People',
icon: <FaUser />, icon: <FaUser />,
whereTemplate: (operand, { firstname, lastname }) => { searchConfig: {
if (operand.keyWord === 'equal') { query: SEARCH_PEOPLE_QUERY,
return { template: (searchInput: string) => ({
_or: [
{ firstname: { _ilike: `%${searchInput}%` } },
{ lastname: { _ilike: `%${searchInput}%` } },
],
}),
resultMapper: (person) => ({
render: (person) => `${person.firstname} ${person.lastname}`,
value: mapPerson(person),
}),
},
selectedValueRender: (person) => `${person.firstname} ${person.lastname}`,
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
_and: [ _and: [
{ firstname: { _eq: `${firstname}` } }, { firstname: { _eq: `${person.firstname}` } },
{ lastname: { _eq: `${lastname}` } }, { lastname: { _eq: `${person.lastname}` } },
], ],
}; }),
} },
{
if (operand.keyWord === 'not_equal') { label: 'Not equal',
return { id: 'not-equal',
whereTemplate: (person) => ({
_not: { _not: {
_and: [ _and: [
{ firstname: { _eq: `${firstname}` } }, { firstname: { _eq: `${person.firstname}` } },
{ lastname: { _eq: `${lastname}` } }, { lastname: { _eq: `${person.lastname}` } },
], ],
}, },
}; }),
} },
},
searchQuery: SEARCH_PEOPLE_QUERY,
searchTemplate: (searchInput: string) => ({
_or: [
{ firstname: { _ilike: `%${searchInput}%` } },
{ lastname: { _ilike: `%${searchInput}%` } },
],
}),
searchResultMapper: (person: GraphqlQueryPerson) => ({
displayValue: `${person.firstname} ${person.lastname}`,
value: { firstname: person.firstname, lastname: person.lastname },
}),
operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
], ],
} satisfies FilterType<People_Bool_Exp>; } satisfies FilterConfigType<Person, Person>;
const companyFilter = { export const companyFilter = {
key: 'company_name', key: 'company_name',
label: 'Company', label: 'Company',
icon: <FaBuilding />, icon: <FaBuilding />,
whereTemplate: (operand, { companyName }) => { searchConfig: {
if (operand.keyWord === 'equal') { query: SEARCH_COMPANY_QUERY,
return { template: (searchInput: string) => ({
company: { name: { _eq: companyName } }, name: { _ilike: `%${searchInput}%` },
}; }),
} resultMapper: (data) => ({
value: mapCompany(data),
if (operand.keyWord === 'not_equal') { render: (company) => company.name,
return { }),
_not: { company: { name: { _eq: companyName } } },
};
}
}, },
searchQuery: SEARCH_COMPANY_QUERY, selectedValueRender: (company) => company.name,
searchTemplate: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (company: GraphqlQueryCompany) => ({
displayValue: company.name,
value: { companyName: company.name },
}),
operands: [ operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' }, {
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' }, label: 'Equal',
id: 'equal',
whereTemplate: (company) => ({
company: { name: { _eq: company.name } },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (company) => ({
_not: { company: { name: { _eq: company.name } } },
}),
},
], ],
} satisfies FilterType<People_Bool_Exp>; } satisfies FilterConfigType<Person, Company>;
const emailFilter = { export const emailFilter = {
key: 'email', key: 'email',
label: 'Email', label: 'Email',
icon: <FaEnvelope />, icon: <FaEnvelope />,
whereTemplate: (operand, { email }) => { searchConfig: {
if (operand.keyWord === 'equal') { query: SEARCH_PEOPLE_QUERY,
return { template: (searchInput: string) => ({
email: { _eq: email }, email: { _ilike: `%${searchInput}%` },
}; }),
} resultMapper: (person) => ({
render: (person) => person.email,
if (operand.keyWord === 'not_equal') { value: mapPerson(person),
return { }),
_not: { email: { _eq: email } },
};
}
}, },
searchQuery: SEARCH_PEOPLE_QUERY,
searchTemplate: (searchInput: string) => ({
email: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (person: GraphqlQueryPerson) => ({
displayValue: person.email,
value: { email: person.email },
}),
operands: [ operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' }, {
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' }, label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
email: { _eq: person.email },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (person) => ({
_not: { email: { _eq: person.email } },
}),
},
], ],
} satisfies FilterType<People_Bool_Exp>; selectedValueRender: (person) => person.email,
} satisfies FilterConfigType<Person, Person>;
const cityFilter = { export const cityFilter = {
key: 'city', key: 'city',
label: 'City', label: 'City',
icon: <FaMapPin />, icon: <FaMapPin />,
whereTemplate: (operand, { city }) => { searchConfig: {
if (operand.keyWord === 'equal') { query: SEARCH_PEOPLE_QUERY,
return { template: (searchInput: string) => ({
city: { _eq: city }, city: { _ilike: `%${searchInput}%` },
}; }),
} resultMapper: (person) => ({
render: (person) => person.city,
if (operand.keyWord === 'not_equal') { value: mapPerson(person),
return { }),
_not: { city: { _eq: city } },
};
}
}, },
searchQuery: SEARCH_PEOPLE_QUERY,
searchTemplate: (searchInput: string) => ({
city: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (person: GraphqlQueryPerson) => ({
displayValue: person.city,
value: { city: person.city },
}),
operands: [ operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' }, {
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' }, label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
city: { _eq: person.city },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (person) => ({
_not: { city: { _eq: person.city } },
}),
},
], ],
} satisfies FilterType<People_Bool_Exp>; selectedValueRender: (person) => person.email,
} satisfies FilterConfigType<Person, Person>;
export const availableFilters = [ export const availableFilters = [
fullnameFilter, fullnameFilter,
companyFilter, companyFilter,
emailFilter, emailFilter,
cityFilter, cityFilter,
// { ];
// key: 'phone',
// label: 'Phone',
// icon: faPhone,
// whereTemplate: () => ({ phone: { _ilike: '%value%' } }),
// searchQuery: GET_PEOPLE,
// searchTemplate: { phone: { _ilike: '%value%' } },
// },
// {
// key: 'created_at',
// label: 'Created at',
// icon: faCalendar,
// whereTemplate: () => ({ created_at: { _eq: '%value%' } }),
// searchQuery: GET_PEOPLE,
// searchTemplate: { created_at: { _eq: '%value%' } },
// },
] satisfies FilterType<People_Bool_Exp>[];
const columnHelper = createColumnHelper<Person>(); const columnHelper = createColumnHelper<Person>();
@ -300,53 +286,36 @@ export const usePeopleColumns = () => {
<ColumnHead viewName="Company" viewIcon={<FaRegBuilding />} /> <ColumnHead viewName="Company" viewIcon={<FaRegBuilding />} />
), ),
cell: (props) => ( cell: (props) => (
<EditableRelation<PartialCompany, CompanyChipPropsType> <EditableRelation<Company, CompanyChipPropsType>
relation={props.row.original.company} relation={props.row.original.company}
searchPlaceholder="Company" searchPlaceholder="Company"
ChipComponent={CompanyChip} ChipComponent={CompanyChip}
chipComponentPropsMapper={( chipComponentPropsMapper={(company): CompanyChipPropsType => {
company: PartialCompany,
): CompanyChipPropsType => {
return { return {
name: company.name, name: company.name,
picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`, picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`,
}; };
}} }}
changeHandler={(relation: PartialCompany) => { changeHandler={(relation) => {
const person = props.row.original; const person = props.row.original;
if (person.company) { if (person.company) {
person.company.id = relation.id; person.company.id = relation.id;
} else { } else {
person.company = { person.company = relation;
id: relation.id,
name: relation.name,
domain_name: relation.domain_name,
};
} }
updatePerson(person); updatePerson(person);
}} }}
searchFilter={ searchConfig={
{ {
key: 'company_name', query: SEARCH_COMPANY_QUERY,
label: 'Company', template: (searchInput: string) => ({
icon: <FaBuilding />,
whereTemplate: () => {
return {};
},
searchQuery: SEARCH_COMPANY_QUERY,
searchTemplate: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` }, name: { _ilike: `%${searchInput}%` },
}), }),
searchResultMapper: (company: GraphqlQueryCompany) => ({ resultMapper: (company) => ({
displayValue: company.name, render: (company) => company.name,
value: { value: mapCompany(company),
id: company.id,
name: company.name,
domain_name: company.domain_name,
},
}), }),
operands: [], } satisfies SearchConfigType<Company>
} satisfies FilterType<People_Bool_Exp>
} }
/> />
), ),

View File

@ -1,8 +1,9 @@
import { gql, useQuery } from '@apollo/client'; import { gql, useQuery } from '@apollo/client';
import { People_Bool_Exp } from '../../generated/graphql';
import {} from '../../interfaces/company.interface';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { FilterType } from '../../components/table/table-header/interface'; import {
SearchConfigType,
SearchableType,
} from '../../components/table/table-header/interface';
export const SEARCH_PEOPLE_QUERY = gql` export const SEARCH_PEOPLE_QUERY = gql`
query SearchQuery($where: people_bool_exp, $limit: Int) { query SearchQuery($where: people_bool_exp, $limit: Int) {
@ -57,12 +58,18 @@ const debounce = <FuncArgs extends any[]>(
}; };
}; };
export const useSearch = (): [ export const useSearch = <T extends SearchableType>(): [
{ results: { displayValue: string; value: any }[]; loading: boolean }, {
results: {
render: (value: T) => string;
value: T;
}[];
loading: boolean;
},
React.Dispatch<React.SetStateAction<string>>, React.Dispatch<React.SetStateAction<string>>,
React.Dispatch<React.SetStateAction<FilterType<People_Bool_Exp> | null>>, React.Dispatch<React.SetStateAction<SearchConfigType<T> | null>>,
] => { ] => {
const [filter, setFilter] = useState<FilterType<People_Bool_Exp> | null>( const [searchConfig, setSearchConfig] = useState<SearchConfigType<T> | null>(
null, null,
); );
const [searchInput, setSearchInput] = useState<string>(''); const [searchInput, setSearchInput] = useState<string>('');
@ -74,26 +81,28 @@ export const useSearch = (): [
const where = useMemo(() => { const where = useMemo(() => {
return ( return (
filter && filter.searchTemplate && filter.searchTemplate(searchInput) searchConfig &&
searchConfig.template &&
searchConfig.template(searchInput)
); );
}, [filter, searchInput]); }, [searchConfig, searchInput]);
const searchFilterQueryResults = useQuery( const searchFilterQueryResults = useQuery(
filter?.searchQuery || EMPTY_QUERY, searchConfig?.query || EMPTY_QUERY,
{ {
variables: { variables: {
where, where,
limit: 5, limit: 5,
}, },
skip: !filter, skip: !searchConfig,
}, },
); );
const searchFilterResults = useMemo<{ const searchFilterResults = useMemo<{
results: { displayValue: string; value: any }[]; results: { render: (value: T) => string; value: any }[];
loading: boolean; loading: boolean;
}>(() => { }>(() => {
if (filter == null) { if (searchConfig == null) {
return { return {
loading: false, loading: false,
results: [], results: [],
@ -108,10 +117,10 @@ export const useSearch = (): [
return { return {
loading: false, loading: false,
results: searchFilterQueryResults.data.searchResults.map( results: searchFilterQueryResults.data.searchResults.map(
filter.searchResultMapper, searchConfig.resultMapper,
), ),
}; };
}, [filter, searchFilterQueryResults]); }, [searchConfig, searchFilterQueryResults]);
return [searchFilterResults, debouncedsetSearchInput, setFilter]; return [searchFilterResults, debouncedsetSearchInput, setSearchConfig];
}; };