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
This commit is contained in:
Charles Bochet
2023-05-21 22:47:18 +02:00
committed by GitHub
parent 3370499ad8
commit 67353eda8e
13 changed files with 214 additions and 86 deletions

View File

@ -31,10 +31,11 @@ type StyledEditModeContainerProps = {
const StyledNonEditModeContainer = styled.div` const StyledNonEditModeContainer = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: calc(100% - ${(props) => props.theme.spacing(5)});
height: 100%; height: 100%;
padding-left: ${(props) => props.theme.spacing(2)}; padding-left: ${(props) => props.theme.spacing(2)};
padding-right: ${(props) => props.theme.spacing(2)}; padding-right: ${(props) => props.theme.spacing(2)};
overflow: hidden;
`; `;
const StyledEditModeContainer = styled.div<StyledEditModeContainerProps>` const StyledEditModeContainer = styled.div<StyledEditModeContainerProps>`

View File

@ -21,13 +21,17 @@ const StyledContainer = styled.span`
input[type='checkbox']::before { input[type='checkbox']::before {
content: ''; content: '';
border: 1px solid ${(props) => props.theme.text80}; border: 1px solid ${(props) => props.theme.text40};
width: 12px; width: 12px;
height: 12px; height: 12px;
border-radius: 2px; border-radius: 2px;
display: block; display: block;
} }
input[type='checkbox']:hover::before {
border: 1px solid ${(props) => props.theme.text80};
}
input[type='checkbox']:checked::before { input[type='checkbox']:checked::before {
border: 1px solid ${(props) => props.theme.blue}; border: 1px solid ${(props) => props.theme.blue};
} }

View File

@ -43,6 +43,7 @@ const StyledTable = styled.table`
border-collapse: collapse; border-collapse: collapse;
margin-left: ${(props) => props.theme.spacing(2)}; margin-left: ${(props) => props.theme.spacing(2)};
margin-right: ${(props) => props.theme.spacing(2)}; margin-right: ${(props) => props.theme.spacing(2)};
table-layout: fixed;
th { th {
border-collapse: collapse; border-collapse: collapse;
@ -148,7 +149,12 @@ const Table = <
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<th key={header.id}> <th
key={header.id}
style={{
width: `${header.getSize()}px`,
}}
>
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(

View File

@ -91,25 +91,27 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
); );
} }
return filterSearchResults.results.map((result, index) => ( return filterSearchResults.results.map((result, index) => {
<DropdownButton.StyledDropdownItem return (
key={`fields-value-${index}`} <DropdownButton.StyledDropdownItem
onClick={() => { key={`fields-value-${index}`}
onFilterSelect({ onClick={() => {
key: selectedFilter.key, onFilterSelect({
label: selectedFilter.label, key: selectedFilter.key,
value: result.value, label: selectedFilter.label,
displayValue: result.render(result.value), value: result.value,
icon: selectedFilter.icon, displayValue: result.render(result.value),
operand: selectedFilterOperand, icon: selectedFilter.icon,
}); operand: selectedFilterOperand,
setIsUnfolded(false); });
setSelectedFilter(undefined); setIsUnfolded(false);
}} setSelectedFilter(undefined);
> }}
{result.render(result.value)} >
</DropdownButton.StyledDropdownItem> {result.render(result.value)}
)); </DropdownButton.StyledDropdownItem>
);
});
}; };
function renderValueSelection( function renderValueSelection(

View File

@ -20,6 +20,7 @@ const StyledChip = styled.div`
padding: ${(props) => props.theme.spacing(1) + ' ' + props.theme.spacing(2)}; padding: ${(props) => props.theme.spacing(1) + ' ' + props.theme.spacing(2)};
margin-left: ${(props) => props.theme.spacing(2)}; margin-left: ${(props) => props.theme.spacing(2)};
font-size: ${(props) => props.theme.fontSizeSmall}; font-size: ${(props) => props.theme.fontSizeSmall};
align-items: center;
`; `;
const StyledIcon = styled.div` const StyledIcon = styled.div`
margin-right: ${(props) => props.theme.spacing(1)}; margin-right: ${(props) => props.theme.spacing(1)};

View File

@ -12,6 +12,8 @@ export type AnyEntity = {
__typename: string; __typename: string;
} & Record<string, any>; } & Record<string, any>;
export type UnknownType = void;
export type GqlType<T> = T extends Company export type GqlType<T> = T extends Company
? GraphqlQueryCompany ? GraphqlQueryCompany
: T extends Person : T extends Person

