From 499752ed6be83cc8d640eee196ba5993cb8b77d1 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Wed, 17 May 2023 13:25:33 +0200 Subject: [PATCH] Refactor Filters and Search (#119) --- front/package.json | 2 +- front/src/components/table/Table.tsx | 25 +- .../table/editable-cell/EditableRelation.tsx | 26 +- .../__stories__/EditableRelation.stories.tsx | 46 +-- .../__tests__/EditableRelation.test.tsx | 20 +- .../table-header/FilterDropdownButton.tsx | 61 ++-- .../table/table-header/SortAndFilterBar.tsx | 14 +- .../table/table-header/TableHeader.tsx | 33 +- .../FilterDropdownButton.stories.tsx | 45 +-- .../__stories__/SortAndFilterBar.stories.tsx | 38 +-- .../__tests__/FilterDropdownButton.test.tsx | 31 +- .../components/table/table-header/helpers.ts | 13 +- .../table/table-header/interface.ts | 76 +++-- front/src/interfaces/company.interface.ts | 3 - front/src/interfaces/person.interface.test.ts | 4 + front/src/interfaces/person.interface.ts | 19 +- front/src/pages/companies/Companies.tsx | 9 +- .../companies-filter.test.ts.snap | 53 ---- .../__tests__/companies-filter.test.ts | 62 ---- front/src/pages/companies/companies-table.tsx | 148 ++++----- front/src/pages/people/People.tsx | 9 +- .../__snapshots__/people-filter.test.ts.snap | 127 -------- .../people/__tests__/people-filter.test.ts | 78 +---- front/src/pages/people/people-table.tsx | 287 ++++++++---------- front/src/services/search/search.ts | 41 ++- 25 files changed, 466 insertions(+), 804 deletions(-) delete mode 100644 front/src/pages/companies/__tests__/__snapshots__/companies-filter.test.ts.snap delete mode 100644 front/src/pages/companies/__tests__/companies-filter.test.ts diff --git a/front/package.json b/front/package.json index ce3c578d7..49b860966 100644 --- a/front/package.json +++ b/front/package.json @@ -67,7 +67,7 @@ "coverageThreshold": { "global": { "branches": 70, - "functions": 80, + "functions": 75, "lines": 80, "statements": 80 } diff --git a/front/src/components/table/Table.tsx b/front/src/components/table/Table.tsx index 933c085de..62f7ced8c 100644 --- a/front/src/components/table/Table.tsx +++ b/front/src/components/table/Table.tsx @@ -9,7 +9,9 @@ import { import TableHeader from './table-header/TableHeader'; import styled from '@emotion/styled'; import { - FilterType, + FilterConfigType, + SearchConfigType, + SearchableType, SelectedFilterType, SelectedSortType, SortType, @@ -21,23 +23,24 @@ declare module 'react' { ): (props: P & React.RefAttributes) => React.ReactElement | null; } -type OwnProps = { +type OwnProps = { data: Array; columns: Array>; viewName: string; viewIcon?: React.ReactNode; availableSorts?: Array>; - availableFilters?: FilterType[]; + availableFilters?: FilterConfigType[]; filterSearchResults?: { - results: { displayValue: string; value: any }[]; + results: { + render: (value: SearchableType) => string; + value: SearchableType; + }[]; loading: boolean; }; onSortsUpdate?: (sorts: Array>) => void; - onFiltersUpdate?: ( - sorts: Array>, - ) => void; + onFiltersUpdate?: (sorts: Array) => void; onFilterSearch?: ( - filter: FilterType | null, + filter: SearchConfigType | null, searchValue: string, ) => void; onRowSelectionChange?: (rowSelection: string[]) => void; @@ -94,7 +97,7 @@ const StyledTableScrollableContainer = styled.div` flex: 1; `; -const Table = ( +const Table = ( { data, columns, @@ -107,7 +110,7 @@ const Table = ( onFiltersUpdate, onFilterSearch, onRowSelectionChange, - }: OwnProps, + }: OwnProps, ref: React.ForwardedRef<{ resetRowSelection: () => void } | undefined>, ) => { const [internalRowSelection, setInternalRowSelection] = React.useState({}); @@ -144,7 +147,7 @@ const Table = ( viewName={viewName} viewIcon={viewIcon} availableSorts={availableSorts} - availableFilters={availableFilters} + availableFilters={availableFilters as FilterConfigType[]} filterSearchResults={filterSearchResults} onSortsUpdate={onSortsUpdate} onFiltersUpdate={onFiltersUpdate} diff --git a/front/src/components/table/editable-cell/EditableRelation.tsx b/front/src/components/table/editable-cell/EditableRelation.tsx index 23540b468..a99ff61a3 100644 --- a/front/src/components/table/editable-cell/EditableRelation.tsx +++ b/front/src/components/table/editable-cell/EditableRelation.tsx @@ -2,8 +2,7 @@ import { ChangeEvent, ComponentType, useState } from 'react'; import EditableCellWrapper from './EditableCellWrapper'; import styled from '@emotion/styled'; import { useSearch } from '../../../services/search/search'; -import { FilterType } from '../table-header/interface'; -import { People_Bool_Exp } from '../../../generated/graphql'; +import { SearchConfigType, SearchableType } from '../table-header/interface'; const StyledEditModeContainer = styled.div` width: 200px; @@ -48,10 +47,13 @@ const StyledEditModeResultItem = styled.div` cursor: pointer; `; -export type EditableRelationProps = { +export type EditableRelationProps< + RelationType extends SearchableType, + ChipComponentPropsType, +> = { relation?: RelationType | null; searchPlaceholder: string; - searchFilter: FilterType; + searchConfig: SearchConfigType; changeHandler: (relation: RelationType) => void; editModeHorizontalAlign?: 'left' | 'right'; ChipComponent: ComponentType; @@ -60,10 +62,13 @@ export type EditableRelationProps = { ) => ChipComponentPropsType & JSX.IntrinsicAttributes; }; -function EditableRelation({ +function EditableRelation< + RelationType extends SearchableType, + ChipComponentPropsType, +>({ relation, searchPlaceholder, - searchFilter, + searchConfig, changeHandler, editModeHorizontalAlign, ChipComponent, @@ -72,7 +77,8 @@ function EditableRelation({ const [selectedRelation, setSelectedRelation] = useState(relation); const [isEditMode, setIsEditMode] = useState(false); - const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch(); + const [filterSearchResults, setSearchInput, setFilterSearch] = + useSearch(); return ( ({ ) => { - setFilterSearch(searchFilter); + setFilterSearch(searchConfig); setSearchInput(event.target.value); }} /> {filterSearchResults.results && - filterSearchResults.results.map((result) => ( + filterSearchResults.results.map((result, index) => ( { setSelectedRelation(result.value); changeHandler(result.value); diff --git a/front/src/components/table/editable-cell/__stories__/EditableRelation.stories.tsx b/front/src/components/table/editable-cell/__stories__/EditableRelation.stories.tsx index 6bbf91020..995a1a666 100644 --- a/front/src/components/table/editable-cell/__stories__/EditableRelation.stories.tsx +++ b/front/src/components/table/editable-cell/__stories__/EditableRelation.stories.tsx @@ -3,16 +3,11 @@ import { ThemeProvider } from '@emotion/react'; import { lightTheme } from '../../../../layout/styles/themes'; import { StoryFn } from '@storybook/react'; import CompanyChip, { CompanyChipPropsType } from '../../../chips/CompanyChip'; -import { - GraphqlQueryCompany, - PartialCompany, -} from '../../../../interfaces/company.interface'; +import { Company, mapCompany } from '../../../../interfaces/company.interface'; import { MockedProvider } from '@apollo/client/testing'; import { SEARCH_COMPANY_QUERY } from '../../../../services/search/search'; import styled from '@emotion/styled'; -import { People_Bool_Exp } from '../../../../generated/graphql'; -import { FilterType } from '../../table-header/interface'; -import { FaBuilding } from 'react-icons/fa'; +import { SearchConfigType } from '../../table-header/interface'; const component = { title: 'editable-cell/EditableRelation', @@ -58,13 +53,13 @@ const mocks = [ ]; const Template: StoryFn< - typeof EditableRelation -> = (args: EditableRelationProps) => { + typeof EditableRelation +> = (args: EditableRelationProps) => { return ( - {...args} /> + {...args} /> @@ -77,36 +72,25 @@ EditableRelationStory.args = { id: '123', name: 'Heroku', domain_name: 'heroku.com', - } as PartialCompany, + } as Company, ChipComponent: CompanyChip, - chipComponentPropsMapper: (company: PartialCompany): CompanyChipPropsType => { + chipComponentPropsMapper: (company: Company): CompanyChipPropsType => { return { name: company.name, picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`, }; }, - changeHandler: (relation: PartialCompany) => { + changeHandler: (relation: Company) => { console.log('changed', relation); }, - searchFilter: { - key: 'company_name', - label: 'Company', - icon: , - whereTemplate: () => { - return {}; - }, - searchQuery: SEARCH_COMPANY_QUERY, - searchTemplate: (searchInput: string) => ({ + searchConfig: { + query: SEARCH_COMPANY_QUERY, + template: (searchInput: string) => ({ name: { _ilike: `%${searchInput}%` }, }), - searchResultMapper: (company: GraphqlQueryCompany) => ({ - displayValue: company.name, - value: { - id: company.id, - name: company.name, - domain_name: company.domain_name, - }, + resultMapper: (company) => ({ + render: (company) => company.name, + value: mapCompany(company), }), - operands: [], - } satisfies FilterType, + } satisfies SearchConfigType, }; diff --git a/front/src/components/table/editable-cell/__tests__/EditableRelation.test.tsx b/front/src/components/table/editable-cell/__tests__/EditableRelation.test.tsx index 18f119ff2..97ac53da3 100644 --- a/front/src/components/table/editable-cell/__tests__/EditableRelation.test.tsx +++ b/front/src/components/table/editable-cell/__tests__/EditableRelation.test.tsx @@ -2,17 +2,17 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import { EditableRelationStory } from '../__stories__/EditableRelation.stories'; import { CompanyChipPropsType } from '../../../chips/CompanyChip'; -import { PartialCompany } from '../../../../interfaces/company.interface'; import { EditableRelationProps } from '../EditableRelation'; import { act } from 'react-dom/test-utils'; +import { Company } from '../../../../interfaces/company.interface'; it('Checks the EditableRelation editing event bubbles up', async () => { const func = jest.fn(() => null); const { getByTestId, getByText } = render( )} changeHandler={func} @@ -49,10 +49,16 @@ it('Checks the EditableRelation editing event bubbles up', async () => { }); await waitFor(() => { - expect(func).toBeCalledWith({ - domain_name: 'abnb.com', - id: 'abnb', - name: 'Airbnb', - }); + expect(func).toBeCalledWith( + expect.objectContaining({ + accountOwner: null, + address: undefined, + domain_name: 'abnb.com', + employees: undefined, + id: 'abnb', + name: 'Airbnb', + opportunities: [], + }), + ); }); }); diff --git a/front/src/components/table/table-header/FilterDropdownButton.tsx b/front/src/components/table/table-header/FilterDropdownButton.tsx index 420b3af1f..b6b68c985 100644 --- a/front/src/components/table/table-header/FilterDropdownButton.tsx +++ b/front/src/components/table/table-header/FilterDropdownButton.tsx @@ -1,34 +1,43 @@ import { ChangeEvent, useCallback, useState } from 'react'; import DropdownButton from './DropdownButton'; -import { FilterOperandType, FilterType, SelectedFilterType } from './interface'; +import { + FilterConfigType, + FilterOperandType, + SearchConfigType, + SearchableType, + SelectedFilterType, +} from './interface'; -type OwnProps = { +type OwnProps = { isFilterSelected: boolean; - availableFilters: FilterType[]; + availableFilters: FilterConfigType[]; filterSearchResults?: { - results: { displayValue: string; value: any }[]; + results: { + render: (value: SearchableType) => string; + value: SearchableType; + }[]; loading: boolean; }; - onFilterSelect: (filter: SelectedFilterType) => void; + onFilterSelect: (filter: SelectedFilterType) => void; onFilterSearch: ( - filter: FilterType | null, + filter: SearchConfigType | null, searchValue: string, ) => void; }; -export function FilterDropdownButton({ +export function FilterDropdownButton({ availableFilters, filterSearchResults, onFilterSearch, onFilterSelect, isFilterSelected, -}: OwnProps) { +}: OwnProps) { const [isUnfolded, setIsUnfolded] = useState(false); const [isOptionUnfolded, setIsOptionUnfolded] = useState(false); const [selectedFilter, setSelectedFilter] = useState< - FilterType | undefined + FilterConfigType | undefined >(undefined); const [selectedFilterOperand, setSelectedFilterOperand] = useState< @@ -57,10 +66,8 @@ export function FilterDropdownButton({ ); const renderSearchResults = ( - filterSearchResults: NonNullable< - OwnProps['filterSearchResults'] - >, - selectedFilter: FilterType, + filterSearchResults: NonNullable, + selectedFilter: FilterConfigType, selectedFilterOperand: FilterOperandType, ) => { if (filterSearchResults.loading) { @@ -70,32 +77,24 @@ export function FilterDropdownButton({ ); } - return filterSearchResults.results.map((value, index) => ( + + return filterSearchResults.results.map((result, index) => ( { onFilterSelect({ - ...selectedFilter, - key: value.displayValue, - operand: selectedFilterOperand, - searchQuery: selectedFilter.searchQuery, - searchTemplate: selectedFilter.searchTemplate, - whereTemplate: selectedFilter.whereTemplate, + key: selectedFilter.key, label: selectedFilter.label, - value: value.displayValue, + value: result.value, + displayValue: result.render(result.value), icon: selectedFilter.icon, - where: - selectedFilter.whereTemplate( - selectedFilterOperand, - value.value, - ) || ({} as FilterProperties), - searchResultMapper: selectedFilter.searchResultMapper, + operand: selectedFilterOperand, }); setIsUnfolded(false); setSelectedFilter(undefined); }} > - {value.displayValue} + {result.render(result.value)} )); }; @@ -106,7 +105,7 @@ export function FilterDropdownButton({ onClick={() => { setSelectedFilter(filter); setSelectedFilterOperand(filter.operands[0]); - onFilterSearch(filter, ''); + onFilterSearch(filter.searchConfig, ''); }} > {filter.icon} @@ -115,7 +114,7 @@ export function FilterDropdownButton({ )); function renderFilterDropdown( - selectedFilter: FilterType, + selectedFilter: FilterConfigType, selectedFilterOperand: FilterOperandType, ) { return ( @@ -133,7 +132,7 @@ export function FilterDropdownButton({ type="text" placeholder={selectedFilter.label} onChange={(event: ChangeEvent) => - onFilterSearch(selectedFilter, event.target.value) + onFilterSearch(selectedFilter.searchConfig, event.target.value) } /> diff --git a/front/src/components/table/table-header/SortAndFilterBar.tsx b/front/src/components/table/table-header/SortAndFilterBar.tsx index ca30034c8..3eb74a78e 100644 --- a/front/src/components/table/table-header/SortAndFilterBar.tsx +++ b/front/src/components/table/table-header/SortAndFilterBar.tsx @@ -3,13 +3,11 @@ import SortOrFilterChip from './SortOrFilterChip'; import { FaArrowDown, FaArrowUp } from 'react-icons/fa'; import { SelectedFilterType, SelectedSortType } from './interface'; -type OwnProps = { +type OwnProps = { sorts: Array>; onRemoveSort: (sortId: SelectedSortType['key']) => void; - filters: Array>; - onRemoveFilter: ( - filterId: SelectedFilterType['key'], - ) => void; + filters: Array; + onRemoveFilter: (filterId: SelectedFilterType['key']) => void; onCancelClick: () => void; }; @@ -42,13 +40,13 @@ const StyledCancelButton = styled.button` } `; -function SortAndFilterBar({ +function SortAndFilterBar({ sorts, onRemoveSort, filters, onRemoveFilter, onCancelClick, -}: OwnProps) { +}: OwnProps) { return ( {sorts.map((sort) => { @@ -67,7 +65,7 @@ function SortAndFilterBar({ onRemoveFilter(filter.key)} diff --git a/front/src/components/table/table-header/TableHeader.tsx b/front/src/components/table/table-header/TableHeader.tsx index 9805dbb9d..e8e7d6b9b 100644 --- a/front/src/components/table/table-header/TableHeader.tsx +++ b/front/src/components/table/table-header/TableHeader.tsx @@ -1,6 +1,8 @@ import styled from '@emotion/styled'; import { - FilterType, + FilterConfigType, + SearchConfigType, + SearchableType, SelectedFilterType, SelectedSortType, SortType, @@ -10,21 +12,22 @@ import { SortDropdownButton } from './SortDropdownButton'; import { FilterDropdownButton } from './FilterDropdownButton'; import SortAndFilterBar from './SortAndFilterBar'; -type OwnProps = { +type OwnProps = { viewName: string; viewIcon?: ReactNode; availableSorts?: Array>; - availableFilters?: FilterType[]; + availableFilters?: FilterConfigType[]; filterSearchResults?: { - results: { displayValue: string; value: any }[]; + results: { + render: (value: SearchableType) => string; + value: SearchableType; + }[]; loading: boolean; }; onSortsUpdate?: (sorts: Array>) => void; - onFiltersUpdate?: ( - sorts: Array>, - ) => void; + onFiltersUpdate?: (sorts: Array) => void; onFilterSearch?: ( - filter: FilterType | null, + filter: SearchConfigType | null, searchValue: string, ) => void; }; @@ -65,7 +68,7 @@ const StyledFilters = styled.div` margin-right: ${(props) => props.theme.spacing(2)}; `; -function TableHeader({ +function TableHeader({ viewName, viewIcon, availableSorts, @@ -74,13 +77,11 @@ function TableHeader({ onSortsUpdate, onFiltersUpdate, onFilterSearch, -}: OwnProps) { +}: OwnProps) { const [sorts, innerSetSorts] = useState>>( [], ); - const [filters, innerSetFilters] = useState< - Array> - >([]); + const [filters, innerSetFilters] = useState>([]); const sortSelect = useCallback( (newSort: SelectedSortType) => { @@ -101,7 +102,7 @@ function TableHeader({ ); const filterSelect = useCallback( - (filter: SelectedFilterType) => { + (filter: SelectedFilterType) => { const newFilters = updateSortOrFilterByKey(filters, filter); innerSetFilters(newFilters); @@ -111,7 +112,7 @@ function TableHeader({ ); const filterUnselect = useCallback( - (filterId: SelectedFilterType['key']) => { + (filterId: SelectedFilterType['key']) => { const newFilters = filters.filter((filter) => filter.key !== filterId); innerSetFilters(newFilters); onFiltersUpdate && onFiltersUpdate(newFilters); @@ -120,7 +121,7 @@ function TableHeader({ ); const filterSearch = useCallback( - (filter: FilterType | null, searchValue: string) => { + (filter: SearchConfigType | null, searchValue: string) => { onFilterSearch && onFilterSearch(filter, searchValue); }, [onFilterSearch], 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 ee9930c87..ea460ec8e 100644 --- a/front/src/components/table/table-header/__stories__/FilterDropdownButton.stories.tsx +++ b/front/src/components/table/table-header/__stories__/FilterDropdownButton.stories.tsx @@ -2,16 +2,16 @@ import { ThemeProvider } from '@emotion/react'; import { lightTheme } from '../../../../layout/styles/themes'; import { FilterDropdownButton } from '../FilterDropdownButton'; import styled from '@emotion/styled'; -import { FilterType, SelectedFilterType } from '../interface'; +import { FilterConfigType, SelectedFilterType } from '../interface'; import { useCallback, useState } from 'react'; -import { People_Bool_Exp } from '../../../../generated/graphql'; -import { FaUsers } from 'react-icons/fa'; import { SEARCH_PEOPLE_QUERY, useSearch, } from '../../../../services/search/search'; import { MockedProvider } from '@apollo/client/testing'; import { mockData } from '../../../../pages/people/__tests__/__data__/mock-data'; +import { availableFilters } from '../../../../pages/people/people-table'; +import { Person } from '../../../../interfaces/person.interface'; const component = { title: 'FilterDropdownButton', @@ -78,35 +78,6 @@ const mocks = [ }, ]; -const availableFilters = [ - { - key: 'fullname', - label: 'People', - icon: , - 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[]; - const StyleDiv = styled.div` height: 200px; width: 200px; @@ -114,12 +85,12 @@ const StyleDiv = styled.div` const InnerRegularFilterDropdownButton = ({ setFilter: setFilters, -}: OwnProps) => { - const [, innerSetFilters] = useState>(); +}: OwnProps) => { + const [, innerSetFilters] = useState>(); const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch(); const outerSetFilters = useCallback( - (filter: SelectedFilterType) => { + (filter: SelectedFilterType) => { innerSetFilters(filter); setFilters(filter); }, @@ -128,7 +99,7 @@ const InnerRegularFilterDropdownButton = ({ return ( ) => { +}: OwnProps) => { return ( 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 289de4af4..a60a5614e 100644 --- a/front/src/components/table/table-header/__stories__/SortAndFilterBar.stories.tsx +++ b/front/src/components/table/table-header/__stories__/SortAndFilterBar.stories.tsx @@ -1,8 +1,8 @@ import SortAndFilterBar from '../SortAndFilterBar'; import { ThemeProvider } from '@emotion/react'; import { lightTheme } from '../../../../layout/styles/themes'; -import { GET_PEOPLE } from '../../../../services/people'; import { FaArrowDown } from 'react-icons/fa'; +import { SelectedFilterType } from '../interface'; const component = { title: 'SortAndFilterBar', @@ -45,26 +45,28 @@ export const RegularSortAndFilterBar = ({ filters={[ { label: 'People', - operand: { label: 'Include', id: 'include', keyWord: 'ilike' }, + operand: { + label: 'Include', + id: 'include', + whereTemplate: (person) => { + return { email: { _eq: person.email } }; + }, + }, key: 'test_filter', icon: , - value: 'John Doe', - where: { - firstname: { _ilike: 'John Doe' }, + displayValue: 'john@doedoe.com', + value: { + id: 'test', + email: 'john@doedoe.com', + firstname: 'John', + lastname: 'Doe', + phone: '123456789', + company: null, + creationDate: new Date(), + pipe: null, + city: 'Paris', }, - searchQuery: GET_PEOPLE, - searchTemplate: () => ({ - firstname: { _ilike: 'John Doe' }, - }), - whereTemplate: () => { - return { firstname: { _ilike: 'John Doe' } }; - }, - searchResultMapper: (data) => ({ - displayValue: 'John Doe', - value: data.firstname, - }), - operands: [], - }, + } satisfies SelectedFilterType, ]} /> 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 65e71f37f..4213cabe4 100644 --- a/front/src/components/table/table-header/__tests__/FilterDropdownButton.test.tsx +++ b/front/src/components/table/table-header/__tests__/FilterDropdownButton.test.tsx @@ -1,6 +1,5 @@ import { fireEvent, render, waitFor } from '@testing-library/react'; import { RegularFilterDropdownButton } from '../__stories__/FilterDropdownButton.stories'; -import { FaUsers } from 'react-icons/fa'; it('Checks the default top option is Include', async () => { const setFilters = jest.fn(); @@ -24,15 +23,9 @@ it('Checks the default top option is Include', async () => { expect(setFilters).toHaveBeenCalledWith( expect.objectContaining({ - key: 'Alexandre Prot', - value: 'Alexandre Prot', + displayValue: 'Alexandre Prot', + key: 'fullname', label: 'People', - operand: { - id: 'equal', - keyWord: 'equal', - label: 'Equal', - }, - icon: , }), ); }); @@ -65,15 +58,9 @@ it('Checks the selection of top option for Not Equal', async () => { expect(setFilters).toHaveBeenCalledWith( expect.objectContaining({ - key: 'Alexandre Prot', - value: 'Alexandre Prot', + key: 'fullname', + displayValue: 'Alexandre Prot', label: 'People', - operand: { - id: 'not-equal', - keyWord: 'not_equal', - label: 'Not equal', - }, - icon: , }), ); const blueSortDropdownButton = getByText('Filter'); @@ -118,15 +105,9 @@ it('Calls the filters when typing a new name', async () => { expect(setFilters).toHaveBeenCalledWith( expect.objectContaining({ - key: 'Jane Doe', - value: 'Jane Doe', + key: 'fullname', + displayValue: 'Jane Doe', label: 'People', - operand: { - id: 'equal', - keyWord: 'equal', - label: 'Equal', - }, - icon: , }), ); const blueSortDropdownButton = getByText('Filter'); diff --git a/front/src/components/table/table-header/helpers.ts b/front/src/components/table/table-header/helpers.ts index 66ab073f4..afba39c3a 100644 --- a/front/src/components/table/table-header/helpers.ts +++ b/front/src/components/table/table-header/helpers.ts @@ -1,13 +1,12 @@ import { Order_By } from '../../../generated/graphql'; -import { SelectedFilterType, SelectedSortType } from './interface'; +import { BoolExpType, SelectedFilterType, SelectedSortType } from './interface'; -export const reduceFiltersToWhere = ( - filters: Array>, -): T => { +export const reduceFiltersToWhere = ( + filters: Array>, +): BoolExpType => { const where = filters.reduce((acc, filter) => { - const { where } = filter; - return { ...acc, ...where }; - }, {} as T); + return { ...acc, ...filter.operand.whereTemplate(filter.value) }; + }, {} as BoolExpType); return where; }; diff --git a/front/src/components/table/table-header/interface.ts b/front/src/components/table/table-header/interface.ts index 6380f30e8..2a64ba708 100644 --- a/front/src/components/table/table-header/interface.ts +++ b/front/src/components/table/table-header/interface.ts @@ -6,6 +6,15 @@ import { People_Bool_Exp, Users_Bool_Exp, } 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 = | { @@ -26,33 +35,64 @@ export type SelectedSortType = SortType & { order: 'asc' | 'desc'; }; -export type FilterType> = { - operands: FilterOperandType[]; - label: string; +export type FilterableFieldsType = Person | Company; +export type FilterWhereType = Person | Company | User; + +type FilterConfigGqlType = WhereType extends Company + ? GraphqlQueryCompany + : WhereType extends Person + ? GraphqlQueryPerson + : WhereType extends User + ? GraphqlQueryUser + : never; + +export type BoolExpType = T extends Company + ? Companies_Bool_Exp + : T extends Person + ? People_Bool_Exp + : never; + +export type FilterConfigType = { key: string; + label: string; icon: ReactNode; - whereTemplate: ( - operand: FilterOperandType, - value: FilterValue, - ) => WhereTemplate | undefined; - searchQuery: DocumentNode; - searchTemplate: ( + operands: FilterOperandType[]; + searchConfig: WhereType extends SearchableType + ? SearchConfigType + : null; + selectedValueRender: (selected: WhereType) => string; +}; + +export type SearchableType = Person | Company | User; + +export type SearchConfigType = { + query: DocumentNode; + template: ( searchInput: string, ) => People_Bool_Exp | Companies_Bool_Exp | Users_Bool_Exp; - searchResultMapper: (data: any) => { - displayValue: string; - value: FilterValue; + resultMapper: (data: FilterConfigGqlType) => { + value: SearchType; + render: (value: SearchType) => ReactNode; }; }; -export type FilterOperandType = { +export type FilterOperandType< + FilteredType = FilterableFieldsType, + WhereType = any, +> = { label: string; id: string; - keyWord: 'ilike' | 'not_ilike' | 'equal' | 'not_equal'; + whereTemplate: (value: WhereType) => BoolExpType; }; -export type SelectedFilterType = FilterType & { - value: string; - operand: FilterOperandType; - where: WhereTemplate; +export type SelectedFilterType< + FilteredType = FilterableFieldsType, + WhereType = any, +> = { + key: string; + value: WhereType; + displayValue: string; + label: string; + icon: ReactNode; + operand: FilterOperandType; }; diff --git a/front/src/interfaces/company.interface.ts b/front/src/interfaces/company.interface.ts index c6974655a..3ae6397e5 100644 --- a/front/src/interfaces/company.interface.ts +++ b/front/src/interfaces/company.interface.ts @@ -17,9 +17,6 @@ export type Company = { creationDate: Date; }; -export type PartialCompany = Partial & - Pick; - export type GraphqlQueryCompany = { id: string; name: string; diff --git a/front/src/interfaces/person.interface.test.ts b/front/src/interfaces/person.interface.test.ts index 002237200..7eebcd860 100644 --- a/front/src/interfaces/person.interface.test.ts +++ b/front/src/interfaces/person.interface.test.ts @@ -15,6 +15,10 @@ describe('mapPerson', () => { id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', name: '', domain_name: '', + employees: 0, + address: '', + created_at: '', + account_owner: null, }, __typename: '', }); diff --git a/front/src/interfaces/person.interface.ts b/front/src/interfaces/person.interface.ts index e9f739701..d6d34e891 100644 --- a/front/src/interfaces/person.interface.ts +++ b/front/src/interfaces/person.interface.ts @@ -1,4 +1,4 @@ -import { PartialCompany } from './company.interface'; +import { Company, GraphqlQueryCompany, mapCompany } from './company.interface'; import { Pipe } from './pipe.interface'; export type Person = { @@ -7,7 +7,7 @@ export type Person = { lastname: string; picture?: string; email: string; - company: PartialCompany | null; + company: Company | null; phone: string; creationDate: Date; pipe: Pipe | null; @@ -16,12 +16,7 @@ export type Person = { export type GraphqlQueryPerson = { city: string; - company: { - __typename: string; - id: string; - name: string; - domain_name: string; - }; + company: GraphqlQueryCompany | null; created_at: string; email: string; firstname: string; @@ -56,13 +51,7 @@ export const mapPerson = (person: GraphqlQueryPerson): Person => ({ id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', icon: '💰', }, - company: person.company - ? { - id: person.company.id, - name: person.company.name, - domain_name: person.company.domain_name, - } - : null, + company: person.company ? mapCompany(person.company) : null, }); export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({ diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx index c4212c473..97e750952 100644 --- a/front/src/pages/companies/Companies.tsx +++ b/front/src/pages/companies/Companies.tsx @@ -25,7 +25,10 @@ import { Companies_Bool_Exp, Companies_Order_By, } 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 ActionBar from '../../components/table/action-bar/ActionBar'; @@ -47,7 +50,7 @@ function Companies() { }, []); const updateFilters = useCallback( - (filters: Array>) => { + (filters: Array>) => { setWhere(reduceFiltersToWhere(filters)); }, [], @@ -108,7 +111,7 @@ function Companies() { viewName="All Companies" viewIcon={} availableSorts={availableSorts} - availableFilters={availableFilters} + availableFilters={availableFilters as Array} filterSearchResults={filterSearchResults} onSortsUpdate={updateSorts} onFiltersUpdate={updateFilters} diff --git a/front/src/pages/companies/__tests__/__snapshots__/companies-filter.test.ts.snap b/front/src/pages/companies/__tests__/__snapshots__/companies-filter.test.ts.snap deleted file mode 100644 index 75edaba29..000000000 --- a/front/src/pages/companies/__tests__/__snapshots__/companies-filter.test.ts.snap +++ /dev/null @@ -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%", - }, -} -`; diff --git a/front/src/pages/companies/__tests__/companies-filter.test.ts b/front/src/pages/companies/__tests__/companies-filter.test.ts deleted file mode 100644 index e92ce7490..000000000 --- a/front/src/pages/companies/__tests__/companies-filter.test.ts +++ /dev/null @@ -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( - filter: FilterType, -): filter is FilterType & { - searchResultMapper: (data: GraphqlQueryCompany) => { - displayValue: string; - value: FilterValue; - }; -} { - return filter.searchQuery === SEARCH_COMPANY_QUERY; -} - -function assertFilterUsePeopleSearch( - filter: FilterType, -): filter is FilterType & { - 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(); - }); - } -}); diff --git a/front/src/pages/companies/companies-table.tsx b/front/src/pages/companies/companies-table.tsx index 4718ebe59..5b5dd6cd2 100644 --- a/front/src/pages/companies/companies-table.tsx +++ b/front/src/pages/companies/companies-table.tsx @@ -1,8 +1,5 @@ import { CellContext, createColumnHelper } from '@tanstack/react-table'; -import { - Company, - GraphqlQueryCompany, -} from '../../interfaces/company.interface'; +import { Company, mapCompany } from '../../interfaces/company.interface'; import { updateCompany } from '../../services/companies'; import ColumnHead from '../../components/table/ColumnHead'; import CompanyChip from '../../components/chips/CompanyChip'; @@ -15,28 +12,24 @@ import { FaRegUser, FaUsers, FaBuilding, - FaUser, } from 'react-icons/fa'; import PersonChip, { PersonChipPropsType, } from '../../components/chips/PersonChip'; import EditableChip from '../../components/table/editable-cell/EditableChip'; import { - FilterType, + FilterConfigType, + SearchConfigType, SortType, } from '../../components/table/table-header/interface'; -import { - Companies_Bool_Exp, - Companies_Order_By, - Users_Bool_Exp, -} from '../../generated/graphql'; +import { Companies_Order_By } from '../../generated/graphql'; import { SEARCH_COMPANY_QUERY, SEARCH_USER_QUERY, } from '../../services/search/search'; import EditableDate from '../../components/table/editable-cell/EditableDate'; 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 { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox'; import Checkbox from '../../components/form/Checkbox'; @@ -79,63 +72,67 @@ export const availableFilters = [ key: 'company_name', label: 'Company', icon: , - whereTemplate: (operand, { companyName }) => { - if (operand.keyWord === 'equal') { - return { - name: { _eq: companyName }, - }; - } - - if (operand.keyWord === 'not_equal') { - return { - _not: { name: { _eq: companyName } }, - }; - } + searchConfig: { + query: SEARCH_COMPANY_QUERY, + template: (searchInput) => ({ + name: { _ilike: `%${searchInput}%` }, + }), + resultMapper: (company) => ({ + render: (company) => company.name, + value: mapCompany(company), + }), }, - searchQuery: SEARCH_COMPANY_QUERY, - searchTemplate: (searchInput: string) => ({ - name: { _ilike: `%${searchInput}%` }, - }), - searchResultMapper: (company: GraphqlQueryCompany) => ({ - displayValue: company.name, - value: { companyName: company.name }, - }), + selectedValueRender: (company) => company.name, 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, { - key: 'domainName', + key: 'company_domain_name', label: 'Url', icon: , - whereTemplate: (operand, { domainName }) => { - if (operand.keyWord === 'equal') { - return { - domain_name: { _eq: domainName }, - }; - } - - if (operand.keyWord === 'not_equal') { - return { - _not: { domain_name: { _eq: domainName } }, - }; - } + searchConfig: { + query: SEARCH_COMPANY_QUERY, + template: (searchInput) => ({ + name: { _ilike: `%${searchInput}%` }, + }), + resultMapper: (company) => ({ + render: (company) => company.domain_name, + value: mapCompany(company), + }), }, - searchQuery: SEARCH_COMPANY_QUERY, - searchTemplate: (searchInput: string) => ({ - domain_name: { _ilike: `%${searchInput}%` }, - }), - searchResultMapper: (company: GraphqlQueryCompany) => ({ - displayValue: company.domain_name, - value: { domainName: company.domain_name }, - }), + selectedValueRender: (company) => company.domain_name, 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 } }, + }), + }, ], - }, -] satisfies Array>; + } as FilterConfigType, +]; const columnHelper = createColumnHelper(); @@ -239,18 +236,18 @@ export const useCompaniesColumns = () => { } /> ), cell: (props) => ( - + relation={props.row.original.accountOwner} searchPlaceholder="Account Owner" ChipComponent={PersonChip} chipComponentPropsMapper={( - accountOwner: PartialUser, + accountOwner: User, ): PersonChipPropsType => { return { name: accountOwner.displayName, }; }} - changeHandler={(relation: PartialUser) => { + changeHandler={(relation: User) => { const company = props.row.original; if (company.accountOwner) { company.accountOwner.id = relation.id; @@ -263,28 +260,17 @@ export const useCompaniesColumns = () => { } updateCompany(company); }} - searchFilter={ + searchConfig={ { - key: 'account_owner_name', - label: 'Account Owner', - icon: , - whereTemplate: () => { - return {}; - }, - searchQuery: SEARCH_USER_QUERY, - searchTemplate: (searchInput: string) => ({ + query: SEARCH_USER_QUERY, + template: (searchInput: string) => ({ displayName: { _ilike: `%${searchInput}%` }, }), - searchResultMapper: (accountOwner: GraphqlQueryUser) => ({ - displayValue: accountOwner.displayName, - value: { - id: accountOwner.id, - email: accountOwner.email, - displayName: accountOwner.displayName, - }, + resultMapper: (accountOwner) => ({ + render: (accountOwner) => accountOwner.displayName, + value: mapUser(accountOwner), }), - operands: [], - } satisfies FilterType + } satisfies SearchConfigType } /> ), diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index 348703150..3822a407d 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -19,7 +19,10 @@ import { } from '../../services/people'; import { useSearch } from '../../services/search/search'; 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 { reduceFiltersToWhere, reduceSortsToOrderBy, @@ -44,7 +47,7 @@ function People() { }, []); const updateFilters = useCallback( - (filters: Array>) => { + (filters: Array>) => { setWhere(reduceFiltersToWhere(filters)); }, [], @@ -106,7 +109,7 @@ function People() { viewName="All People" viewIcon={} availableSorts={availableSorts} - availableFilters={availableFilters} + availableFilters={availableFilters as Array} filterSearchResults={filterSearchResults} onSortsUpdate={updateSorts} onFiltersUpdate={updateFilters} 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 index 8905fb197..b2577ff5f 100644 --- a/front/src/pages/people/__tests__/__snapshots__/people-filter.test.ts.snap +++ b/front/src/pages/people/__tests__/__snapshots__/people-filter.test.ts.snap @@ -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%", - }, - }, - ], -} -`; diff --git a/front/src/pages/people/__tests__/people-filter.test.ts b/front/src/pages/people/__tests__/people-filter.test.ts index fed069e07..451ca7316 100644 --- a/front/src/pages/people/__tests__/people-filter.test.ts +++ b/front/src/pages/people/__tests__/people-filter.test.ts @@ -1,65 +1,19 @@ -import { FilterType } from '../../../components/table/table-header/interface'; -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( - filter: FilterType, -): filter is FilterType & { - searchResultMapper: (data: GraphqlQueryCompany) => { - displayValue: string; - value: FilterValue; - }; -} { - return filter.searchQuery === SEARCH_COMPANY_QUERY; -} - -function assertFilterUsePeopleSearch( - filter: FilterType, -): filter is FilterType & { - 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; +import { cityFilter } from '../people-table'; describe('PeopleFilter', () => { - for (const filter of availableFilters) { - it(`should render the filter ${filter.key}`, () => { - if (assertFilterUseCompanySearch(filter)) { - const filterSelectedValue = filter.searchResultMapper( - mockCompanyData[0], - ); - for (const operand of filter.operands) { - expect( - filter.whereTemplate(operand, filterSelectedValue.value), - ).toMatchSnapshot(); - } - } - if (assertFilterUsePeopleSearch(filter)) { - const filterSelectedValue = filter.searchResultMapper(JohnDoeUser); - 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(); - }); - } + it(`should render the filter ${cityFilter.key}`, () => { + expect( + cityFilter.operands[0].whereTemplate({ + id: 'test-id', + city: 'Paris', + email: 'john@doe.com', + firstname: 'John', + lastname: 'Doe', + phone: '0123456789', + creationDate: new Date(), + pipe: null, + company: null, + }), + ).toMatchSnapshot(); + }); }); diff --git a/front/src/pages/people/people-table.tsx b/front/src/pages/people/people-table.tsx index ebaf03286..08fc465e7 100644 --- a/front/src/pages/people/people-table.tsx +++ b/front/src/pages/people/people-table.tsx @@ -14,25 +14,19 @@ import Checkbox from '../../components/form/Checkbox'; import CompanyChip, { CompanyChipPropsType, } 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 { - FilterType, + FilterConfigType, + SearchConfigType, SortType, } from '../../components/table/table-header/interface'; -import { - Order_By, - People_Bool_Exp, - People_Order_By, -} from '../../generated/graphql'; +import { Order_By, People_Order_By } from '../../generated/graphql'; import { SEARCH_COMPANY_QUERY, SEARCH_PEOPLE_QUERY, } from '../../services/search/search'; -import { - GraphqlQueryCompany, - PartialCompany, -} from '../../interfaces/company.interface'; +import { Company, mapCompany } from '../../interfaces/company.interface'; import EditablePhone from '../../components/table/editable-cell/EditablePhone'; import EditableFullName from '../../components/table/editable-cell/EditableFullName'; import EditableDate from '../../components/table/editable-cell/EditableDate'; @@ -85,163 +79,155 @@ export const availableSorts = [ }, ] satisfies Array>; -const fullnameFilter = { +export const fullnameFilter = { key: 'fullname', label: 'People', icon: , - whereTemplate: (operand, { firstname, lastname }) => { - if (operand.keyWord === 'equal') { - return { + searchConfig: { + query: SEARCH_PEOPLE_QUERY, + 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: [ - { firstname: { _eq: `${firstname}` } }, - { lastname: { _eq: `${lastname}` } }, + { firstname: { _eq: `${person.firstname}` } }, + { lastname: { _eq: `${person.lastname}` } }, ], - }; - } - - if (operand.keyWord === 'not_equal') { - return { + }), + }, + { + label: 'Not equal', + id: 'not-equal', + whereTemplate: (person) => ({ _not: { _and: [ - { firstname: { _eq: `${firstname}` } }, - { lastname: { _eq: `${lastname}` } }, + { firstname: { _eq: `${person.firstname}` } }, + { 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; +} satisfies FilterConfigType; -const companyFilter = { +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 } } }, - }; - } + searchConfig: { + query: SEARCH_COMPANY_QUERY, + template: (searchInput: string) => ({ + name: { _ilike: `%${searchInput}%` }, + }), + resultMapper: (data) => ({ + value: mapCompany(data), + render: (company) => company.name, + }), }, - searchQuery: SEARCH_COMPANY_QUERY, - searchTemplate: (searchInput: string) => ({ - name: { _ilike: `%${searchInput}%` }, - }), - searchResultMapper: (company: GraphqlQueryCompany) => ({ - displayValue: company.name, - value: { companyName: company.name }, - }), + selectedValueRender: (company) => company.name, 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; +} satisfies FilterConfigType; -const emailFilter = { +export const emailFilter = { key: 'email', label: 'Email', icon: , - whereTemplate: (operand, { email }) => { - if (operand.keyWord === 'equal') { - return { - email: { _eq: email }, - }; - } - - if (operand.keyWord === 'not_equal') { - return { - _not: { email: { _eq: email } }, - }; - } + searchConfig: { + query: SEARCH_PEOPLE_QUERY, + template: (searchInput: string) => ({ + email: { _ilike: `%${searchInput}%` }, + }), + resultMapper: (person) => ({ + render: (person) => person.email, + value: mapPerson(person), + }), }, - searchQuery: SEARCH_PEOPLE_QUERY, - searchTemplate: (searchInput: string) => ({ - email: { _ilike: `%${searchInput}%` }, - }), - searchResultMapper: (person: GraphqlQueryPerson) => ({ - displayValue: person.email, - value: { email: person.email }, - }), 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; + selectedValueRender: (person) => person.email, +} satisfies FilterConfigType; -const cityFilter = { +export const cityFilter = { key: 'city', label: 'City', icon: , - whereTemplate: (operand, { city }) => { - if (operand.keyWord === 'equal') { - return { - city: { _eq: city }, - }; - } - - if (operand.keyWord === 'not_equal') { - return { - _not: { city: { _eq: city } }, - }; - } + searchConfig: { + query: SEARCH_PEOPLE_QUERY, + template: (searchInput: string) => ({ + city: { _ilike: `%${searchInput}%` }, + }), + resultMapper: (person) => ({ + render: (person) => person.city, + value: mapPerson(person), + }), }, - searchQuery: SEARCH_PEOPLE_QUERY, - searchTemplate: (searchInput: string) => ({ - city: { _ilike: `%${searchInput}%` }, - }), - searchResultMapper: (person: GraphqlQueryPerson) => ({ - displayValue: person.city, - value: { city: person.city }, - }), 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; + selectedValueRender: (person) => person.email, +} satisfies FilterConfigType; export const availableFilters = [ fullnameFilter, companyFilter, emailFilter, 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[]; +]; const columnHelper = createColumnHelper(); @@ -300,53 +286,36 @@ export const usePeopleColumns = () => { } /> ), cell: (props) => ( - + relation={props.row.original.company} searchPlaceholder="Company" ChipComponent={CompanyChip} - chipComponentPropsMapper={( - company: PartialCompany, - ): CompanyChipPropsType => { + chipComponentPropsMapper={(company): CompanyChipPropsType => { return { name: company.name, picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`, }; }} - changeHandler={(relation: PartialCompany) => { + changeHandler={(relation) => { const person = props.row.original; if (person.company) { person.company.id = relation.id; } else { - person.company = { - id: relation.id, - name: relation.name, - domain_name: relation.domain_name, - }; + person.company = relation; } updatePerson(person); }} - searchFilter={ + searchConfig={ { - key: 'company_name', - label: 'Company', - icon: , - whereTemplate: () => { - return {}; - }, - searchQuery: SEARCH_COMPANY_QUERY, - searchTemplate: (searchInput: string) => ({ + query: SEARCH_COMPANY_QUERY, + template: (searchInput: string) => ({ name: { _ilike: `%${searchInput}%` }, }), - searchResultMapper: (company: GraphqlQueryCompany) => ({ - displayValue: company.name, - value: { - id: company.id, - name: company.name, - domain_name: company.domain_name, - }, + resultMapper: (company) => ({ + render: (company) => company.name, + value: mapCompany(company), }), - operands: [], - } satisfies FilterType + } satisfies SearchConfigType } /> ), diff --git a/front/src/services/search/search.ts b/front/src/services/search/search.ts index 4719f0589..1c911fc23 100644 --- a/front/src/services/search/search.ts +++ b/front/src/services/search/search.ts @@ -1,8 +1,9 @@ import { gql, useQuery } from '@apollo/client'; -import { People_Bool_Exp } from '../../generated/graphql'; -import {} from '../../interfaces/company.interface'; 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` query SearchQuery($where: people_bool_exp, $limit: Int) { @@ -57,12 +58,18 @@ const debounce = ( }; }; -export const useSearch = (): [ - { results: { displayValue: string; value: any }[]; loading: boolean }, +export const useSearch = (): [ + { + results: { + render: (value: T) => string; + value: T; + }[]; + loading: boolean; + }, React.Dispatch>, - React.Dispatch | null>>, + React.Dispatch | null>>, ] => { - const [filter, setFilter] = useState | null>( + const [searchConfig, setSearchConfig] = useState | null>( null, ); const [searchInput, setSearchInput] = useState(''); @@ -74,26 +81,28 @@ export const useSearch = (): [ const where = useMemo(() => { return ( - filter && filter.searchTemplate && filter.searchTemplate(searchInput) + searchConfig && + searchConfig.template && + searchConfig.template(searchInput) ); - }, [filter, searchInput]); + }, [searchConfig, searchInput]); const searchFilterQueryResults = useQuery( - filter?.searchQuery || EMPTY_QUERY, + searchConfig?.query || EMPTY_QUERY, { variables: { where, limit: 5, }, - skip: !filter, + skip: !searchConfig, }, ); const searchFilterResults = useMemo<{ - results: { displayValue: string; value: any }[]; + results: { render: (value: T) => string; value: any }[]; loading: boolean; }>(() => { - if (filter == null) { + if (searchConfig == null) { return { loading: false, results: [], @@ -108,10 +117,10 @@ export const useSearch = (): [ return { loading: false, results: searchFilterQueryResults.data.searchResults.map( - filter.searchResultMapper, + searchConfig.resultMapper, ), }; - }, [filter, searchFilterQueryResults]); + }, [searchConfig, searchFilterQueryResults]); - return [searchFilterResults, debouncedsetSearchInput, setFilter]; + return [searchFilterResults, debouncedsetSearchInput, setSearchConfig]; };