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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
import { render, waitFor } from '@testing-library/react';
import { RegularApp } from '../__stories__/App.stories';
const assignMock = jest.fn();
delete window.location;
window.location = { assign: assignMock };
it('Checks the App component renders', async () => {
const { getByText } = render(<RegularApp />);
expect(getByText('Companies')).toBeDefined();
expect(getByText('Opportunities')).toBeDefined();
await waitFor(() => {
expect(getByText('Twenty')).toBeDefined();
});
});

View File

@ -1,9 +1,9 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { Opportunity } from '../../interfaces/company.interface';
type OwnProps = {
name: string;
picture?: string;
opportunity: Opportunity;
};
const StyledContainer = styled.span`
@ -20,11 +20,11 @@ const StyledContainer = styled.span`
}
`;
function PipeChip({ name, picture }: OwnProps) {
function PipeChip({ opportunity }: OwnProps) {
return (
<StyledContainer data-testid="company-chip">
{picture && <span>{picture}</span>}
<span>{name}</span>
<StyledContainer data-testid="company-chip" key={opportunity.id}>
{opportunity.icon && <span>{opportunity.icon}</span>}
<span>{opportunity.name}</span>
</StyledContainer>
);
}

View File

@ -10,18 +10,30 @@ import TableHeader from './table-header/TableHeader';
import styled from '@emotion/styled';
import {
FilterType,
SelectedFilterType,
SelectedSortType,
SortType,
} from './table-header/interface';
type OwnProps<TData, SortField> = {
type OwnProps<TData, SortField, FilterProperties> = {
data: Array<TData>;
columns: Array<ColumnDef<TData, any>>;
viewName: string;
viewIcon?: React.ReactNode;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
availableSorts?: Array<SortType<SortField>>;
availableFilters?: FilterType[];
availableFilters?: FilterType<FilterProperties>[];
filterSearchResults?: {
results: { displayValue: string; value: any }[];
loading: boolean;
};
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: (
sorts: Array<SelectedFilterType<FilterProperties>>,
) => void;
onFilterSearch?: (
filter: FilterType<FilterProperties> | null,
searchValue: string,
) => void;
};
const StyledTable = styled.table`
@ -75,15 +87,18 @@ const StyledTableScrollableContainer = styled.div`
flex: 1;
`;
function Table<TData, SortField extends string>({
function Table<TData, SortField extends string, FilterProperies>({
data,
columns,
viewName,
viewIcon,
onSortsUpdate,
availableSorts,
availableFilters,
}: OwnProps<TData, SortField>) {
filterSearchResults,
onSortsUpdate,
onFiltersUpdate,
onFilterSearch,
}: OwnProps<TData, SortField, FilterProperies>) {
const table = useReactTable({
data,
columns,
@ -95,9 +110,12 @@ function Table<TData, SortField extends string>({
<TableHeader
viewName={viewName}
viewIcon={viewIcon}
onSortsUpdate={onSortsUpdate}
availableSorts={availableSorts}
availableFilters={availableFilters}
filterSearchResults={filterSearchResults}
onSortsUpdate={onSortsUpdate}
onFiltersUpdate={onFiltersUpdate}
onFilterSearch={onFilterSearch}
/>
<StyledTableScrollableContainer>
<StyledTable>

View File

@ -1,24 +1,31 @@
import EditableCell from '../EditableCell';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../layout/styles/themes';
import { StoryFn } from '@storybook/react';
const component = {
title: 'EditableCell',
component: EditableCell,
};
export default component;
type OwnProps = {
changeHandler: () => void;
content: string;
changeHandler: (updated: string) => void;
};
export const RegularEditableCell = ({ changeHandler }: OwnProps) => {
export default component;
const Template: StoryFn<typeof EditableCell> = (args: OwnProps) => {
return (
<ThemeProvider theme={lightTheme}>
<div data-testid="content-editable-parent">
<EditableCell content={''} changeHandler={changeHandler} />,
<EditableCell {...args} />
</div>
</ThemeProvider>
);
};
export const EditableCellStory = Template.bind({});
EditableCellStory.args = {
content: 'Test string',
};

View File

@ -1,10 +1,12 @@
import { fireEvent, render } from '@testing-library/react';
import { RegularEditableCell } from '../__stories__/EditableCell.stories';
import { EditableCellStory } from '../__stories__/EditableCell.stories';
it('Checks the EditableCell editing event bubbles up', async () => {
const func = jest.fn(() => null);
const { getByTestId } = render(<RegularEditableCell changeHandler={func} />);
const { getByTestId } = render(
<EditableCellStory content="test" changeHandler={func} />,
);
const parent = getByTestId('content-editable-parent');
const editableInput = parent.querySelector('input');

View File

@ -1,47 +1,40 @@
import { useCallback, useState } from 'react';
import { ChangeEvent, useCallback, useState } from 'react';
import DropdownButton from './DropdownButton';
import { FilterType, SelectedFilterType } from './interface';
import { FilterOperandType, FilterType, SelectedFilterType } from './interface';
type OwnProps = {
filters: SelectedFilterType[];
setFilters: (sorts: SelectedFilterType[]) => void;
availableFilters: FilterType[];
type OwnProps<FilterProperties> = {
isFilterSelected: boolean;
availableFilters: FilterType<FilterProperties>[];
filterSearchResults?: {
results: { displayValue: string; value: any }[];
loading: boolean;
};
onFilterSelect: (filter: SelectedFilterType<FilterProperties>) => void;
onFilterSearch: (
filter: FilterType<FilterProperties> | null,
searchValue: string,
) => void;
};
type FilterOperandType = { label: string; id: string };
const filterOperands: FilterOperandType[] = [
{ label: 'Include', id: 'include' },
{ label: "Doesn't include", id: 'not-include' },
{ label: 'Include', id: 'include', keyWord: 'ilike' },
{ label: "Doesn't include", id: 'not-include', keyWord: 'not_ilike' },
];
const someFieldRandomValue = [
'John Doe',
'Jane Doe',
'John Smith',
'Jane Smith',
'John Johnson',
'Jane Johnson',
'John Williams',
'Jane Williams',
'John Brown',
'Jane Brown',
'John Jones',
'Jane Jones',
];
export function FilterDropdownButton({
export function FilterDropdownButton<FilterProperties>({
availableFilters,
setFilters,
filters,
}: OwnProps) {
filterSearchResults,
onFilterSearch,
onFilterSelect,
isFilterSelected,
}: OwnProps<FilterProperties>) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedFilter, setSelectedFilter] = useState<FilterType | undefined>(
undefined,
);
const [selectedFilter, setSelectedFilter] = useState<
FilterType<FilterProperties> | undefined
>(undefined);
const [selectedFilterOperand, setSelectedFilterOperand] =
useState<FilterOperandType>(filterOperands[0]);
@ -50,7 +43,8 @@ export function FilterDropdownButton({
setIsOptionUnfolded(false);
setSelectedFilter(undefined);
setSelectedFilterOperand(filterOperands[0]);
}, []);
onFilterSearch(null, '');
}, [onFilterSearch]);
const renderSelectOptionItems = filterOperands.map((filterOperand, index) => (
<DropdownButton.StyledDropdownItem
@ -64,11 +58,49 @@ export function FilterDropdownButton({
</DropdownButton.StyledDropdownItem>
));
const renderSearchResults = (
filterSearchResults: NonNullable<
OwnProps<FilterProperties>['filterSearchResults']
>,
selectedFilter: FilterType<FilterProperties>,
) => {
if (filterSearchResults.loading) {
return 'LOADING';
}
return filterSearchResults.results.map((value, index) => (
<DropdownButton.StyledDropdownItem
key={`fields-value-${index}`}
onClick={() => {
onFilterSelect({
key: value.displayValue,
operand: selectedFilterOperand,
searchQuery: selectedFilter.searchQuery,
searchTemplate: selectedFilter.searchTemplate,
whereTemplate: selectedFilter.whereTemplate,
label: selectedFilter.label,
value: value.displayValue,
icon: selectedFilter.icon,
where: selectedFilter.whereTemplate(
selectedFilterOperand,
value.value,
),
searchResultMapper: selectedFilter.searchResultMapper,
});
setIsUnfolded(false);
setSelectedFilter(undefined);
}}
>
{value.displayValue}
</DropdownButton.StyledDropdownItem>
));
};
const renderSelectFilterITems = availableFilters.map((filter, index) => (
<DropdownButton.StyledDropdownItem
key={`select-filter-${index}`}
onClick={() => {
setSelectedFilter(filter);
onFilterSearch(filter, '');
}}
>
<DropdownButton.StyledIcon>{filter.icon}</DropdownButton.StyledIcon>
@ -76,46 +108,36 @@ export function FilterDropdownButton({
</DropdownButton.StyledDropdownItem>
));
function renderFilterDropdown(selectedFilter: FilterType) {
return [
<DropdownButton.StyledDropdownTopOption
key={'selected-filter-operand'}
onClick={() => setIsOptionUnfolded(true)}
>
{selectedFilterOperand.label}
<DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption>,
<DropdownButton.StyledSearchField key={'search-filter'}>
<input type="text" placeholder={selectedFilter.label} />
</DropdownButton.StyledSearchField>,
someFieldRandomValue.map((value, index) => (
<DropdownButton.StyledDropdownItem
key={`fields-value-${index}`}
onClick={() => {
setFilters([
{
id: value,
operand: selectedFilterOperand,
label: selectedFilter.label,
value: value,
icon: selectedFilter.icon,
},
]);
setIsUnfolded(false);
setSelectedFilter(undefined);
}}
function renderFilterDropdown(selectedFilter: FilterType<FilterProperties>) {
return (
<>
<DropdownButton.StyledDropdownTopOption
key={'selected-filter-operand'}
onClick={() => setIsOptionUnfolded(true)}
>
{value}
</DropdownButton.StyledDropdownItem>
)),
];
{selectedFilterOperand.label}
<DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption>
<DropdownButton.StyledSearchField key={'search-filter'}>
<input
type="text"
placeholder={selectedFilter.label}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onFilterSearch(selectedFilter, event.target.value)
}
/>
</DropdownButton.StyledSearchField>
{filterSearchResults &&
renderSearchResults(filterSearchResults, selectedFilter)}
</>
);
}
return (
<DropdownButton
label="Filter"
isActive={filters.length > 0}
isActive={isFilterSelected}
isUnfolded={isUnfolded}
setIsUnfolded={setIsUnfolded}
resetState={resetState}

View File

@ -3,11 +3,13 @@ import SortOrFilterChip from './SortOrFilterChip';
import { FaArrowDown, FaArrowUp } from 'react-icons/fa';
import { SelectedFilterType, SelectedSortType } from './interface';
type OwnProps<SortField> = {
type OwnProps<SortField, FilterProperties> = {
sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
filters: Array<SelectedFilterType>;
onRemoveFilter: (filterId: SelectedFilterType['id']) => void;
filters: Array<SelectedFilterType<FilterProperties>>;
onRemoveFilter: (
filterId: SelectedFilterType<FilterProperties>['key'],
) => void;
};
const StyledBar = styled.div`
@ -39,12 +41,12 @@ const StyledCancelButton = styled.button`
}
`;
function SortAndFilterBar<SortField extends string>({
function SortAndFilterBar<SortField extends string, FilterProperties>({
sorts,
onRemoveSort,
filters,
onRemoveFilter,
}: OwnProps<SortField>) {
}: OwnProps<SortField, FilterProperties>) {
return (
<StyledBar>
{sorts.map((sort) => {
@ -61,12 +63,12 @@ function SortAndFilterBar<SortField extends string>({
{filters.map((filter) => {
return (
<SortOrFilterChip
key={filter.id}
key={filter.key}
labelKey={filter.label}
labelValue={`${filter.operand.label} ${filter.value}`}
id={filter.id}
id={filter.key}
icon={filter.icon}
onRemove={() => onRemoveFilter(filter.id)}
onRemove={() => onRemoveFilter(filter.key)}
/>
);
})}
@ -75,7 +77,7 @@ function SortAndFilterBar<SortField extends string>({
data-testid={'cancel-button'}
onClick={() => {
sorts.forEach((i) => onRemoveSort(i.key));
filters.forEach((i) => onRemoveFilter(i.id));
filters.forEach((i) => onRemoveFilter(i.key));
}}
>
Cancel

View File

@ -3,17 +3,17 @@ import DropdownButton from './DropdownButton';
import { SelectedSortType, SortType } from './interface';
type OwnProps<SortField> = {
sorts: SelectedSortType<SortField>[];
setSorts: (sorts: SelectedSortType<SortField>[]) => void;
isSortSelected: boolean;
onSortSelect: (sort: SelectedSortType<SortField>) => void;
availableSorts: SortType<SortField>[];
};
const options: Array<SelectedSortType<string>['order']> = ['asc', 'desc'];
export function SortDropdownButton<SortField extends string>({
isSortSelected,
availableSorts,
setSorts,
sorts,
onSortSelect,
}: OwnProps<SortField>) {
const [isUnfolded, setIsUnfolded] = useState(false);
@ -24,10 +24,9 @@ export function SortDropdownButton<SortField extends string>({
const onSortItemSelect = useCallback(
(sort: SortType<SortField>) => {
const newSorts = [{ ...sort, order: selectedSortDirection }];
setSorts(newSorts);
onSortSelect({ ...sort, order: selectedSortDirection });
},
[setSorts, selectedSortDirection],
[onSortSelect, selectedSortDirection],
);
const resetState = useCallback(() => {
@ -38,7 +37,7 @@ export function SortDropdownButton<SortField extends string>({
return (
<DropdownButton
label="Sort"
isActive={sorts.length > 0}
isActive={isSortSelected}
isUnfolded={isUnfolded}
setIsUnfolded={setIsUnfolded}
resetState={resetState}

View File

@ -11,13 +11,23 @@ import { SortDropdownButton } from './SortDropdownButton';
import { FilterDropdownButton } from './FilterDropdownButton';
import SortAndFilterBar from './SortAndFilterBar';
type OwnProps<SortField> = {
type OwnProps<SortField, FilterProperties> = {
viewName: string;
viewIcon?: ReactNode;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: (sorts: Array<SelectedFilterType>) => void;
availableSorts?: Array<SortType<SortField>>;
availableFilters?: FilterType[];
availableFilters?: FilterType<FilterProperties>[];
filterSearchResults?: {
results: { displayValue: string; value: any }[];
loading: boolean;
};
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: (
sorts: Array<SelectedFilterType<FilterProperties>>,
) => void;
onFilterSearch?: (
filter: FilterType<FilterProperties> | null,
searchValue: string,
) => void;
};
const StyledContainer = styled.div`
@ -56,27 +66,32 @@ const StyledFilters = styled.div`
margin-right: ${(props) => props.theme.spacing(2)};
`;
function TableHeader<SortField extends string>({
function TableHeader<SortField extends string, FilterProperties>({
viewName,
viewIcon,
onSortsUpdate,
onFiltersUpdate,
availableSorts,
availableFilters,
}: OwnProps<SortField>) {
filterSearchResults,
onSortsUpdate,
onFiltersUpdate,
onFilterSearch,
}: OwnProps<SortField, FilterProperties>) {
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
[],
);
const [filters, innerSetFilters] = useState<
Array<SelectedFilterType<FilterProperties>>
>([]);
const setSorts = useCallback(
(sorts: SelectedSortType<SortField>[]) => {
innerSetSorts(sorts);
onSortsUpdate && onSortsUpdate(sorts);
const sortSelect = useCallback(
(sort: SelectedSortType<SortField>) => {
innerSetSorts([sort]);
onSortsUpdate && onSortsUpdate([sort]);
},
[onSortsUpdate],
);
const onSortItemUnSelect = useCallback(
const sortUnselect = useCallback(
(sortId: string) => {
const newSorts = [] as SelectedSortType<SortField>[];
innerSetSorts(newSorts);
@ -85,25 +100,30 @@ function TableHeader<SortField extends string>({
[onSortsUpdate],
);
const [filters, innerSetFilters] = useState<Array<SelectedFilterType>>([]);
const setFilters = useCallback(
(filters: SelectedFilterType[]) => {
innerSetFilters(filters);
onFiltersUpdate && onFiltersUpdate(filters);
const filterSelect = useCallback(
(filter: SelectedFilterType<FilterProperties>) => {
innerSetFilters([filter]);
onFiltersUpdate && onFiltersUpdate([filter]);
},
[onFiltersUpdate],
);
const onFilterItemUnSelect = useCallback(
(filterId: SelectedFilterType['id']) => {
const newFilters = [] as SelectedFilterType[];
const filterUnselect = useCallback(
(filterId: SelectedFilterType<FilterProperties>['key']) => {
const newFilters = [] as SelectedFilterType<FilterProperties>[];
innerSetFilters(newFilters);
onFiltersUpdate && onFiltersUpdate(newFilters);
},
[onFiltersUpdate],
);
const filterSearch = useCallback(
(filter: FilterType<FilterProperties> | null, searchValue: string) => {
onFilterSearch && onFilterSearch(filter, searchValue);
},
[onFilterSearch],
);
return (
<StyledContainer>
<StyledTableHeader>
@ -113,14 +133,16 @@ function TableHeader<SortField extends string>({
</StyledViewSection>
<StyledFilters>
<FilterDropdownButton
filters={filters}
setFilters={setFilters}
isFilterSelected={filters.length > 0}
availableFilters={availableFilters || []}
filterSearchResults={filterSearchResults}
onFilterSelect={filterSelect}
onFilterSearch={filterSearch}
/>
<SortDropdownButton
setSorts={setSorts}
sorts={sorts}
<SortDropdownButton<SortField>
isSortSelected={sorts.length > 0}
availableSorts={availableSorts || []}
onSortSelect={sortSelect}
/>
<DropdownButton label="Settings" isActive={false}></DropdownButton>
@ -129,9 +151,9 @@ function TableHeader<SortField extends string>({
{sorts.length + filters.length > 0 && (
<SortAndFilterBar
sorts={sorts}
onRemoveSort={onSortItemUnSelect}
filters={filters}
onRemoveFilter={onFilterItemUnSelect}
onRemoveSort={sortUnselect}
onRemoveFilter={filterUnselect}
/>
)}
</StyledContainer>

View File

@ -3,15 +3,15 @@ import { lightTheme } from '../../../../layout/styles/themes';
import { FilterDropdownButton } from '../FilterDropdownButton';
import styled from '@emotion/styled';
import { FilterType, SelectedFilterType } from '../interface';
import {
FaRegUser,
FaRegBuilding,
FaEnvelope,
FaPhone,
FaCalendar,
FaMapPin,
} from 'react-icons/fa';
import { useCallback, useState } from 'react';
import { People_Bool_Exp } from '../../../../generated/graphql';
import { FaUsers } from 'react-icons/fa';
import {
SEARCH_PEOPLE_QUERY,
useSearch,
} from '../../../../services/search/search';
import { MockedProvider } from '@apollo/client/testing';
import { defaultData } from '../../../../pages/people/default-data';
const component = {
title: 'FilterDropdownButton',
@ -20,58 +20,129 @@ const component = {
export default component;
type OwnProps = {
setFilters: (filters: SelectedFilterType[]) => void;
type OwnProps<FilterProperties> = {
setFilter: (filters: SelectedFilterType<FilterProperties>) => void;
};
const mocks = [
{
request: {
query: SEARCH_PEOPLE_QUERY, // TODO this should not be called for empty filters
variables: {
where: undefined,
},
},
result: {
data: {
searchResults: defaultData,
},
},
},
{
request: {
query: SEARCH_PEOPLE_QUERY, // TODO this should not be called for empty filters
variables: {
where: {
_or: [
{ firstname: { _ilike: '%%' } },
{ lastname: { _ilike: '%%' } },
],
},
},
},
result: {
data: {
searchResults: defaultData,
},
},
},
{
request: {
query: SEARCH_PEOPLE_QUERY, // TODO this should not be called for empty filters
variables: {
where: {
_or: [
{ firstname: { _ilike: '%Jane%' } },
{ lastname: { _ilike: '%Jane%' } },
],
},
},
},
result: {
data: {
searchResults: [defaultData.find((p) => p.firstname === 'Jane')],
},
},
},
];
const availableFilters = [
{
key: 'fullname',
label: 'People',
icon: <FaRegUser />,
icon: <FaUsers />,
searchQuery: SEARCH_PEOPLE_QUERY,
searchTemplate: (searchInput: string) => ({
_or: [
{ firstname: { _ilike: `%${searchInput}%` } },
{ lastname: { _ilike: `%${searchInput}%` } },
],
}),
whereTemplate: () => ({
_or: [
{ firstname: { _ilike: 'value' } },
{ lastname: { _ilike: 'value' } },
],
}),
searchResultMapper: (data) => ({
displayValue: data.firstname + ' ' + data.lastname,
value: data.firstname,
}),
},
{
key: 'company_name',
label: 'Company',
icon: <FaRegBuilding />,
},
{
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[];
] satisfies FilterType<People_Bool_Exp>[];
const StyleDiv = styled.div`
height: 200px;
width: 200px;
`;
export const RegularFilterDropdownButton = ({ setFilters }: OwnProps) => {
const [filters, innerSetFilters] = useState<SelectedFilterType[]>([]);
const InnerRegularFilterDropdownButton = ({
setFilter: setFilters,
}: OwnProps<People_Bool_Exp>) => {
const [, innerSetFilters] = useState<SelectedFilterType<People_Bool_Exp>>();
const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch();
const outerSetFilters = useCallback(
(filters: SelectedFilterType[]) => {
innerSetFilters(filters);
setFilters(filters);
(filter: SelectedFilterType<People_Bool_Exp>) => {
innerSetFilters(filter);
setFilters(filter);
},
[setFilters],
);
return (
<ThemeProvider theme={lightTheme}>
<StyleDiv>
<FilterDropdownButton
availableFilters={availableFilters}
filters={filters}
setFilters={outerSetFilters}
/>
</StyleDiv>
</ThemeProvider>
<StyleDiv>
<FilterDropdownButton
availableFilters={availableFilters}
isFilterSelected={true}
onFilterSelect={outerSetFilters}
filterSearchResults={filterSearchResults}
onFilterSearch={(filter, searchValue) => {
setSearhInput(searchValue);
setFilterSearch(filter);
}}
/>
</StyleDiv>
);
};
export const RegularFilterDropdownButton = ({
setFilter: setFilters,
}: OwnProps<People_Bool_Exp>) => {
return (
<MockedProvider mocks={mocks}>
<ThemeProvider theme={lightTheme}>
<InnerRegularFilterDropdownButton setFilter={setFilters} />
</ThemeProvider>
</MockedProvider>
);
};

View File

@ -1,6 +1,7 @@
import SortAndFilterBar from '../SortAndFilterBar';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import { GET_PEOPLE } from '../../../../services/people';
import { FaArrowDown } from 'react-icons/fa';
const component = {
@ -37,10 +38,24 @@ export const RegularSortAndFilterBar = ({ removeFunction }: OwnProps) => {
filters={[
{
label: 'People',
operand: { id: 'include', label: 'Include' },
id: 'test_filter',
operand: { label: 'Include', id: 'include', keyWord: 'ilike' },
key: 'test_filter',
icon: <FaArrowDown />,
value: 'John Doe',
where: {
firstname: { _ilike: 'John Doe' },
},
searchQuery: GET_PEOPLE,
searchTemplate: () => ({
firstname: { _ilike: 'John Doe' },
}),
whereTemplate: () => {
return { firstname: { _ilike: 'John Doe' } };
},
searchResultMapper: (data) => ({
displayValue: 'John Doe',
value: data.firstname,
}),
},
]}
/>

View File

@ -1,4 +1,4 @@
import { SelectedSortType, SortType } from '../interface';
import { SortType } from '../interface';
import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes';
import {
@ -23,8 +23,6 @@ type OwnProps = {
setSorts: () => void;
};
const sorts = [] satisfies SelectedSortType[];
const availableSorts = [
{
key: 'fullname',
@ -60,9 +58,9 @@ export const RegularSortDropdownButton = ({ setSorts }: OwnProps) => {
<ThemeProvider theme={lightTheme}>
<StyleDiv>
<SortDropdownButton
sorts={sorts}
isSortSelected={true}
availableSorts={availableSorts}
setSorts={setSorts}
onSortSelect={setSorts}
/>
</StyleDiv>
</ThemeProvider>

View File

@ -1,44 +1,53 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { RegularFilterDropdownButton } from '../__stories__/FilterDropdownButton.stories';
import { FaEnvelope } from 'react-icons/fa';
import { FaUsers } from 'react-icons/fa';
it('Checks the default top option is Include', async () => {
const setSorts = jest.fn();
const setFilters = jest.fn();
const { getByText } = render(
<RegularFilterDropdownButton setFilters={setSorts} />,
<RegularFilterDropdownButton setFilter={setFilters} />,
);
const sortDropdownButton = getByText('Filter');
fireEvent.click(sortDropdownButton);
const sortByEmail = getByText('Email');
fireEvent.click(sortByEmail);
const filterByPeople = getByText('People');
fireEvent.click(filterByPeople);
const filterByJohn = getByText('John Doe');
await waitFor(() => {
const firstSearchResult = getByText('Alexandre Prot');
expect(firstSearchResult).toBeDefined();
});
const filterByJohn = getByText('Alexandre Prot');
fireEvent.click(filterByJohn);
expect(setSorts).toHaveBeenCalledWith([
{
id: 'John Doe',
value: 'John Doe',
label: 'Email',
operand: { id: 'include', label: 'Include' },
icon: <FaEnvelope />,
},
]);
expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({
key: 'Alexandre Prot',
value: 'Alexandre Prot',
label: 'People',
operand: {
id: 'include',
keyWord: 'ilike',
label: 'Include',
},
icon: <FaUsers />,
}),
);
});
it('Checks the selection of top option for Doesnot include', async () => {
const setSorts = jest.fn();
const setFilters = jest.fn();
const { getByText } = render(
<RegularFilterDropdownButton setFilters={setSorts} />,
<RegularFilterDropdownButton setFilter={setFilters} />,
);
const sortDropdownButton = getByText('Filter');
fireEvent.click(sortDropdownButton);
const sortByEmail = getByText('Email');
fireEvent.click(sortByEmail);
const filterByPeople = getByText('People');
fireEvent.click(filterByPeople);
const openOperandOptions = getByText('Include');
fireEvent.click(openOperandOptions);
@ -46,19 +55,80 @@ it('Checks the selection of top option for Doesnot include', async () => {
const selectOperand = getByText("Doesn't include");
fireEvent.click(selectOperand);
const filterByJohn = getByText('John Doe');
await waitFor(() => {
const firstSearchResult = getByText('Alexandre Prot');
expect(firstSearchResult).toBeDefined();
});
const filterByJohn = getByText('Alexandre Prot');
fireEvent.click(filterByJohn);
expect(setSorts).toHaveBeenCalledWith([
{
id: 'John Doe',
value: 'John Doe',
label: 'Email',
operand: { id: 'not-include', label: "Doesn't include" },
icon: <FaEnvelope />,
},
]);
expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({
key: 'Alexandre Prot',
value: 'Alexandre Prot',
label: 'People',
operand: {
id: 'not-include',
keyWord: 'not_ilike',
label: "Doesn't include",
},
icon: <FaUsers />,
}),
);
const blueSortDropdownButton = getByText('Filter');
await waitFor(() => {
expect(blueSortDropdownButton).toHaveAttribute('aria-selected', 'true');
});
});
it('Calls the filters when typing a new name', async () => {
const setFilters = jest.fn();
const { getByText, getByPlaceholderText, queryByText } = render(
<RegularFilterDropdownButton setFilter={setFilters} />,
);
const sortDropdownButton = getByText('Filter');
fireEvent.click(sortDropdownButton);
const filterByPeople = getByText('People');
fireEvent.click(filterByPeople);
const filterSearch = getByPlaceholderText('People');
fireEvent.click(filterSearch);
fireEvent.change(filterSearch, { target: { value: 'Jane' } });
await waitFor(() => {
const loadingDiv = getByText('LOADING');
expect(loadingDiv).toBeDefined();
});
await waitFor(() => {
const firstSearchResult = getByText('Jane Doe');
expect(firstSearchResult).toBeDefined();
const alexandreSearchResult = queryByText('Alexandre Prot');
expect(alexandreSearchResult).not.toBeInTheDocument();
});
const filterByJane = getByText('Jane Doe');
fireEvent.click(filterByJane);
expect(setFilters).toHaveBeenCalledWith(
expect.objectContaining({
key: 'Jane Doe',
value: 'Jane Doe',
label: 'People',
operand: {
id: 'include',
keyWord: 'ilike',
label: 'Include',
},
icon: <FaUsers />,
}),
);
const blueSortDropdownButton = getByText('Filter');
await waitFor(() => {
expect(blueSortDropdownButton).toHaveAttribute('aria-selected', 'true');

View File

@ -14,14 +14,12 @@ it('Checks the default top option is Ascending', async () => {
const sortByEmail = getByText('Email');
fireEvent.click(sortByEmail);
expect(setSorts).toHaveBeenCalledWith([
{
label: 'Email',
key: 'email',
icon: <FaEnvelope />,
order: 'asc',
},
]);
expect(setSorts).toHaveBeenCalledWith({
label: 'Email',
key: 'email',
icon: <FaEnvelope />,
order: 'asc',
});
});
it('Checks the selection of Descending', async () => {
@ -42,12 +40,10 @@ it('Checks the selection of Descending', async () => {
const sortByEmail = getByText('Email');
fireEvent.click(sortByEmail);
expect(setSorts).toHaveBeenCalledWith([
{
label: 'Email',
key: 'email',
icon: <FaEnvelope />,
order: 'desc',
},
]);
expect(setSorts).toHaveBeenCalledWith({
label: 'Email',
key: 'email',
icon: <FaEnvelope />,
order: 'desc',
});
});

View File

@ -1,4 +1,10 @@
import { DocumentNode } from 'graphql';
import { ReactNode } from 'react';
import {
Companies_Bool_Exp,
People_Bool_Exp,
Users_Bool_Exp,
} from '../../../generated/graphql';
export type SortType<SortKey = string> = {
label: string;
@ -6,20 +12,30 @@ export type SortType<SortKey = string> = {
icon?: ReactNode;
};
export type FilterType<FilterKey = string> = {
label: string;
key: FilterKey;
icon: ReactNode;
};
export type SelectedFilterType = {
id: string;
label: string;
value: string;
operand: { id: string; label: string };
icon: ReactNode;
};
export type SelectedSortType<SortField = string> = SortType<SortField> & {
order: 'asc' | 'desc';
};
export type FilterType<WhereTemplate, T = Record<string, string>> = {
label: string;
key: string;
icon: ReactNode;
whereTemplate: (operand: FilterOperandType, value: T) => WhereTemplate;
searchQuery: DocumentNode;
searchTemplate: (
searchInput: string,
) => People_Bool_Exp | Companies_Bool_Exp | Users_Bool_Exp;
searchResultMapper: (data: any) => { displayValue: string; value: T };
};
export type FilterOperandType = {
label: string;
id: string;
keyWord: 'ilike' | 'not_ilike';
};
export type SelectedFilterType<WhereTemplate> = FilterType<WhereTemplate> & {
value: string;
operand: FilterOperandType;
where: WhereTemplate;
};

View File

@ -13,6 +13,7 @@ describe('mapCompany', () => {
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
email: 'john@example.com',
displayName: 'John Doe',
__typename: 'User',
},
employees: 10,
address: '1 Infinite Loop, 95014 Cupertino, California, USA',

View File

@ -1,6 +1,7 @@
import { User } from './user.interface';
import { GraphqlQueryUser, User } from './user.interface';
export interface Opportunity {
id: string;
name: string;
icon: string;
}
@ -16,17 +17,11 @@ export interface Company {
creationDate: Date;
}
export type GraphqlQueryAccountOwner = {
id: string;
email: string;
displayName: string;
};
export type GraphqlQueryCompany = {
id: string;
name: string;
domain_name: string;
account_owner?: GraphqlQueryAccountOwner;
account_owner?: GraphqlQueryUser;
employees: number;
address: string;
created_at: string;
@ -54,7 +49,7 @@ export const mapCompany = (company: GraphqlQueryCompany): Company => ({
}
: undefined,
creationDate: new Date(company.created_at),
opportunities: [{ name: 'Sales Pipeline', icon: '' }],
opportunities: [],
});
export const mapGqlCompany = (company: Company): GraphqlMutationCompany => ({

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,39 @@
import { GraphqlQueryAccountOwner } from './company.interface';
import { Workspace } from './workspace.interface';
import {
GraphqlQueryWorkspaceMember,
WorkspaceMember,
} from './workspace.interface';
export type GraphqlQueryUser = {
id: string;
email: string;
displayName: string;
workspace_member?: GraphqlQueryWorkspaceMember;
__typename: string;
};
export interface User {
id: string;
email: string;
displayName: string;
workspace_member?: {
workspace: Workspace;
};
workspace_member?: WorkspaceMember;
}
export const mapUser = (user: GraphqlQueryAccountOwner): User => ({
...user,
});
export const mapUser = (user: GraphqlQueryUser): User => {
const mappedUser = {
id: user.id,
email: user.email,
displayName: user.displayName,
} as User;
if (user.workspace_member) {
mappedUser['workspace_member'] = {
workspace: {
id: user.workspace_member.workspace.id,
displayName: user.workspace_member.workspace.display_name,
domainName: user.workspace_member.workspace.domain_name,
logo: user.workspace_member.workspace.logo,
},
};
}
return mappedUser;
};

View File

@ -1,5 +1,22 @@
export interface Workspace {
id: string;
display_name: string;
domainName: string;
displayName: string;
logo: string;
}
export interface WorkspaceMember {
workspace: Workspace;
}
export type GraphqlQueryWorkspace = {
id: string;
display_name: string;
domain_name: string;
logo: string;
__typename: string;
};
export type GraphqlQueryWorkspaceMember = {
workspace: GraphqlQueryWorkspace;
__typename: string;
};

View File

@ -43,7 +43,7 @@ function WorkspaceContainer({ workspace }: OwnProps) {
return (
<StyledContainer>
<StyledLogo logo={workspace.logo}></StyledLogo>
<StyledName>{workspace?.display_name}</StyledName>
<StyledName>{workspace?.displayName}</StyledName>
</StyledContainer>
);
}

File diff suppressed because one or more lines are too long

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>
),
}),

View File

@ -1,7 +1,14 @@
import { QueryResult, gql, useQuery } from '@apollo/client';
import { GraphqlQueryPerson } from '../../interfaces/person.interface';
import { Order_By, People_Order_By } from '../../generated/graphql';
import { SelectedSortType } from '../../components/table/table-header/interface';
import {
Order_By,
People_Bool_Exp,
People_Order_By,
} from '../../generated/graphql';
import {
SelectedFilterType,
SelectedSortType,
} from '../../components/table/table-header/interface';
export type OrderByFields = keyof People_Order_By | 'fullname' | 'company_name';
@ -11,6 +18,16 @@ const mapOrder = (order: 'asc' | 'desc'): Order_By => {
return order === 'asc' ? Order_By.Asc : Order_By.Desc;
};
export const reduceFiltersToWhere = <T>(
filters: Array<SelectedFilterType<T>>,
): T => {
const where = filters.reduce((acc, filter) => {
const { where } = filter;
return { ...acc, ...where };
}, {} as T);
return where;
};
export const reduceSortsToOrderBy = (
sorts: Array<PeopleSelectedSortType>,
): People_Order_By[] => {
@ -31,8 +48,12 @@ export const reduceSortsToOrderBy = (
};
export const GET_PEOPLE = gql`
query GetPeople($orderBy: [people_order_by!]) {
people(order_by: $orderBy) {
query GetPeople(
$orderBy: [people_order_by!]
$where: people_bool_exp
$limit: Int
) {
people(order_by: $orderBy, where: $where, limit: $limit) {
id
phone
email
@ -51,9 +72,10 @@ export const GET_PEOPLE = gql`
export function usePeopleQuery(
orderBy: People_Order_By[],
where: People_Bool_Exp,
): QueryResult<{ people: GraphqlQueryPerson[] }> {
return useQuery<{ people: GraphqlQueryPerson[] }>(GET_PEOPLE, {
variables: { orderBy },
variables: { orderBy, where },
});
}

View File

@ -0,0 +1,105 @@
import { gql, useQuery } from '@apollo/client';
import { People_Bool_Exp } from '../../generated/graphql';
import {} from '../../interfaces/company.interface';
import { useMemo, useState } from 'react';
import { FilterType } from '../../components/table/table-header/interface';
export const SEARCH_PEOPLE_QUERY = gql`
query SearchQuery($where: people_bool_exp, $limit: Int) {
searchResults: people(where: $where, limit: $limit) {
id
phone
email
city
firstname
lastname
created_at
}
}
`;
const EMPTY_QUERY = gql`
query EmptyQuery {
_
}
`;
export const SEARCH_COMPANY_QUERY = gql`
query SearchQuery($where: companies_bool_exp, $limit: Int) {
searchResults: companies(where: $where, limit: $limit) {
id
name
}
}
`;
const debounce = <FuncArgs extends any[]>(
func: (...args: FuncArgs) => void,
delay: number,
) => {
let timeoutId: ReturnType<typeof setTimeout>;
return (...args: FuncArgs) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
};
export const useSearch = (): [
{ results: { displayValue: string; value: any }[]; loading: boolean },
React.Dispatch<React.SetStateAction<string>>,
React.Dispatch<React.SetStateAction<FilterType<People_Bool_Exp> | null>>,
] => {
const [filter, setFilter] = useState<FilterType<People_Bool_Exp> | null>(
null,
);
const [searchInput, setSearchInput] = useState<string>('');
const debouncedsetSearchInput = useMemo(
() => debounce(setSearchInput, 500),
[],
);
const where = useMemo(() => {
return (
filter && filter.searchTemplate && filter.searchTemplate(searchInput)
);
}, [filter, searchInput]);
const searchFilterQueryResults = useQuery(
filter?.searchQuery || EMPTY_QUERY,
{
variables: {
where,
},
skip: !filter,
},
);
const searchFilterResults = useMemo<{
results: { displayValue: string; value: any }[];
loading: boolean;
}>(() => {
if (filter == null) {
return {
loading: false,
results: [],
};
}
if (searchFilterQueryResults.loading) {
return {
loading: true,
results: [],
};
}
return {
loading: false,
results: searchFilterQueryResults.data.searchResults.map(
filter.searchResultMapper,
),
};
}, [filter, searchFilterQueryResults]);
return [searchFilterResults, debouncedsetSearchInput, setFilter];
};

View File

@ -1,11 +0,0 @@
describe('Get Current user', () => {
it('should return a parsed user if api returns it', () => {
// TBD
});
it('should not return a user if api does not return it', () => {
// TBD
});
});
export {};

View File

@ -1,5 +1,5 @@
import { QueryResult, gql, useQuery } from '@apollo/client';
import { GraphqlQueryAccountOwner } from '../../interfaces/company.interface';
import { GraphqlQueryUser } from '../../interfaces/user.interface';
export const GET_CURRENT_USER = gql`
query GetCurrentUser {
@ -20,7 +20,7 @@ export const GET_CURRENT_USER = gql`
`;
export function useGetCurrentUserQuery(): QueryResult<{
users: GraphqlQueryAccountOwner[];
users: GraphqlQueryUser[];
}> {
return useQuery<{ users: GraphqlQueryAccountOwner[] }>(GET_CURRENT_USER);
return useQuery<{ users: GraphqlQueryUser[] }>(GET_CURRENT_USER);
}