View File

@ -1,10 +1,13 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { SearchConfigType } from '../search/interface'; 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 FilterableFieldsType = AnyEntity;
export type FilterWhereRelationType = AnyEntity; export type FilterWhereRelationType = AnyEntity;
type UnknownType = void;
export type FilterWhereType = FilterWhereRelationType | string | UnknownType; export type FilterWhereType = FilterWhereRelationType | string | UnknownType;
export type FilterConfigType< export type FilterConfigType<

View File

@ -1,29 +1,25 @@
import { DocumentNode } from 'graphql'; import { DocumentNode } from 'graphql';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { import {
Companies_Bool_Exp, AnyEntity,
People_Bool_Exp, BoolExpType,
Users_Bool_Exp, GqlType,
} from '../../generated/graphql'; UnknownType,
import { AnyEntity, GqlType } from '../entities/generic.interface'; } from '../entities/generic.interface';
type UnknownType = void;
export type SearchConfigType< export type SearchConfigType<
SearchType extends AnyEntity | UnknownType = AnyEntity, SearchType extends AnyEntity | UnknownType = UnknownType,
> = SearchType extends AnyEntity > = SearchType extends UnknownType
? { ? {
query: DocumentNode; query: DocumentNode;
template: ( template: (searchInput: string) => any;
searchInput: string, resultMapper: (data: any) => any;
) => People_Bool_Exp | Companies_Bool_Exp | Users_Bool_Exp; }
: {
query: DocumentNode;
template: (searchInput: string) => BoolExpType<SearchType>;
resultMapper: (data: GqlType<SearchType>) => { resultMapper: (data: GqlType<SearchType>) => {
value: SearchType; value: SearchType;
render: (value: SearchType) => ReactNode; render: (value: SearchType) => ReactNode;
}; };
}
: {
query: DocumentNode;
template: (searchInput: string) => any;
resultMapper: (data: any) => any;
}; };

View File

