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`
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<StyledEditModeContainerProps>`

View File

@ -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};
}

View File

@ -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) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
<th
key={header.id}
style={{
width: `${header.getSize()}px`,
}}
>
{header.isPlaceholder
? null
: flexRender(

View File

@ -91,25 +91,27 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
);
}
return filterSearchResults.results.map((result, index) => (
<DropdownButton.StyledDropdownItem
key={`fields-value-${index}`}
onClick={() => {
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)}
</DropdownButton.StyledDropdownItem>
));
return filterSearchResults.results.map((result, index) => {
return (
<DropdownButton.StyledDropdownItem
key={`fields-value-${index}`}
onClick={() => {
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)}
</DropdownButton.StyledDropdownItem>
);
});
};
function renderValueSelection(

View File

@ -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)};

View File

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

View File

@ -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<

View File

@ -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<SearchType>;
resultMapper: (data: GqlType<SearchType>) => {
value: SearchType;
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()}
/>
),
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: () => (

View File

@ -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<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 = {
key: 'company_domain_name',
label: 'Url',
@ -77,33 +107,6 @@ export const addressFilter = {
],
} 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 = {
key: 'company_created_at',
label: 'Created At',
@ -131,10 +134,45 @@ export const creationDateFilter = {
],
} 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 = [
nameFilter,
employeesFilter,
urlFilter,
addressFilter,
employeesFilter,
creationDateFilter,
accountOwnerFilter,
];

View File

@ -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: () => (

View File

@ -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<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 = {
key: 'company_name',
label: 'Company',
@ -72,29 +102,56 @@ export const companyFilter = {
],
} satisfies FilterConfigType<Person, Company>;
export const emailFilter = {
key: 'email',
label: 'Email',
icon: <TbMail size={16} />,
export const phoneFilter = {
key: 'phone',
label: 'Phone',
icon: <TbPhone size={16} />,
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<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 = {
key: 'city',
label: 'City',
@ -120,7 +177,9 @@ export const cityFilter = {
export const availableFilters = [
fullnameFilter,
companyFilter,
emailFilter,
companyFilter,
phoneFilter,
creationDateFilter,
cityFilter,
] satisfies FilterConfigType<Person>[];

View File

@ -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 = <FuncArgs extends any[]>(
};
};
export type SearchResultsType<T extends AnyEntity = AnyEntity> = {
results: {
render: (value: T) => string;
value: T;
}[];
loading: boolean;
};
export type SearchResultsType<T extends AnyEntity | UnknownType = UnknownType> =
{
results: {
render: (value: T) => string;
value: T;
}[];
loading: boolean;
};
export const useSearch = <T extends AnyEntity = AnyEntity>(): [
export const useSearch = <T extends AnyEntity | UnknownType = UnknownType>(): [
SearchResultsType<T>,
React.Dispatch<React.SetStateAction<string>>,
React.Dispatch<React.SetStateAction<SearchConfigType<T> | null>>,