From 9cd57083f1b0780ce3931c682f05013070343ba0 Mon Sep 17 00:00:00 2001 From: Sammy Teillet Date: Fri, 5 May 2023 10:25:06 +0200 Subject: [PATCH] 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 --- .../table-header/FilterDropdownButton.tsx | 54 ++++---- .../FilterDropdownButton.stories.tsx | 4 + .../__stories__/SortAndFilterBar.stories.tsx | 1 + .../__tests__/FilterDropdownButton.test.tsx | 24 ++-- .../table/table-header/interface.ts | 15 ++- .../__stories__/Companies.stories.tsx | 4 +- .../pages/companies/__stories__/mock-data.ts | 2 +- .../__snapshots__/people-filter.test.ts.snap | 73 +++++++++++ .../people/__tests__/people-filter.test.ts | 29 +++++ front/src/pages/people/people-table.tsx | 117 ++++++++++++------ 10 files changed, 243 insertions(+), 80 deletions(-) create mode 100644 front/src/pages/people/__tests__/__snapshots__/people-filter.test.ts.snap create mode 100644 front/src/pages/people/__tests__/people-filter.test.ts diff --git a/front/src/components/table/table-header/FilterDropdownButton.tsx b/front/src/components/table/table-header/FilterDropdownButton.tsx index 23a0a6808..a5025fbe7 100644 --- a/front/src/components/table/table-header/FilterDropdownButton.tsx +++ b/front/src/components/table/table-header/FilterDropdownButton.tsx @@ -16,11 +16,6 @@ type OwnProps = { ) => void; }; -const filterOperands: FilterOperandType[] = [ - { label: 'Include', id: 'include', keyWord: 'ilike' }, - { label: "Doesn't include", id: 'not-include', keyWord: 'not_ilike' }, -]; - export function FilterDropdownButton({ availableFilters, filterSearchResults, @@ -36,33 +31,37 @@ export function FilterDropdownButton({ FilterType | undefined >(undefined); - const [selectedFilterOperand, setSelectedFilterOperand] = - useState(filterOperands[0]); + const [selectedFilterOperand, setSelectedFilterOperand] = useState< + FilterOperandType | undefined + >(undefined); const resetState = useCallback(() => { setIsOptionUnfolded(false); setSelectedFilter(undefined); - setSelectedFilterOperand(filterOperands[0]); + setSelectedFilterOperand(undefined); onFilterSearch(null, ''); }, [onFilterSearch]); - const renderSelectOptionItems = filterOperands.map((filterOperand, index) => ( - { - setSelectedFilterOperand(filterOperand); - setIsOptionUnfolded(false); - }} - > - {filterOperand.label} - - )); + const renderSelectOptionItems = selectedFilter?.operands.map( + (filterOperand, index) => ( + { + setSelectedFilterOperand(filterOperand); + setIsOptionUnfolded(false); + }} + > + {filterOperand.label} + + ), + ); const renderSearchResults = ( filterSearchResults: NonNullable< OwnProps['filterSearchResults'] >, selectedFilter: FilterType, + selectedFilterOperand: FilterOperandType, ) => { if (filterSearchResults.loading) { return ( @@ -76,6 +75,7 @@ export function FilterDropdownButton({ key={`fields-value-${index}`} onClick={() => { onFilterSelect({ + ...selectedFilter, key: value.displayValue, operand: selectedFilterOperand, searchQuery: selectedFilter.searchQuery, @@ -104,6 +104,7 @@ export function FilterDropdownButton({ key={`select-filter-${index}`} onClick={() => { setSelectedFilter(filter); + setSelectedFilterOperand(filter.operands[0]); onFilterSearch(filter, ''); }} > @@ -112,7 +113,10 @@ export function FilterDropdownButton({ )); - function renderFilterDropdown(selectedFilter: FilterType) { + function renderFilterDropdown( + selectedFilter: FilterType, + selectedFilterOperand: FilterOperandType, + ) { return ( <> ({ /> {filterSearchResults && - renderSearchResults(filterSearchResults, selectedFilter)} + renderSearchResults( + filterSearchResults, + selectedFilter, + selectedFilterOperand, + )} ); } @@ -146,10 +154,10 @@ export function FilterDropdownButton({ setIsUnfolded={setIsUnfolded} resetState={resetState} > - {selectedFilter + {selectedFilter && selectedFilterOperand ? isOptionUnfolded ? renderSelectOptionItems - : renderFilterDropdown(selectedFilter) + : renderFilterDropdown(selectedFilter, selectedFilterOperand) : renderSelectFilterITems} ); diff --git a/front/src/components/table/table-header/__stories__/FilterDropdownButton.stories.tsx b/front/src/components/table/table-header/__stories__/FilterDropdownButton.stories.tsx index b07f1a260..ce551c6f3 100644 --- a/front/src/components/table/table-header/__stories__/FilterDropdownButton.stories.tsx +++ b/front/src/components/table/table-header/__stories__/FilterDropdownButton.stories.tsx @@ -100,6 +100,10 @@ const availableFilters = [ 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[]; diff --git a/front/src/components/table/table-header/__stories__/SortAndFilterBar.stories.tsx b/front/src/components/table/table-header/__stories__/SortAndFilterBar.stories.tsx index df605d4b1..85a279177 100644 --- a/front/src/components/table/table-header/__stories__/SortAndFilterBar.stories.tsx +++ b/front/src/components/table/table-header/__stories__/SortAndFilterBar.stories.tsx @@ -56,6 +56,7 @@ export const RegularSortAndFilterBar = ({ removeFunction }: OwnProps) => { displayValue: 'John Doe', value: data.firstname, }), + operands: [], }, ]} /> diff --git a/front/src/components/table/table-header/__tests__/FilterDropdownButton.test.tsx b/front/src/components/table/table-header/__tests__/FilterDropdownButton.test.tsx index adb5eea25..65e71f37f 100644 --- a/front/src/components/table/table-header/__tests__/FilterDropdownButton.test.tsx +++ b/front/src/components/table/table-header/__tests__/FilterDropdownButton.test.tsx @@ -28,16 +28,16 @@ it('Checks the default top option is Include', async () => { value: 'Alexandre Prot', label: 'People', operand: { - id: 'include', - keyWord: 'ilike', - label: 'Include', + id: 'equal', + keyWord: 'equal', + label: 'Equal', }, icon: , }), ); }); -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 { getByText } = render( , @@ -49,10 +49,10 @@ it('Checks the selection of top option for Doesnot include', async () => { const filterByPeople = getByText('People'); fireEvent.click(filterByPeople); - const openOperandOptions = getByText('Include'); + const openOperandOptions = getByText('Equal'); fireEvent.click(openOperandOptions); - const selectOperand = getByText("Doesn't include"); + const selectOperand = getByText('Not equal'); fireEvent.click(selectOperand); await waitFor(() => { @@ -69,9 +69,9 @@ it('Checks the selection of top option for Doesnot include', async () => { value: 'Alexandre Prot', label: 'People', operand: { - id: 'not-include', - keyWord: 'not_ilike', - label: "Doesn't include", + id: 'not-equal', + keyWord: 'not_equal', + label: 'Not equal', }, icon: , }), @@ -122,9 +122,9 @@ it('Calls the filters when typing a new name', async () => { value: 'Jane Doe', label: 'People', operand: { - id: 'include', - keyWord: 'ilike', - label: 'Include', + id: 'equal', + keyWord: 'equal', + label: 'Equal', }, icon: , }), diff --git a/front/src/components/table/table-header/interface.ts b/front/src/components/table/table-header/interface.ts index b274843f1..17d03f578 100644 --- a/front/src/components/table/table-header/interface.ts +++ b/front/src/components/table/table-header/interface.ts @@ -16,22 +16,29 @@ export type SelectedSortType = SortType & { order: 'asc' | 'desc'; }; -export type FilterType> = { +export type FilterType> = { + operands: FilterOperandType[]; label: string; key: string; icon: ReactNode; - whereTemplate: (operand: FilterOperandType, value: T) => WhereTemplate; + whereTemplate: ( + operand: FilterOperandType, + value: FilterValue, + ) => WhereTemplate; searchQuery: DocumentNode; searchTemplate: ( searchInput: string, ) => 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 = { label: string; id: string; - keyWord: 'ilike' | 'not_ilike'; + keyWord: 'ilike' | 'not_ilike' | 'equal' | 'not_equal'; }; export type SelectedFilterType = FilterType & { diff --git a/front/src/pages/companies/__stories__/Companies.stories.tsx b/front/src/pages/companies/__stories__/Companies.stories.tsx index cd28a0ce2..a98150f38 100644 --- a/front/src/pages/companies/__stories__/Companies.stories.tsx +++ b/front/src/pages/companies/__stories__/Companies.stories.tsx @@ -3,7 +3,7 @@ import Companies from '../Companies'; import { ThemeProvider } from '@emotion/react'; import { lightTheme } from '../../../layout/styles/themes'; import { GET_COMPANIES } from '../../../services/companies'; -import { defaultData } from './mock-data'; +import { mockCompanyData } from './mock-data'; import { MockedProvider } from '@apollo/client/testing'; const component = { @@ -23,7 +23,7 @@ const mocks = [ }, result: { data: { - companies: defaultData, + companies: mockCompanyData, }, }, }, diff --git a/front/src/pages/companies/__stories__/mock-data.ts b/front/src/pages/companies/__stories__/mock-data.ts index 9ada5e568..6da623491 100644 --- a/front/src/pages/companies/__stories__/mock-data.ts +++ b/front/src/pages/companies/__stories__/mock-data.ts @@ -1,6 +1,6 @@ import { GraphqlQueryCompany } from '../../../interfaces/company.interface'; -export const defaultData: Array = [ +export const mockCompanyData: Array = [ { id: 'f121ab32-fac4-4b8c-9a3d-150c877319c2', name: 'ACME', diff --git a/front/src/pages/people/__tests__/__snapshots__/people-filter.test.ts.snap b/front/src/pages/people/__tests__/__snapshots__/people-filter.test.ts.snap new file mode 100644 index 000000000..7d8de6d77 --- /dev/null +++ b/front/src/pages/people/__tests__/__snapshots__/people-filter.test.ts.snap @@ -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", + }, + }, + ], + }, +} +`; diff --git a/front/src/pages/people/__tests__/people-filter.test.ts b/front/src/pages/people/__tests__/people-filter.test.ts new file mode 100644 index 000000000..4e684532d --- /dev/null +++ b/front/src/pages/people/__tests__/people-filter.test.ts @@ -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(); + } + }); +}); diff --git a/front/src/pages/people/people-table.tsx b/front/src/pages/people/people-table.tsx index dc697ada2..a132c75e5 100644 --- a/front/src/pages/people/people-table.tsx +++ b/front/src/pages/people/people-table.tsx @@ -56,45 +56,86 @@ export const availableSorts = [ { key: 'city', label: 'City', icon: }, ] satisfies Array>; +export const fullnameFilter = { + key: 'fullname', + label: 'People', + icon: , + 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; + +export const companyFilter = { + key: 'company_name', + label: 'Company', + icon: , + 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; + export const availableFilters = [ - { - key: 'fullname', - label: 'People', - icon: , - 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: , - 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 }, - }), - }, + fullnameFilter, + companyFilter, // { // key: 'email', // label: 'Email',