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:
@ -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',
|
||||
|
||||
@ -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>
|
||||
),
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = () => (
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user