From 67353eda8e2e85341f184674c27187bcdca25d9f Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sun, 21 May 2023 22:47:18 +0200 Subject: [PATCH] Add all filters to tables + make column width fixed (#133) * Add additional filters on companies and people page * Make colunn width fixed * Remove duplicate declaration of Unknown type --- .../editable-cell/EditableCellWrapper.tsx | 3 +- front/src/components/form/Checkbox.tsx | 6 +- front/src/components/table/Table.tsx | 8 +- .../table-header/FilterDropdownButton.tsx | 40 ++++---- .../table/table-header/SortOrFilterChip.tsx | 1 + .../interfaces/entities/generic.interface.ts | 2 + front/src/interfaces/filters/interface.ts | 7 +- front/src/interfaces/search/interface.ts | 30 +++--- .../src/pages/companies/companies-columns.tsx | 6 ++ .../src/pages/companies/companies-filters.tsx | 94 +++++++++++++------ front/src/pages/people/people-columns.tsx | 6 ++ front/src/pages/people/people-filters.tsx | 75 +++++++++++++-- front/src/services/api/search/search.ts | 22 +++-- 13 files changed, 214 insertions(+), 86 deletions(-) diff --git a/front/src/components/editable-cell/EditableCellWrapper.tsx b/front/src/components/editable-cell/EditableCellWrapper.tsx index eb6cc128d318..0fd94ee95aad 100644 --- a/front/src/components/editable-cell/EditableCellWrapper.tsx +++ b/front/src/components/editable-cell/EditableCellWrapper.tsx @@ -31,10 +31,11 @@ type StyledEditModeContainerProps = { const StyledNonEditModeContainer = styled.div` display: flex; align-items: center; - width: 100%; + width: calc(100% - ${(props) => props.theme.spacing(5)}); height: 100%; padding-left: ${(props) => props.theme.spacing(2)}; padding-right: ${(props) => props.theme.spacing(2)}; + overflow: hidden; `; const StyledEditModeContainer = styled.div` diff --git a/front/src/components/form/Checkbox.tsx b/front/src/components/form/Checkbox.tsx index 43216c0dc693..10ca8a473d81 100644 --- a/front/src/components/form/Checkbox.tsx +++ b/front/src/components/form/Checkbox.tsx @@ -21,13 +21,17 @@ const StyledContainer = styled.span` input[type='checkbox']::before { content: ''; - border: 1px solid ${(props) => props.theme.text80}; + border: 1px solid ${(props) => props.theme.text40}; width: 12px; height: 12px; border-radius: 2px; display: block; } + input[type='checkbox']:hover::before { + border: 1px solid ${(props) => props.theme.text80}; + } + input[type='checkbox']:checked::before { border: 1px solid ${(props) => props.theme.blue}; } diff --git a/front/src/components/table/Table.tsx b/front/src/components/table/Table.tsx index c2a6ea020c91..bf15efacf1d2 100644 --- a/front/src/components/table/Table.tsx +++ b/front/src/components/table/Table.tsx @@ -43,6 +43,7 @@ const StyledTable = styled.table` border-collapse: collapse; margin-left: ${(props) => props.theme.spacing(2)}; margin-right: ${(props) => props.theme.spacing(2)}; + table-layout: fixed; th { border-collapse: collapse; @@ -148,7 +149,12 @@ const Table = < {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( - + {header.isPlaceholder ? null : flexRender( diff --git a/front/src/components/table/table-header/FilterDropdownButton.tsx b/front/src/components/table/table-header/FilterDropdownButton.tsx index 1490e09c5f3c..586c031d5dd0 100644 --- a/front/src/components/table/table-header/FilterDropdownButton.tsx +++ b/front/src/components/table/table-header/FilterDropdownButton.tsx @@ -91,25 +91,27 @@ export const FilterDropdownButton = ({ ); } - return filterSearchResults.results.map((result, index) => ( - { - onFilterSelect({ - key: selectedFilter.key, - label: selectedFilter.label, - value: result.value, - displayValue: result.render(result.value), - icon: selectedFilter.icon, - operand: selectedFilterOperand, - }); - setIsUnfolded(false); - setSelectedFilter(undefined); - }} - > - {result.render(result.value)} - - )); + return filterSearchResults.results.map((result, index) => { + return ( + { + onFilterSelect({ + key: selectedFilter.key, + label: selectedFilter.label, + value: result.value, + displayValue: result.render(result.value), + icon: selectedFilter.icon, + operand: selectedFilterOperand, + }); + setIsUnfolded(false); + setSelectedFilter(undefined); + }} + > + {result.render(result.value)} + + ); + }); }; function renderValueSelection( diff --git a/front/src/components/table/table-header/SortOrFilterChip.tsx b/front/src/components/table/table-header/SortOrFilterChip.tsx index 47ad74bb3ed3..27016400dd0e 100644 --- a/front/src/components/table/table-header/SortOrFilterChip.tsx +++ b/front/src/components/table/table-header/SortOrFilterChip.tsx @@ -20,6 +20,7 @@ const StyledChip = styled.div` padding: ${(props) => props.theme.spacing(1) + ' ' + props.theme.spacing(2)}; margin-left: ${(props) => props.theme.spacing(2)}; font-size: ${(props) => props.theme.fontSizeSmall}; + align-items: center; `; const StyledIcon = styled.div` margin-right: ${(props) => props.theme.spacing(1)}; diff --git a/front/src/interfaces/entities/generic.interface.ts b/front/src/interfaces/entities/generic.interface.ts index dce737770a89..bd1be698477d 100644 --- a/front/src/interfaces/entities/generic.interface.ts +++ b/front/src/interfaces/entities/generic.interface.ts @@ -12,6 +12,8 @@ export type AnyEntity = { __typename: string; } & Record; +export type UnknownType = void; + export type GqlType = T extends Company ? GraphqlQueryCompany : T extends Person diff --git a/front/src/interfaces/filters/interface.ts b/front/src/interfaces/filters/interface.ts index dc178dd4189d..38d85d83e5d5 100644 --- a/front/src/interfaces/filters/interface.ts +++ b/front/src/interfaces/filters/interface.ts @@ -1,10 +1,13 @@ import { ReactNode } from 'react'; import { SearchConfigType } from '../search/interface'; -import { AnyEntity, BoolExpType } from '../entities/generic.interface'; +import { + AnyEntity, + BoolExpType, + UnknownType, +} from '../entities/generic.interface'; export type FilterableFieldsType = AnyEntity; export type FilterWhereRelationType = AnyEntity; -type UnknownType = void; export type FilterWhereType = FilterWhereRelationType | string | UnknownType; export type FilterConfigType< diff --git a/front/src/interfaces/search/interface.ts b/front/src/interfaces/search/interface.ts index dbfe823c8065..c5a3b6e4a34e 100644 --- a/front/src/interfaces/search/interface.ts +++ b/front/src/interfaces/search/interface.ts @@ -1,29 +1,25 @@ import { DocumentNode } from 'graphql'; import { ReactNode } from 'react'; import { - Companies_Bool_Exp, - People_Bool_Exp, - Users_Bool_Exp, -} from '../../generated/graphql'; -import { AnyEntity, GqlType } from '../entities/generic.interface'; - -type UnknownType = void; + AnyEntity, + BoolExpType, + GqlType, + UnknownType, +} from '../entities/generic.interface'; export type SearchConfigType< - SearchType extends AnyEntity | UnknownType = AnyEntity, -> = SearchType extends AnyEntity + SearchType extends AnyEntity | UnknownType = UnknownType, +> = SearchType extends UnknownType ? { query: DocumentNode; - template: ( - searchInput: string, - ) => People_Bool_Exp | Companies_Bool_Exp | Users_Bool_Exp; + template: (searchInput: string) => any; + resultMapper: (data: any) => any; + } + : { + query: DocumentNode; + template: (searchInput: string) => BoolExpType; resultMapper: (data: GqlType) => { value: SearchType; render: (value: SearchType) => ReactNode; }; - } - : { - query: DocumentNode; - template: (searchInput: string) => any; - resultMapper: (data: any) => any; }; diff --git a/front/src/pages/companies/companies-columns.tsx b/front/src/pages/companies/companies-columns.tsx index eb6c711fbef5..1ad46a58dd07 100644 --- a/front/src/pages/companies/companies-columns.tsx +++ b/front/src/pages/companies/companies-columns.tsx @@ -50,6 +50,7 @@ export const useCompaniesColumns = () => { onChange={props.row.getToggleSelectedHandler()} /> ), + size: 25, }, columnHelper.accessor('name', { header: () => ( @@ -68,6 +69,7 @@ export const useCompaniesColumns = () => { ChipComponent={CompanyChip} /> ), + size: 120, }), columnHelper.accessor('employees', { header: () => ( @@ -89,6 +91,7 @@ export const useCompaniesColumns = () => { }} /> ), + size: 70, }), columnHelper.accessor('domainName', { header: () => ( @@ -104,6 +107,7 @@ export const useCompaniesColumns = () => { }} /> ), + size: 100, }), columnHelper.accessor('address', { header: () => ( @@ -119,6 +123,7 @@ export const useCompaniesColumns = () => { }} /> ), + size: 170, }), columnHelper.accessor('creationDate', { header: () => ( @@ -134,6 +139,7 @@ export const useCompaniesColumns = () => { }} /> ), + size: 70, }), columnHelper.accessor('accountOwner', { header: () => ( diff --git a/front/src/pages/companies/companies-filters.tsx b/front/src/pages/companies/companies-filters.tsx index 6ae4a2b33a7a..6fbb98bb62eb 100644 --- a/front/src/pages/companies/companies-filters.tsx +++ b/front/src/pages/companies/companies-filters.tsx @@ -4,9 +4,12 @@ import { TbLink, TbMapPin, TbSum, + TbUser, } from 'react-icons/tb'; import { Company } from '../../interfaces/entities/company.interface'; import { FilterConfigType } from '../../interfaces/filters/interface'; +import { SEARCH_USER_QUERY } from '../../services/api/search/search'; +import { User, mapToUser } from '../../interfaces/entities/user.interface'; export const nameFilter = { key: 'company_name', @@ -31,6 +34,33 @@ export const nameFilter = { ], } satisfies FilterConfigType; +export const employeesFilter = { + key: 'company_employees', + label: 'Employees', + icon: , + type: 'text', + operands: [ + { + label: 'Greater than', + id: 'greater_than', + whereTemplate: (searchString) => ({ + employees: { + _gte: isNaN(Number(searchString)) ? undefined : Number(searchString), + }, + }), + }, + { + label: 'Less than', + id: 'less_than', + whereTemplate: (searchString) => ({ + employees: { + _lte: isNaN(Number(searchString)) ? undefined : Number(searchString), + }, + }), + }, + ], +} satisfies FilterConfigType; + export const urlFilter = { key: 'company_domain_name', label: 'Url', @@ -77,33 +107,6 @@ export const addressFilter = { ], } satisfies FilterConfigType; -export const employeesFilter = { - key: 'company_employees', - label: 'Employees', - icon: , - type: 'text', - operands: [ - { - label: 'Greater than', - id: 'greater_than', - whereTemplate: (searchString) => ({ - employees: { - _gte: isNaN(Number(searchString)) ? undefined : Number(searchString), - }, - }), - }, - { - label: 'Less than', - id: 'less_than', - whereTemplate: (searchString) => ({ - employees: { - _lte: isNaN(Number(searchString)) ? undefined : Number(searchString), - }, - }), - }, - ], -} satisfies FilterConfigType; - export const creationDateFilter = { key: 'company_created_at', label: 'Created At', @@ -131,10 +134,45 @@ export const creationDateFilter = { ], } satisfies FilterConfigType; +export const accountOwnerFilter = { + key: 'account_owner_name', + label: 'Account Owner', + icon: , + type: 'relation', + searchConfig: { + query: SEARCH_USER_QUERY, + template: (searchString: string) => ({ + displayName: { _ilike: `%${searchString}%` }, + }), + resultMapper: (data) => ({ + value: mapToUser(data), + render: (owner) => owner.displayName, + }), + }, + selectedValueRender: (owner) => owner.displayName || '', + operands: [ + { + label: 'Is', + id: 'is', + whereTemplate: (owner) => ({ + account_owner: { displayName: { _eq: owner.displayName } }, + }), + }, + { + label: 'Is not', + id: 'is_not', + whereTemplate: (owner) => ({ + _not: { account_owner: { displayName: { _eq: owner.displayName } } }, + }), + }, + ], +} satisfies FilterConfigType; + export const availableFilters = [ nameFilter, + employeesFilter, urlFilter, addressFilter, - employeesFilter, creationDateFilter, + accountOwnerFilter, ]; diff --git a/front/src/pages/people/people-columns.tsx b/front/src/pages/people/people-columns.tsx index b9d8bea84c7e..f9430af4627f 100644 --- a/front/src/pages/people/people-columns.tsx +++ b/front/src/pages/people/people-columns.tsx @@ -53,6 +53,7 @@ export const usePeopleColumns = () => { onChange={props.row.getToggleSelectedHandler()} /> ), + size: 25, }, columnHelper.accessor('firstname', { header: () => ( @@ -70,6 +71,7 @@ export const usePeopleColumns = () => { }} /> ), + size: 200, }), columnHelper.accessor('email', { header: () => ( @@ -86,6 +88,7 @@ export const usePeopleColumns = () => { }} /> ), + size: 200, }), columnHelper.accessor('company', { header: () => ( @@ -125,6 +128,7 @@ export const usePeopleColumns = () => { } /> ), + size: 150, }), columnHelper.accessor('phone', { header: () => ( @@ -141,6 +145,7 @@ export const usePeopleColumns = () => { }} /> ), + size: 130, }), columnHelper.accessor('creationDate', { header: () => ( @@ -156,6 +161,7 @@ export const usePeopleColumns = () => { }} /> ), + size: 100, }), columnHelper.accessor('city', { header: () => ( diff --git a/front/src/pages/people/people-filters.tsx b/front/src/pages/people/people-filters.tsx index 6abc232b3f3f..bc92f9390ac0 100644 --- a/front/src/pages/people/people-filters.tsx +++ b/front/src/pages/people/people-filters.tsx @@ -5,7 +5,14 @@ import { mapToCompany, } from '../../interfaces/entities/company.interface'; import { FilterConfigType } from '../../interfaces/filters/interface'; -import { TbBuilding, TbMail, TbMapPin, TbUser } from 'react-icons/tb'; +import { + TbBuilding, + TbCalendar, + TbMail, + TbMapPin, + TbPhone, + TbUser, +} from 'react-icons/tb'; export const fullnameFilter = { key: 'fullname', @@ -38,6 +45,29 @@ export const fullnameFilter = { ], } satisfies FilterConfigType; +export const emailFilter = { + key: 'email', + label: 'Email', + icon: , + type: 'text', + operands: [ + { + label: 'Contains', + id: 'like', + whereTemplate: (searchString) => ({ + email: { _ilike: `%${searchString}%` }, + }), + }, + { + label: 'Does not contain', + id: 'not_like', + whereTemplate: (searchString) => ({ + _not: { email: { _ilike: `%${searchString}%` } }, + }), + }, + ], +} satisfies FilterConfigType; + export const companyFilter = { key: 'company_name', label: 'Company', @@ -72,29 +102,56 @@ export const companyFilter = { ], } satisfies FilterConfigType; -export const emailFilter = { - key: 'email', - label: 'Email', - icon: , +export const phoneFilter = { + key: 'phone', + label: 'Phone', + icon: , type: 'text', operands: [ { label: 'Contains', id: 'like', whereTemplate: (searchString) => ({ - email: { _ilike: `%${searchString}%` }, + phone: { _ilike: `%${searchString}%` }, }), }, { label: 'Does not contain', id: 'not_like', whereTemplate: (searchString) => ({ - _not: { email: { _ilike: `%${searchString}%` } }, + _not: { phone: { _ilike: `%${searchString}%` } }, }), }, ], } satisfies FilterConfigType; +export const creationDateFilter = { + key: 'person_created_at', + label: 'Created At', + icon: , + type: 'date', + operands: [ + { + label: 'Greater than', + id: 'greater_than', + whereTemplate: (searchString) => ({ + created_at: { + _gte: searchString, + }, + }), + }, + { + label: 'Less than', + id: 'less_than', + whereTemplate: (searchString) => ({ + created_at: { + _lte: searchString, + }, + }), + }, + ], +} satisfies FilterConfigType; + export const cityFilter = { key: 'city', label: 'City', @@ -120,7 +177,9 @@ export const cityFilter = { export const availableFilters = [ fullnameFilter, - companyFilter, emailFilter, + companyFilter, + phoneFilter, + creationDateFilter, cityFilter, ] satisfies FilterConfigType[]; diff --git a/front/src/services/api/search/search.ts b/front/src/services/api/search/search.ts index 8d3d79d553b0..f1e18073e0a7 100644 --- a/front/src/services/api/search/search.ts +++ b/front/src/services/api/search/search.ts @@ -1,7 +1,10 @@ import { gql, useQuery } from '@apollo/client'; import { useMemo, useState } from 'react'; import { SearchConfigType } from '../../../interfaces/search/interface'; -import { AnyEntity } from '../../../interfaces/entities/generic.interface'; +import { + AnyEntity, + UnknownType, +} from '../../../interfaces/entities/generic.interface'; export const SEARCH_PEOPLE_QUERY = gql` query SearchPeopleQuery($where: people_bool_exp, $limit: Int) { @@ -58,15 +61,16 @@ const debounce = ( }; }; -export type SearchResultsType = { - results: { - render: (value: T) => string; - value: T; - }[]; - loading: boolean; -}; +export type SearchResultsType = + { + results: { + render: (value: T) => string; + value: T; + }[]; + loading: boolean; + }; -export const useSearch = (): [ +export const useSearch = (): [ SearchResultsType, React.Dispatch>, React.Dispatch | null>>,