Sammy/t 240 frontend filtering search is refactored (#122)

* refactor: use AnyEntity instead of any

* refactor: remove any and brand company type

* refactor: add typename for user and people

* bugfix: await company to be created before displaying it

* feature: await deletion before removing the lines

* refactor: remove default tyep for filters

* refactor: remove default type AnyEntity

* refactor: remove USers from filterable types

* refactor: do not depend on Filter types in Table

* Add tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Sammy Teillet
2023-05-17 21:49:34 +02:00
committed by GitHub
parent bc49815ff0
commit baca6150f5
25 changed files with 254 additions and 106 deletions

View File

@ -3,14 +3,15 @@ import DropdownButton from './DropdownButton';
import {
FilterConfigType,
FilterOperandType,
FilterableFieldsType,
SearchConfigType,
SearchableType,
SelectedFilterType,
} from './interface';
type OwnProps = {
type OwnProps<TData extends FilterableFieldsType> = {
isFilterSelected: boolean;
availableFilters: FilterConfigType[];
availableFilters: FilterConfigType<TData>[];
filterSearchResults?: {
results: {
render: (value: SearchableType) => string;
@ -18,30 +19,30 @@ type OwnProps = {
}[];
loading: boolean;
};
onFilterSelect: (filter: SelectedFilterType) => void;
onFilterSelect: (filter: SelectedFilterType<TData>) => void;
onFilterSearch: (
filter: SearchConfigType<any> | null,
searchValue: string,
) => void;
};
export function FilterDropdownButton({
export const FilterDropdownButton = <TData extends FilterableFieldsType>({
availableFilters,
filterSearchResults,
onFilterSearch,
onFilterSelect,
isFilterSelected,
}: OwnProps) {
}: OwnProps<TData>) => {
const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedFilter, setSelectedFilter] = useState<
FilterConfigType | undefined
FilterConfigType<TData> | undefined
>(undefined);
const [selectedFilterOperand, setSelectedFilterOperand] = useState<
FilterOperandType | undefined
FilterOperandType<TData> | undefined
>(undefined);
const resetState = useCallback(() => {
@ -66,9 +67,9 @@ export function FilterDropdownButton({
);
const renderSearchResults = (
filterSearchResults: NonNullable<OwnProps['filterSearchResults']>,
selectedFilter: FilterConfigType,
selectedFilterOperand: FilterOperandType,
filterSearchResults: NonNullable<OwnProps<TData>['filterSearchResults']>,
selectedFilter: FilterConfigType<TData>,
selectedFilterOperand: FilterOperandType<TData>,
) => {
if (filterSearchResults.loading) {
return (
@ -114,8 +115,8 @@ export function FilterDropdownButton({
));
function renderFilterDropdown(
selectedFilter: FilterConfigType,
selectedFilterOperand: FilterOperandType,
selectedFilter: FilterConfigType<TData>,
selectedFilterOperand: FilterOperandType<TData>,
) {
return (
<>
@ -161,4 +162,4 @@ export function FilterDropdownButton({
: renderSelectFilterITems}
</DropdownButton>
);
}
};

View File

@ -1,13 +1,17 @@
import styled from '@emotion/styled';
import SortOrFilterChip from './SortOrFilterChip';
import { FaArrowDown, FaArrowUp } from 'react-icons/fa';
import { SelectedFilterType, SelectedSortType } from './interface';
import {
FilterableFieldsType,
SelectedFilterType,
SelectedSortType,
} from './interface';
type OwnProps<SortField> = {
type OwnProps<SortField, TData extends FilterableFieldsType> = {
sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
filters: Array<SelectedFilterType>;
onRemoveFilter: (filterId: SelectedFilterType['key']) => void;
filters: Array<SelectedFilterType<TData>>;
onRemoveFilter: (filterId: SelectedFilterType<TData>['key']) => void;
onCancelClick: () => void;
};
@ -40,13 +44,13 @@ const StyledCancelButton = styled.button`
}
`;
function SortAndFilterBar<SortField>({
function SortAndFilterBar<SortField, TData extends FilterableFieldsType>({
sorts,
onRemoveSort,
filters,
onRemoveFilter,
onCancelClick,
}: OwnProps<SortField>) {
}: OwnProps<SortField, TData>) {
return (
<StyledBar>
{sorts.map((sort) => {

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import {
FilterConfigType,
FilterableFieldsType,
SearchConfigType,
SearchableType,
SelectedFilterType,
@ -12,11 +13,11 @@ import { SortDropdownButton } from './SortDropdownButton';
import { FilterDropdownButton } from './FilterDropdownButton';
import SortAndFilterBar from './SortAndFilterBar';
type OwnProps<SortField> = {
type OwnProps<SortField, TData extends FilterableFieldsType> = {
viewName: string;
viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>;
availableFilters?: FilterConfigType[];
availableFilters?: FilterConfigType<TData>[];
filterSearchResults?: {
results: {
render: (value: SearchableType) => string;
@ -25,7 +26,7 @@ type OwnProps<SortField> = {
loading: boolean;
};
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: (sorts: Array<SelectedFilterType>) => void;
onFiltersUpdate?: (sorts: Array<SelectedFilterType<TData>>) => void;
onFilterSearch?: (
filter: SearchConfigType<any> | null,
searchValue: string,
@ -68,7 +69,7 @@ const StyledFilters = styled.div`
margin-right: ${(props) => props.theme.spacing(2)};
`;
function TableHeader<SortField>({
function TableHeader<SortField, TData extends FilterableFieldsType>({
viewName,
viewIcon,
availableSorts,
@ -77,11 +78,13 @@ function TableHeader<SortField>({
onSortsUpdate,
onFiltersUpdate,
onFilterSearch,
}: OwnProps<SortField>) {
}: OwnProps<SortField, TData>) {
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
[],
);
const [filters, innerSetFilters] = useState<Array<SelectedFilterType>>([]);
const [filters, innerSetFilters] = useState<Array<SelectedFilterType<TData>>>(
[],
);
const sortSelect = useCallback(
(newSort: SelectedSortType<SortField>) => {
@ -102,7 +105,7 @@ function TableHeader<SortField>({
);
const filterSelect = useCallback(
(filter: SelectedFilterType) => {
(filter: SelectedFilterType<TData>) => {
const newFilters = updateSortOrFilterByKey(filters, filter);
innerSetFilters(newFilters);
@ -112,7 +115,7 @@ function TableHeader<SortField>({
);
const filterUnselect = useCallback(
(filterId: SelectedFilterType['key']) => {
(filterId: SelectedFilterType<TData>['key']) => {
const newFilters = filters.filter((filter) => filter.key !== filterId);
innerSetFilters(newFilters);
onFiltersUpdate && onFiltersUpdate(newFilters);

View File

@ -2,7 +2,7 @@ import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { FilterDropdownButton } from '../FilterDropdownButton';
import styled from '@emotion/styled';
import { FilterConfigType, SelectedFilterType } from '../interface';
import { FilterableFieldsType, SelectedFilterType } from '../interface';
import { useCallback, useState } from 'react';
import {
SEARCH_PEOPLE_QUERY,
@ -20,7 +20,7 @@ const component = {
export default component;
type OwnProps<FilterProperties> = {
type OwnProps<FilterProperties extends FilterableFieldsType> = {
setFilter: (filters: SelectedFilterType<FilterProperties>) => void;
};
@ -98,8 +98,8 @@ const InnerRegularFilterDropdownButton = ({
);
return (
<StyleDiv>
<FilterDropdownButton
availableFilters={availableFilters as FilterConfigType[]}
<FilterDropdownButton<Person>
availableFilters={availableFilters}
isFilterSelected={true}
onFilterSelect={outerSetFilters}
filterSearchResults={filterSearchResults}

View File

@ -3,6 +3,7 @@ import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { FaArrowDown } from 'react-icons/fa';
import { SelectedFilterType } from '../interface';
import { Person } from '../../../../interfaces/person.interface';
const component = {
title: 'SortAndFilterBar',
@ -20,6 +21,31 @@ export const RegularSortAndFilterBar = ({
removeFunction,
cancelFunction,
}: OwnProps) => {
const personFilter = {
label: 'People',
operand: {
label: 'Include',
id: 'include',
whereTemplate: (person: Person) => {
return { email: { _eq: person.email } };
},
},
key: 'test_filter',
icon: <FaArrowDown />,
displayValue: 'john@doedoe.com',
value: {
__typename: 'people',
id: 'test',
email: 'john@doedoe.com',
firstname: 'John',
lastname: 'Doe',
phone: '123456789',
company: null,
creationDate: new Date(),
pipes: null,
city: 'Paris',
},
} satisfies SelectedFilterType<Person, Person>;
return (
<ThemeProvider theme={lightTheme}>
<SortAndFilterBar
@ -42,32 +68,7 @@ export const RegularSortAndFilterBar = ({
onRemoveSort={removeFunction}
onRemoveFilter={removeFunction}
onCancelClick={cancelFunction}
filters={[
{
label: 'People',
operand: {
label: 'Include',
id: 'include',
whereTemplate: (person) => {
return { email: { _eq: person.email } };
},
},
key: 'test_filter',
icon: <FaArrowDown />,
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',
},
} satisfies SelectedFilterType,
]}
filters={[personFilter] as SelectedFilterType<Person>[]}
/>
</ThemeProvider>
);

View File

@ -1,7 +1,16 @@
import { Order_By } from '../../../generated/graphql';
import { BoolExpType, SelectedFilterType, SelectedSortType } from './interface';
import {
BoolExpType,
FilterWhereType,
FilterableFieldsType,
SelectedFilterType,
SelectedSortType,
} from './interface';
export const reduceFiltersToWhere = <ValueType, WhereTemplateType>(
export const reduceFiltersToWhere = <
ValueType extends FilterableFieldsType,
WhereTemplateType extends FilterWhereType,
>(
filters: Array<SelectedFilterType<ValueType, WhereTemplateType>>,
): BoolExpType<WhereTemplateType> => {
const where = filters.reduce((acc, filter) => {

View File

@ -35,8 +35,13 @@ export type SelectedSortType<OrderByTemplate> = SortType<OrderByTemplate> & {
order: 'asc' | 'desc';
};
type AnyEntity = {
id: string;
__typename: string;
} & Record<string, any>;
export type FilterableFieldsType = Person | Company;
export type FilterWhereType = Person | Company | User;
export type FilterWhereType = Person | Company | User | AnyEntity;
type FilterConfigGqlType<WhereType> = WhereType extends Company
? GraphqlQueryCompany
@ -50,9 +55,14 @@ export type BoolExpType<T> = T extends Company
? Companies_Bool_Exp
: T extends Person
? People_Bool_Exp
: T extends User
? Users_Bool_Exp
: never;
export type FilterConfigType<FilteredType = any, WhereType = any> = {
export type FilterConfigType<
FilteredType extends FilterableFieldsType,
WhereType extends FilterWhereType = any,
> = {
key: string;
label: string;
icon: ReactNode;
@ -77,17 +87,33 @@ export type SearchConfigType<SearchType extends SearchableType> = {
};
export type FilterOperandType<
FilteredType = FilterableFieldsType,
WhereType = any,
FilteredType extends FilterableFieldsType,
WhereType extends FilterWhereType = AnyEntity,
> =
| FilterOperandExactMatchType<FilteredType, WhereType>
| FilterOperandComparativeType<FilteredType, WhereType>;
type FilterOperandExactMatchType<
FilteredType extends FilterableFieldsType,
WhereType extends FilterWhereType,
> = {
label: string;
id: string;
label: 'Equal' | 'Not equal';
id: 'equal' | 'not-equal';
whereTemplate: (value: WhereType) => BoolExpType<FilteredType>;
};
type FilterOperandComparativeType<
FilteredType extends FilterableFieldsType,
WhereType extends FilterWhereType,
> = {
label: 'Like' | 'Not like' | 'Include';
id: 'like' | 'not_like' | 'include';
whereTemplate: (value: WhereType) => BoolExpType<FilteredType>;
};
export type SelectedFilterType<
FilteredType = FilterableFieldsType,
WhereType = any,
FilteredType extends FilterableFieldsType,
WhereType extends FilterWhereType = AnyEntity,
> = {
key: string;
value: WhereType;