@ -50,6 +50,7 @@ export const useCompaniesColumns = () => {
onChange={props.row.getToggleSelectedHandler()} onChange={props.row.getToggleSelectedHandler()}
/> />
), ),
size: 25,
}, },
columnHelper.accessor('name', { columnHelper.accessor('name', {
header: () => ( header: () => (
@ -68,6 +69,7 @@ export const useCompaniesColumns = () => {
ChipComponent={CompanyChip} ChipComponent={CompanyChip}
/> />
), ),
size: 120,
}), }),
columnHelper.accessor('employees', { columnHelper.accessor('employees', {
header: () => ( header: () => (
@ -89,6 +91,7 @@ export const useCompaniesColumns = () => {
}} }}
/> />
), ),
size: 70,
}), }),
columnHelper.accessor('domainName', { columnHelper.accessor('domainName', {
header: () => ( header: () => (
@ -104,6 +107,7 @@ export const useCompaniesColumns = () => {
}} }}
/> />
), ),
size: 100,
}), }),
columnHelper.accessor('address', { columnHelper.accessor('address', {
header: () => ( header: () => (
@ -119,6 +123,7 @@ export const useCompaniesColumns = () => {
}} }}
/> />
), ),
size: 170,
}), }),
columnHelper.accessor('creationDate', { columnHelper.accessor('creationDate', {
header: () => ( header: () => (
@ -134,6 +139,7 @@ export const useCompaniesColumns = () => {
}} }}
/> />
), ),
size: 70,
}), }),
columnHelper.accessor('accountOwner', { columnHelper.accessor('accountOwner', {
header: () => ( header: () => (

View File

@ -4,9 +4,12 @@ import {
TbLink, TbLink,
TbMapPin, TbMapPin,
TbSum, TbSum,
TbUser,
} from 'react-icons/tb'; } from 'react-icons/tb';
import { Company } from '../../interfaces/entities/company.interface'; import { Company } from '../../interfaces/entities/company.interface';
import { FilterConfigType } from '../../interfaces/filters/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 = { export const nameFilter = {
key: 'company_name', key: 'company_name',
@ -31,6 +34,33 @@ export const nameFilter = {
], ],
} satisfies FilterConfigType<Company, string>; } satisfies FilterConfigType<Company, string>;
export const employeesFilter = {
key: 'company_employees',
label: 'Employees',
icon: <TbSum size={16} />,
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<Company, string>;
export const urlFilter = { export const urlFilter = {
key: 'company_domain_name', key: 'company_domain_name',
label: 'Url', label: 'Url',
@ -77,33 +107,6 @@ export const addressFilter = {
], ],
} satisfies FilterConfigType<Company, string>; } satisfies FilterConfigType<Company, string>;
export const employeesFilter = {
key: 'company_employees',
label: 'Employees',
icon: <TbSum size={16} />,
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<Company, string>;
export const creationDateFilter = { export const creationDateFilter = {
key: 'company_created_at', key: 'company_created_at',
label: 'Created At', label: 'Created At',
@ -131,10 +134,45 @@ export const creationDateFilter = {
], ],
} satisfies FilterConfigType<Company, string>; } satisfies FilterConfigType<Company, string>;
export const accountOwnerFilter = {
key: 'account_owner_name',
label: 'Account Owner',
icon: <TbUser size={16} />,
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<Company, User>;
export const availableFilters = [ export const availableFilters = [
nameFilter, nameFilter,
employeesFilter,
urlFilter, urlFilter,
addressFilter, addressFilter,
employeesFilter,
creationDateFilter, creationDateFilter,
accountOwnerFilter,
]; ];

View File

@ -53,6 +53,7 @@ export const usePeopleColumns = () => {
onChange={props.row.getToggleSelectedHandler()} onChange={props.row.getToggleSelectedHandler()}
/> />
), ),
size: 25,
}, },
columnHelper.accessor('firstname', { columnHelper.accessor('firstname', {
header: () => ( header: () => (
@ -70,6 +71,7 @@ export const usePeopleColumns = () => {
}} }}
/> />
), ),
size: 200,
}), }),
columnHelper.accessor('email', { columnHelper.accessor('email', {
header: () => ( header: () => (
@ -86,6 +88,7 @@ export const usePeopleColumns = () => {
}} }}
/> />
), ),
size: 200,
}), }),
columnHelper.accessor('company', { columnHelper.accessor('company', {
header: () => ( header: () => (
@ -125,6 +128,7 @@ export const usePeopleColumns = () => {
} }
/> />
), ),
size: 150,
}), }),
columnHelper.accessor('phone', { columnHelper.accessor('phone', {
header: () => ( header: () => (
@ -141,6 +145,7 @@ export const usePeopleColumns = () => {
}} }}
/> />
), ),
size: 130,
}), }),
columnHelper.accessor('creationDate', { columnHelper.accessor('creationDate', {
header: () => ( header: () => (
@ -156,6 +161,7 @@ export const usePeopleColumns = () => {
}} }}
/> />
), ),
size: 100,
}), }),
columnHelper.accessor('city', { columnHelper.accessor('city', {
header: () => ( header: () => (

View File

@ -5,7 +5,14 @@ import {
mapToCompany, mapToCompany,
} from '../../interfaces/entities/company.interface'; } from '../../interfaces/entities/company.interface';
import { FilterConfigType } from '../../interfaces/filters/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 = { export const fullnameFilter = {
key: 'fullname', key: 'fullname',
@ -38,6 +45,29 @@ export const fullnameFilter = {
], ],
} satisfies FilterConfigType<Person, string>; } satisfies FilterConfigType<Person, string>;
export const emailFilter = {
key: 'email',
label: 'Email',
icon: <TbMail size={16} />,
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<Person, string>;
export const companyFilter = { export const companyFilter = {
key: 'company_name', key: 'company_name',
label: 'Company', label: 'Company',
@ -72,29 +102,56 @@ export const companyFilter = {
], ],
} satisfies FilterConfigType<Person, Company>; } satisfies FilterConfigType<Person, Company>;
export const emailFilter = { export const phoneFilter = {
key: 'email', key: 'phone',
label: 'Email', label: 'Phone',
icon: <TbMail size={16} />, icon: <TbPhone size={16} />,
type: 'text', type: 'text',
operands: [ operands: [
{ {
label: 'Contains', label: 'Contains',
id: 'like', id: 'like',
whereTemplate: (searchString) => ({ whereTemplate: (searchString) => ({
email: { _ilike: `%${searchString}%` }, phone: { _ilike: `%${searchString}%` },
}), }),
}, },
{ {
label: 'Does not contain', label: 'Does not contain',
id: 'not_like', id: 'not_like',
whereTemplate: (searchString) => ({ whereTemplate: (searchString) => ({
_not: { email: { _ilike: `%${searchString}%` } }, _not: { phone: { _ilike: `%${searchString}%` } },
}), }),
}, },
], ],
} satisfies FilterConfigType<Person, string>; } satisfies FilterConfigType<Person, string>;
export const creationDateFilter = {
key: 'person_created_at',
label: 'Created At',
icon: <TbCalendar size={16} />,
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<Company, string>;
export const cityFilter = { export const cityFilter = {
key: 'city', key: 'city',
label: 'City', label: 'City',
@ -120,7 +177,9 @@ export const cityFilter = {
export const availableFilters = [ export const availableFilters = [
fullnameFilter, fullnameFilter,
companyFilter,
emailFilter, emailFilter,
companyFilter,
phoneFilter,
creationDateFilter,
cityFilter, cityFilter,
] satisfies FilterConfigType<Person>[]; ] satisfies FilterConfigType<Person>[];

View File

@ -1,7 +1,10 @@
import { gql, useQuery } from '@apollo/client'; import { gql, useQuery } from '@apollo/client';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { SearchConfigType } from '../../../interfaces/search/interface'; 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` export const SEARCH_PEOPLE_QUERY = gql`
query SearchPeopleQuery($where: people_bool_exp, $limit: Int) { query SearchPeopleQuery($where: people_bool_exp, $limit: Int) {
@ -58,15 +61,16 @@ const debounce = <FuncArgs extends any[]>(
}; };
}; };
export type SearchResultsType<T extends AnyEntity = AnyEntity> = { export type SearchResultsType<T extends AnyEntity | UnknownType = UnknownType> =
results: { {
render: (value: T) => string; results: {
value: T; render: (value: T) => string;
}[]; value: T;
loading: boolean; }[];
}; loading: boolean;
};
export const useSearch = <T extends AnyEntity = AnyEntity>(): [ export const useSearch = <T extends AnyEntity | UnknownType = UnknownType>(): [
SearchResultsType<T>, SearchResultsType<T>,
React.Dispatch<React.SetStateAction<string>>, React.Dispatch<React.SetStateAction<string>>,
React.Dispatch<React.SetStateAction<SearchConfigType<T> | null>>, React.Dispatch<React.SetStateAction<SearchConfigType<T> | null>>,