Sammy/t 192 aau whan i select does not include it is (#99)

* feature: add operand list to filters

* feature: implement not include

* feature: add operand on filters

* feature: use filters operand instead of defaults

* test: adapt test with new operands

* refactor: remove useless %% in gql where

* test: test fullname filter

* test: add test for where rendering of filters
This commit is contained in:
Sammy Teillet
2023-05-05 10:25:06 +02:00
committed by GitHub
parent 89dc5b4d60
commit 9cd57083f1
10 changed files with 243 additions and 80 deletions

View File

@ -16,11 +16,6 @@ type OwnProps<FilterProperties> = {
) => void; ) => void;
}; };
const filterOperands: FilterOperandType[] = [
{ label: 'Include', id: 'include', keyWord: 'ilike' },
{ label: "Doesn't include", id: 'not-include', keyWord: 'not_ilike' },
];
export function FilterDropdownButton<FilterProperties>({ export function FilterDropdownButton<FilterProperties>({
availableFilters, availableFilters,
filterSearchResults, filterSearchResults,
@ -36,33 +31,37 @@ export function FilterDropdownButton<FilterProperties>({
FilterType<FilterProperties> | undefined FilterType<FilterProperties> | undefined
>(undefined); >(undefined);
const [selectedFilterOperand, setSelectedFilterOperand] = const [selectedFilterOperand, setSelectedFilterOperand] = useState<
useState<FilterOperandType>(filterOperands[0]); FilterOperandType | undefined
>(undefined);
const resetState = useCallback(() => { const resetState = useCallback(() => {
setIsOptionUnfolded(false); setIsOptionUnfolded(false);
setSelectedFilter(undefined); setSelectedFilter(undefined);
setSelectedFilterOperand(filterOperands[0]); setSelectedFilterOperand(undefined);
onFilterSearch(null, ''); onFilterSearch(null, '');
}, [onFilterSearch]); }, [onFilterSearch]);
const renderSelectOptionItems = filterOperands.map((filterOperand, index) => ( const renderSelectOptionItems = selectedFilter?.operands.map(
<DropdownButton.StyledDropdownItem (filterOperand, index) => (
key={`select-filter-operand-${index}`} <DropdownButton.StyledDropdownItem
onClick={() => { key={`select-filter-operand-${index}`}
setSelectedFilterOperand(filterOperand); onClick={() => {
setIsOptionUnfolded(false); setSelectedFilterOperand(filterOperand);
}} setIsOptionUnfolded(false);
> }}
{filterOperand.label} >
</DropdownButton.StyledDropdownItem> {filterOperand.label}
)); </DropdownButton.StyledDropdownItem>
),
);
const renderSearchResults = ( const renderSearchResults = (
filterSearchResults: NonNullable< filterSearchResults: NonNullable<
OwnProps<FilterProperties>['filterSearchResults'] OwnProps<FilterProperties>['filterSearchResults']
>, >,
selectedFilter: FilterType<FilterProperties>, selectedFilter: FilterType<FilterProperties>,
selectedFilterOperand: FilterOperandType,
) => { ) => {
if (filterSearchResults.loading) { if (filterSearchResults.loading) {
return ( return (
@ -76,6 +75,7 @@ export function FilterDropdownButton<FilterProperties>({
key={`fields-value-${index}`} key={`fields-value-${index}`}
onClick={() => { onClick={() => {
onFilterSelect({ onFilterSelect({
...selectedFilter,
key: value.displayValue, key: value.displayValue,
operand: selectedFilterOperand, operand: selectedFilterOperand,
searchQuery: selectedFilter.searchQuery, searchQuery: selectedFilter.searchQuery,
@ -104,6 +104,7 @@ export function FilterDropdownButton<FilterProperties>({
key={`select-filter-${index}`} key={`select-filter-${index}`}
onClick={() => { onClick={() => {
setSelectedFilter(filter); setSelectedFilter(filter);
setSelectedFilterOperand(filter.operands[0]);
onFilterSearch(filter, ''); onFilterSearch(filter, '');
}} }}
> >
@ -112,7 +113,10 @@ export function FilterDropdownButton<FilterProperties>({
</DropdownButton.StyledDropdownItem> </DropdownButton.StyledDropdownItem>
)); ));
function renderFilterDropdown(selectedFilter: FilterType<FilterProperties>) { function renderFilterDropdown(
selectedFilter: FilterType<FilterProperties>,
selectedFilterOperand: FilterOperandType,
) {
return ( return (
<> <>
<DropdownButton.StyledDropdownTopOption <DropdownButton.StyledDropdownTopOption
@ -133,7 +137,11 @@ export function FilterDropdownButton<FilterProperties>({
/> />
</DropdownButton.StyledSearchField> </DropdownButton.StyledSearchField>
{filterSearchResults && {filterSearchResults &&
renderSearchResults(filterSearchResults, selectedFilter)} renderSearchResults(
filterSearchResults,
selectedFilter,
selectedFilterOperand,
)}
</> </>
); );
} }
@ -146,10 +154,10 @@ export function FilterDropdownButton<FilterProperties>({
setIsUnfolded={setIsUnfolded} setIsUnfolded={setIsUnfolded}
resetState={resetState} resetState={resetState}
> >
{selectedFilter {selectedFilter && selectedFilterOperand
? isOptionUnfolded ? isOptionUnfolded
? renderSelectOptionItems ? renderSelectOptionItems
: renderFilterDropdown(selectedFilter) : renderFilterDropdown(selectedFilter, selectedFilterOperand)
: renderSelectFilterITems} : renderSelectFilterITems}
</DropdownButton> </DropdownButton>
); );

View File

@ -100,6 +100,10 @@ const availableFilters = [
displayValue: data.firstname + ' ' + data.lastname, displayValue: data.firstname + ' ' + data.lastname,
value: data.firstname, value: data.firstname,
}), }),
operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
],
}, },
] satisfies FilterType<People_Bool_Exp>[]; ] satisfies FilterType<People_Bool_Exp>[];

View File

@ -56,6 +56,7 @@ export const RegularSortAndFilterBar = ({ removeFunction }: OwnProps) => {
displayValue: 'John Doe', displayValue: 'John Doe',
value: data.firstname, value: data.firstname,
}), }),
operands: [],
}, },
]} ]}
/> />

View File

@ -28,16 +28,16 @@ it('Checks the default top option is Include', async () => {
value: 'Alexandre Prot', value: 'Alexandre Prot',
label: 'People', label: 'People',
operand: { operand: {
id: 'include', id: 'equal',
keyWord: 'ilike', keyWord: 'equal',
label: 'Include', label: 'Equal',
}, },
icon: <FaUsers />, icon: <FaUsers />,
}), }),
); );
}); });
it('Checks the selection of top option for Doesnot include', async () => { it('Checks the selection of top option for Not Equal', async () => {
const setFilters = jest.fn(); const setFilters = jest.fn();
const { getByText } = render( const { getByText } = render(
<RegularFilterDropdownButton setFilter={setFilters} />, <RegularFilterDropdownButton setFilter={setFilters} />,
@ -49,10 +49,10 @@ it('Checks the selection of top option for Doesnot include', async () => {
const filterByPeople = getByText('People'); const filterByPeople = getByText('People');
fireEvent.click(filterByPeople); fireEvent.click(filterByPeople);
const openOperandOptions = getByText('Include'); const openOperandOptions = getByText('Equal');
fireEvent.click(openOperandOptions); fireEvent.click(openOperandOptions);
const selectOperand = getByText("Doesn't include"); const selectOperand = getByText('Not equal');
fireEvent.click(selectOperand); fireEvent.click(selectOperand);
await waitFor(() => { await waitFor(() => {
@ -69,9 +69,9 @@ it('Checks the selection of top option for Doesnot include', async () => {
value: 'Alexandre Prot', value: 'Alexandre Prot',
label: 'People', label: 'People',
operand: { operand: {
id: 'not-include', id: 'not-equal',
keyWord: 'not_ilike', keyWord: 'not_equal',
label: "Doesn't include", label: 'Not equal',
}, },
icon: <FaUsers />, icon: <FaUsers />,
}), }),
@ -122,9 +122,9 @@ it('Calls the filters when typing a new name', async () => {
value: 'Jane Doe', value: 'Jane Doe',
label: 'People', label: 'People',
operand: { operand: {
id: 'include', id: 'equal',
keyWord: 'ilike', keyWord: 'equal',
label: 'Include', label: 'Equal',
}, },
icon: <FaUsers />, icon: <FaUsers />,
}), }),

View File

@ -16,22 +16,29 @@ export type SelectedSortType<SortField = string> = SortType<SortField> & {
order: 'asc' | 'desc'; order: 'asc' | 'desc';
}; };
export type FilterType<WhereTemplate, T = Record<string, string>> = { export type FilterType<WhereTemplate, FilterValue = Record<string, any>> = {
operands: FilterOperandType[];
label: string; label: string;
key: string; key: string;
icon: ReactNode; icon: ReactNode;
whereTemplate: (operand: FilterOperandType, value: T) => WhereTemplate; whereTemplate: (
operand: FilterOperandType,
value: FilterValue,
) => WhereTemplate;
searchQuery: DocumentNode; searchQuery: DocumentNode;
searchTemplate: ( searchTemplate: (
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) => { displayValue: string; value: T }; searchResultMapper: (data: any) => {
displayValue: string;
value: FilterValue;
};
}; };
export type FilterOperandType = { export type FilterOperandType = {
label: string; label: string;
id: string; id: string;
keyWord: 'ilike' | 'not_ilike'; keyWord: 'ilike' | 'not_ilike' | 'equal' | 'not_equal';
}; };
export type SelectedFilterType<WhereTemplate> = FilterType<WhereTemplate> & { export type SelectedFilterType<WhereTemplate> = FilterType<WhereTemplate> & {

View File

@ -3,7 +3,7 @@ import Companies from '../Companies';
import { ThemeProvider } from '@emotion/react'; import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes'; import { lightTheme } from '../../../layout/styles/themes';
import { GET_COMPANIES } from '../../../services/companies'; import { GET_COMPANIES } from '../../../services/companies';
import { defaultData } from './mock-data'; import { mockCompanyData } from './mock-data';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
const component = { const component = {
@ -23,7 +23,7 @@ const mocks = [
}, },
result: { result: {
data: { data: {
companies: defaultData, companies: mockCompanyData,
}, },
}, },
}, },

View File

@ -1,6 +1,6 @@
import { GraphqlQueryCompany } from '../../../interfaces/company.interface'; import { GraphqlQueryCompany } from '../../../interfaces/company.interface';
export const defaultData: Array<GraphqlQueryCompany> = [ export const mockCompanyData: Array<GraphqlQueryCompany> = [
{ {
id: 'f121ab32-fac4-4b8c-9a3d-150c877319c2', id: 'f121ab32-fac4-4b8c-9a3d-150c877319c2',
name: 'ACME', name: 'ACME',

View File

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PeopleFilter Company fitler should generate the where variable of the GQL call 1`] = `
Object {
"_and": Array [
Object {
"firstname": Object {
"_eq": "undefined",
},
},
Object {
"lastname": Object {
"_eq": "undefined",
},
},
],
}
`;
exports[`PeopleFilter Company fitler should generate the where variable of the GQL call 2`] = `
Object {
"_not": Object {
"_and": Array [
Object {
"firstname": Object {
"_eq": "undefined",
},
},
Object {
"lastname": Object {
"_eq": "undefined",
},
},
],
},
}
`;
exports[`PeopleFilter Fullname filter should generate the where variable of the GQL call 1`] = `
Object {
"_and": Array [
Object {
"firstname": Object {
"_eq": "undefined",
},
},
Object {
"lastname": Object {
"_eq": "undefined",
},
},
],
}
`;
exports[`PeopleFilter Fullname filter should generate the where variable of the GQL call 2`] = `
Object {
"_not": Object {
"_and": Array [
Object {
"firstname": Object {
"_eq": "undefined",
},
},
Object {
"lastname": Object {
"_eq": "undefined",
},
},
],
},
}
`;

View File

@ -0,0 +1,29 @@
import { GraphqlQueryPerson } from '../../../interfaces/person.interface';
import { mockCompanyData } from '../../companies/__stories__/mock-data';
import { defaultData } from '../default-data';
import { companyFilter, fullnameFilter } from '../people-table';
const JohnDoeUser = defaultData.find(
(user) => user.email === 'john@linkedin.com',
) as GraphqlQueryPerson;
describe('PeopleFilter', () => {
it('Fullname filter should generate the where variable of the GQL call', () => {
const filterSelectedValue = fullnameFilter.searchResultMapper(JohnDoeUser);
for (const operand of fullnameFilter.operands) {
expect(
fullnameFilter.whereTemplate(operand, filterSelectedValue),
).toMatchSnapshot();
}
});
it('Company fitler should generate the where variable of the GQL call', () => {
const filterSelectedValue = companyFilter.searchResultMapper(
mockCompanyData[0],
);
for (const operand of companyFilter.operands) {
expect(
fullnameFilter.whereTemplate(operand, filterSelectedValue),
).toMatchSnapshot();
}
});
});

View File

@ -56,45 +56,86 @@ export const availableSorts = [
{ key: 'city', label: 'City', icon: <FaMapPin /> }, { key: 'city', label: 'City', icon: <FaMapPin /> },
] satisfies Array<SortType<OrderByFields>>; ] satisfies Array<SortType<OrderByFields>>;
export const fullnameFilter = {
key: 'fullname',
label: 'People',
icon: <FaUser />,
whereTemplate: (operand, { firstname, lastname }) => {
if (operand.keyWord === 'equal') {
return {
_and: [
{ firstname: { _eq: `${firstname}` } },
{ lastname: { _eq: `${lastname}` } },
],
};
}
if (operand.keyWord === 'not_equal') {
return {
_not: {
_and: [
{ firstname: { _eq: `${firstname}` } },
{ lastname: { _eq: `${lastname}` } },
],
},
};
}
console.error(Error(`Unhandled operand: ${operand.keyWord}`));
return {};
},
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>;
export const companyFilter = {
key: 'company_name',
label: 'Company',
icon: <FaBuilding />,
whereTemplate: (operand, { companyName }) => {
if (operand.keyWord === 'equal') {
return {
company: { name: { _eq: companyName } },
};
}
if (operand.keyWord === 'not_equal') {
return {
_not: { company: { name: { _eq: companyName } } },
};
}
console.error(Error(`Unhandled operand: ${operand.keyWord}`));
return {};
},
searchQuery: SEARCH_COMPANY_QUERY,
searchTemplate: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (company: GraphqlQueryCompany) => ({
displayValue: company.name,
value: { companyName: company.name },
}),
operands: [
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
],
} satisfies FilterType<People_Bool_Exp>;
export const availableFilters = [ export const availableFilters = [
{ fullnameFilter,
key: 'fullname', companyFilter,
label: 'People',
icon: <FaUser />,
whereTemplate: (_operand, { firstname, lastname }) => ({
_and: [
{ firstname: { _ilike: `${firstname}` } },
{ lastname: { _ilike: `${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 },
}),
},
{
key: 'company_name',
label: 'Company',
icon: <FaBuilding />,
whereTemplate: (_operand, { companyName }) => ({
company: { name: { _ilike: `%${companyName}%` } },
}),
searchQuery: SEARCH_COMPANY_QUERY,
searchTemplate: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (company: GraphqlQueryCompany) => ({
displayValue: company.name,
value: { companyName: company.name },
}),
},
// { // {
// key: 'email', // key: 'email',
// label: 'Email', // label: 'Email',