Add Filters on Table views (#95)

* Add filter search logic

WIP Filter search

Implement filters

test: fix sorts tests

test: fix filter test

feature: search person and display firstname in results

feature: fix test for filter component

test: mock search filters

refactor: create a useSearch hook

refactor: move debounce in useSearch and reset status of filter selection

feature: debounce set filters

refactor: remove useless setSorts

feature: add where variable to people query

feature: strongly type Filters

feature: update WhereTemplate method

feature: implement filtering on full name

feature: type the useSearch hook

feature: use where reducer

refactor: create a type for readability

feature: use query and mapper from filters

feature: implement filter by company

feature: search filter results on filter select

feature: add loading and results to search results in filters

refactor: move render search results in a function

feature: display a LOADING when it loads

feature: split search input and search filter for different debounce

refactor: remove some warnings

refactor: remove some warnings

* Write test 1

* Write test 2

* test: useSearch is tested

* test: update names of default people data

* test: add a filter search

* Test 3

* Fix tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Sammy Teillet
2023-05-04 13:54:46 +02:00
committed by GitHub
parent 27d5edc031
commit 6a8a8f0728
33 changed files with 913 additions and 316 deletions

View File

@ -9,6 +9,7 @@ export const defaultData: Array<GraphqlQueryCompany> = [
id: '91510aa5-ede6-451f-8029-a7fa69e4bad6',
email: 'john@example.com',
displayName: 'John Doe',
__typename: 'User',
},
employees: 10,
address: '1 Infinity Loop, 95014 Cupertino, California',

View File

@ -96,7 +96,7 @@ export const companiesColumns = [
cell: (props) => (
<ClickableCell href="#">
{props.row.original.opportunities.map((opportunity) => (
<PipeChip name={opportunity.name} picture={opportunity.icon} />
<PipeChip opportunity={opportunity} />
))}
</ClickableCell>
),

View File

@ -12,9 +12,13 @@ import { useCallback, useState } from 'react';
import {
PeopleSelectedSortType,
defaultOrderBy,
reduceFiltersToWhere,
reduceSortsToOrderBy,
usePeopleQuery,
} from '../../services/people';
import { useSearch } from '../../services/search/search';
import { People_Bool_Exp } from '../../generated/graphql';
import { SelectedFilterType } from '../../components/table/table-header/interface';
const StyledPeopleContainer = styled.div`
display: flex;
@ -23,15 +27,22 @@ const StyledPeopleContainer = styled.div`
`;
function People() {
const [, setSorts] = useState([] as Array<PeopleSelectedSortType>);
const [orderBy, setOrderBy] = useState(defaultOrderBy);
const [where, setWhere] = useState<People_Bool_Exp>({});
const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch();
const updateSorts = useCallback((sorts: Array<PeopleSelectedSortType>) => {
setSorts(sorts);
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
}, []);
const { data } = usePeopleQuery(orderBy);
const updateFilters = useCallback(
(filters: Array<SelectedFilterType<People_Bool_Exp>>) => {
setWhere(reduceFiltersToWhere(filters));
},
[],
);
const { data } = usePeopleQuery(orderBy, where);
return (
<WithTopBarContainer title="People" icon={<FaRegUser />}>
@ -42,9 +53,15 @@ function People() {
columns={peopleColumns}
viewName="All People"
viewIcon={<FaList />}
onSortsUpdate={updateSorts}
availableSorts={availableSorts}
availableFilters={availableFilters}
filterSearchResults={filterSearchResults}
onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters}
onFilterSearch={(filter, searchValue) => {
setSearhInput(searchValue);
setFilterSearch(filter);
}}
/>
}
</StyledPeopleContainer>

View File

@ -5,6 +5,7 @@ import { lightTheme } from '../../../layout/styles/themes';
import { MockedProvider } from '@apollo/client/testing';
import { defaultData } from '../default-data';
import { GET_PEOPLE } from '../../../services/people';
import { SEARCH_PEOPLE_QUERY } from '../../../services/search/search';
const component = {
title: 'People',
@ -19,6 +20,7 @@ const mocks = [
query: GET_PEOPLE,
variables: {
orderBy: [{ created_at: 'desc' }],
where: {},
},
},
result: {
@ -27,6 +29,19 @@ const mocks = [
},
},
},
{
request: {
query: SEARCH_PEOPLE_QUERY, // TODO this should not be called for empty filters
variables: {
where: undefined,
},
},
result: {
data: {
people: [],
},
},
},
];
export const PeopleDefault = () => (

View File

@ -21,9 +21,9 @@ export const defaultData: Array<GraphqlQueryPerson> = [
{
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
__typename: 'Person',
firstname: 'Alexandre',
lastname: 'Prot',
email: 'alexandre@qonto.com',
firstname: 'John',
lastname: 'Doe',
email: 'john@linkedin.com',
company: {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6e',
name: 'LinkedIn',
@ -38,9 +38,9 @@ export const defaultData: Array<GraphqlQueryPerson> = [
{
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6f',
__typename: 'Person',
firstname: 'Alexandre',
lastname: 'Prot',
email: 'alexandre@qonto.com',
firstname: 'Jane',
lastname: 'Doe',
email: 'jane@sequoiacap.com',
company: {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6g',
name: 'Sequoia',
@ -56,9 +56,9 @@ export const defaultData: Array<GraphqlQueryPerson> = [
{
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6h',
__typename: 'Person',
firstname: 'Alexandre',
lastname: 'Prot',
email: 'alexandre@qonto.com',
firstname: 'Janice',
lastname: 'Dane',
email: 'janice@facebook.com',
company: {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6i',
name: 'Facebook',

View File

@ -6,6 +6,8 @@ import {
FaMapPin,
FaPhone,
FaStream,
FaUser,
FaBuilding,
} from 'react-icons/fa';
import { createColumnHelper } from '@tanstack/react-table';
import ClickableCell from '../../components/table/ClickableCell';
@ -15,7 +17,7 @@ import Checkbox from '../../components/form/Checkbox';
import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
import CompanyChip from '../../components/chips/CompanyChip';
import PersonChip from '../../components/chips/PersonChip';
import { Person } from '../../interfaces/person.interface';
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
import PipeChip from '../../components/chips/PipeChip';
import EditableCell from '../../components/table/EditableCell';
import { OrderByFields, updatePerson } from '../../services/people';
@ -23,6 +25,12 @@ import {
FilterType,
SortType,
} from '../../components/table/table-header/interface';
import { People_Bool_Exp } from '../../generated/graphql';
import {
SEARCH_COMPANY_QUERY,
SEARCH_PEOPLE_QUERY,
} from '../../services/search/search';
import { GraphqlQueryCompany } from '../../interfaces/company.interface';
export const availableSorts = [
{
@ -53,26 +61,74 @@ export const availableFilters = [
{
key: 'fullname',
label: 'People',
icon: <FaRegUser />,
icon: <FaUser />,
whereTemplate: (_operand, { firstname, lastname }) => ({
_and: [
{ firstname: { _ilike: `${firstname}` } },
{ lastname: { _ilike: `${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 },
}),
},
{
key: 'company_name',
label: 'Company',
icon: <FaRegBuilding />,
icon: <FaBuilding />,
whereTemplate: (_operand, { companyName }) => ({
company: { name: { _ilike: `%${companyName}%` } },
}),
searchQuery: SEARCH_COMPANY_QUERY,
searchTemplate: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` },
}),
searchResultMapper: (company: GraphqlQueryCompany) => ({
displayValue: company.name,
value: { companyName: company.name },
}),
},
{
key: 'email',
label: 'Email',
icon: <FaEnvelope />,
},
{ key: 'phone', label: 'Phone', icon: <FaPhone /> },
{
key: 'created_at',
label: 'Created at',
icon: <FaCalendar />,
},
{ key: 'city', label: 'City', icon: <FaMapPin /> },
] satisfies FilterType[];
// {
// key: 'email',
// label: 'Email',
// icon: faEnvelope,
// whereTemplate: () => ({ email: { _ilike: '%value%' } }),
// searchQuery: GET_PEOPLE,
// searchTemplate: { email: { _ilike: '%value%' } },
// },
// {
// 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%' } },
// },
// {
// key: 'city',
// label: 'City',
// icon: faMapPin,
// whereTemplate: () => ({ city: { _ilike: '%value%' } }),
// searchQuery: GET_PEOPLE,
// searchTemplate: { city: { _ilike: '%value%' } },
// },
] satisfies FilterType<People_Bool_Exp>[];
const columnHelper = createColumnHelper<Person>();
export const peopleColumns = [
@ -151,10 +207,7 @@ export const peopleColumns = [
header: () => <ColumnHead viewName="Pipe" viewIcon={<FaStream />} />,
cell: (props) => (
<ClickableCell href="#">
<PipeChip
name={props.row.original.pipe.name}
picture={props.row.original.pipe.icon}
/>
<PipeChip opportunity={props.row.original.pipe} />
</ClickableCell>
),
}),