Refactor Filters and Search (#119)
This commit is contained in:
@ -67,7 +67,7 @@
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 70,
|
||||
"functions": 80,
|
||||
"functions": 75,
|
||||
"lines": 80,
|
||||
"statements": 80
|
||||
}
|
||||
|
||||
@ -9,7 +9,9 @@ import {
|
||||
import TableHeader from './table-header/TableHeader';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
FilterType,
|
||||
FilterConfigType,
|
||||
SearchConfigType,
|
||||
SearchableType,
|
||||
SelectedFilterType,
|
||||
SelectedSortType,
|
||||
SortType,
|
||||
@ -21,23 +23,24 @@ declare module 'react' {
|
||||
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
|
||||
}
|
||||
|
||||
type OwnProps<TData, SortField, FilterProperties> = {
|
||||
type OwnProps<TData, SortField> = {
|
||||
data: Array<TData>;
|
||||
columns: Array<ColumnDef<TData, any>>;
|
||||
viewName: string;
|
||||
viewIcon?: React.ReactNode;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
availableFilters?: FilterType<FilterProperties>[];
|
||||
availableFilters?: FilterConfigType<TData>[];
|
||||
filterSearchResults?: {
|
||||
results: { displayValue: string; value: any }[];
|
||||
results: {
|
||||
render: (value: SearchableType) => string;
|
||||
value: SearchableType;
|
||||
}[];
|
||||
loading: boolean;
|
||||
};
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
onFiltersUpdate?: (
|
||||
sorts: Array<SelectedFilterType<FilterProperties>>,
|
||||
) => void;
|
||||
onFiltersUpdate?: (sorts: Array<SelectedFilterType>) => void;
|
||||
onFilterSearch?: (
|
||||
filter: FilterType<FilterProperties> | null,
|
||||
filter: SearchConfigType<any> | null,
|
||||
searchValue: string,
|
||||
) => void;
|
||||
onRowSelectionChange?: (rowSelection: string[]) => void;
|
||||
@ -94,7 +97,7 @@ const StyledTableScrollableContainer = styled.div`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Table = <TData extends { id: string }, SortField, FilterProperies>(
|
||||
const Table = <TData extends { id: string }, SortField>(
|
||||
{
|
||||
data,
|
||||
columns,
|
||||
@ -107,7 +110,7 @@ const Table = <TData extends { id: string }, SortField, FilterProperies>(
|
||||
onFiltersUpdate,
|
||||
onFilterSearch,
|
||||
onRowSelectionChange,
|
||||
}: OwnProps<TData, SortField, FilterProperies>,
|
||||
}: OwnProps<TData, SortField>,
|
||||
ref: React.ForwardedRef<{ resetRowSelection: () => void } | undefined>,
|
||||
) => {
|
||||
const [internalRowSelection, setInternalRowSelection] = React.useState({});
|
||||
@ -144,7 +147,7 @@ const Table = <TData extends { id: string }, SortField, FilterProperies>(
|
||||
viewName={viewName}
|
||||
viewIcon={viewIcon}
|
||||
availableSorts={availableSorts}
|
||||
availableFilters={availableFilters}
|
||||
availableFilters={availableFilters as FilterConfigType<any>[]}
|
||||
filterSearchResults={filterSearchResults}
|
||||
onSortsUpdate={onSortsUpdate}
|
||||
onFiltersUpdate={onFiltersUpdate}
|
||||
|
||||
@ -2,8 +2,7 @@ import { ChangeEvent, ComponentType, useState } from 'react';
|
||||
import EditableCellWrapper from './EditableCellWrapper';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSearch } from '../../../services/search/search';
|
||||
import { FilterType } from '../table-header/interface';
|
||||
import { People_Bool_Exp } from '../../../generated/graphql';
|
||||
import { SearchConfigType, SearchableType } from '../table-header/interface';
|
||||
|
||||
const StyledEditModeContainer = styled.div`
|
||||
width: 200px;
|
||||
@ -48,10 +47,13 @@ const StyledEditModeResultItem = styled.div`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export type EditableRelationProps<RelationType, ChipComponentPropsType> = {
|
||||
export type EditableRelationProps<
|
||||
RelationType extends SearchableType,
|
||||
ChipComponentPropsType,
|
||||
> = {
|
||||
relation?: RelationType | null;
|
||||
searchPlaceholder: string;
|
||||
searchFilter: FilterType<People_Bool_Exp>;
|
||||
searchConfig: SearchConfigType<RelationType>;
|
||||
changeHandler: (relation: RelationType) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
ChipComponent: ComponentType<ChipComponentPropsType>;
|
||||
@ -60,10 +62,13 @@ export type EditableRelationProps<RelationType, ChipComponentPropsType> = {
|
||||
) => ChipComponentPropsType & JSX.IntrinsicAttributes;
|
||||
};
|
||||
|
||||
function EditableRelation<RelationType, ChipComponentPropsType>({
|
||||
function EditableRelation<
|
||||
RelationType extends SearchableType,
|
||||
ChipComponentPropsType,
|
||||
>({
|
||||
relation,
|
||||
searchPlaceholder,
|
||||
searchFilter,
|
||||
searchConfig,
|
||||
changeHandler,
|
||||
editModeHorizontalAlign,
|
||||
ChipComponent,
|
||||
@ -72,7 +77,8 @@ function EditableRelation<RelationType, ChipComponentPropsType>({
|
||||
const [selectedRelation, setSelectedRelation] = useState(relation);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch();
|
||||
const [filterSearchResults, setSearchInput, setFilterSearch] =
|
||||
useSearch<RelationType>();
|
||||
|
||||
return (
|
||||
<EditableCellWrapper
|
||||
@ -97,16 +103,16 @@ function EditableRelation<RelationType, ChipComponentPropsType>({
|
||||
<StyledEditModeSearchInput
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterSearch(searchFilter);
|
||||
setFilterSearch(searchConfig);
|
||||
setSearchInput(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</StyledEditModeSearchContainer>
|
||||
<StyledEditModeResults>
|
||||
{filterSearchResults.results &&
|
||||
filterSearchResults.results.map((result) => (
|
||||
filterSearchResults.results.map((result, index) => (
|
||||
<StyledEditModeResultItem
|
||||
key={result.value.id}
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setSelectedRelation(result.value);
|
||||
changeHandler(result.value);
|
||||
|
||||
@ -3,16 +3,11 @@ import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
import CompanyChip, { CompanyChipPropsType } from '../../../chips/CompanyChip';
|
||||
import {
|
||||
GraphqlQueryCompany,
|
||||
PartialCompany,
|
||||
} from '../../../../interfaces/company.interface';
|
||||
import { Company, mapCompany } from '../../../../interfaces/company.interface';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { SEARCH_COMPANY_QUERY } from '../../../../services/search/search';
|
||||
import styled from '@emotion/styled';
|
||||
import { People_Bool_Exp } from '../../../../generated/graphql';
|
||||
import { FilterType } from '../../table-header/interface';
|
||||
import { FaBuilding } from 'react-icons/fa';
|
||||
import { SearchConfigType } from '../../table-header/interface';
|
||||
|
||||
const component = {
|
||||
title: 'editable-cell/EditableRelation',
|
||||
@ -58,13 +53,13 @@ const mocks = [
|
||||
];
|
||||
|
||||
const Template: StoryFn<
|
||||
typeof EditableRelation<PartialCompany, CompanyChipPropsType>
|
||||
> = (args: EditableRelationProps<PartialCompany, CompanyChipPropsType>) => {
|
||||
typeof EditableRelation<Company, CompanyChipPropsType>
|
||||
> = (args: EditableRelationProps<Company, CompanyChipPropsType>) => {
|
||||
return (
|
||||
<MockedProvider mocks={mocks}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<StyledParent data-testid="content-editable-parent">
|
||||
<EditableRelation<PartialCompany, CompanyChipPropsType> {...args} />
|
||||
<EditableRelation<Company, CompanyChipPropsType> {...args} />
|
||||
</StyledParent>
|
||||
</ThemeProvider>
|
||||
</MockedProvider>
|
||||
@ -77,36 +72,25 @@ EditableRelationStory.args = {
|
||||
id: '123',
|
||||
name: 'Heroku',
|
||||
domain_name: 'heroku.com',
|
||||
} as PartialCompany,
|
||||
} as Company,
|
||||
ChipComponent: CompanyChip,
|
||||
chipComponentPropsMapper: (company: PartialCompany): CompanyChipPropsType => {
|
||||
chipComponentPropsMapper: (company: Company): CompanyChipPropsType => {
|
||||
return {
|
||||
name: company.name,
|
||||
picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`,
|
||||
};
|
||||
},
|
||||
changeHandler: (relation: PartialCompany) => {
|
||||
changeHandler: (relation: Company) => {
|
||||
console.log('changed', relation);
|
||||
},
|
||||
searchFilter: {
|
||||
key: 'company_name',
|
||||
label: 'Company',
|
||||
icon: <FaBuilding />,
|
||||
whereTemplate: () => {
|
||||
return {};
|
||||
},
|
||||
searchQuery: SEARCH_COMPANY_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
searchConfig: {
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||
displayValue: company.name,
|
||||
value: {
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
domain_name: company.domain_name,
|
||||
},
|
||||
resultMapper: (company) => ({
|
||||
render: (company) => company.name,
|
||||
value: mapCompany(company),
|
||||
}),
|
||||
operands: [],
|
||||
} satisfies FilterType<People_Bool_Exp>,
|
||||
} satisfies SearchConfigType<Company>,
|
||||
};
|
||||
|
||||
@ -2,17 +2,17 @@ import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { EditableRelationStory } from '../__stories__/EditableRelation.stories';
|
||||
import { CompanyChipPropsType } from '../../../chips/CompanyChip';
|
||||
import { PartialCompany } from '../../../../interfaces/company.interface';
|
||||
|
||||
import { EditableRelationProps } from '../EditableRelation';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Company } from '../../../../interfaces/company.interface';
|
||||
|
||||
it('Checks the EditableRelation editing event bubbles up', async () => {
|
||||
const func = jest.fn(() => null);
|
||||
const { getByTestId, getByText } = render(
|
||||
<EditableRelationStory
|
||||
{...(EditableRelationStory.args as EditableRelationProps<
|
||||
PartialCompany,
|
||||
Company,
|
||||
CompanyChipPropsType
|
||||
>)}
|
||||
changeHandler={func}
|
||||
@ -49,10 +49,16 @@ it('Checks the EditableRelation editing event bubbles up', async () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(func).toBeCalledWith({
|
||||
domain_name: 'abnb.com',
|
||||
id: 'abnb',
|
||||
name: 'Airbnb',
|
||||
});
|
||||
expect(func).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
accountOwner: null,
|
||||
address: undefined,
|
||||
domain_name: 'abnb.com',
|
||||
employees: undefined,
|
||||
id: 'abnb',
|
||||
name: 'Airbnb',
|
||||
opportunities: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,34 +1,43 @@
|
||||
import { ChangeEvent, useCallback, useState } from 'react';
|
||||
import DropdownButton from './DropdownButton';
|
||||
import { FilterOperandType, FilterType, SelectedFilterType } from './interface';
|
||||
import {
|
||||
FilterConfigType,
|
||||
FilterOperandType,
|
||||
SearchConfigType,
|
||||
SearchableType,
|
||||
SelectedFilterType,
|
||||
} from './interface';
|
||||
|
||||
type OwnProps<FilterProperties> = {
|
||||
type OwnProps = {
|
||||
isFilterSelected: boolean;
|
||||
availableFilters: FilterType<FilterProperties>[];
|
||||
availableFilters: FilterConfigType[];
|
||||
filterSearchResults?: {
|
||||
results: { displayValue: string; value: any }[];
|
||||
results: {
|
||||
render: (value: SearchableType) => string;
|
||||
value: SearchableType;
|
||||
}[];
|
||||
loading: boolean;
|
||||
};
|
||||
onFilterSelect: (filter: SelectedFilterType<FilterProperties>) => void;
|
||||
onFilterSelect: (filter: SelectedFilterType) => void;
|
||||
onFilterSearch: (
|
||||
filter: FilterType<FilterProperties> | null,
|
||||
filter: SearchConfigType<any> | null,
|
||||
searchValue: string,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function FilterDropdownButton<FilterProperties>({
|
||||
export function FilterDropdownButton({
|
||||
availableFilters,
|
||||
filterSearchResults,
|
||||
onFilterSearch,
|
||||
onFilterSelect,
|
||||
isFilterSelected,
|
||||
}: OwnProps<FilterProperties>) {
|
||||
}: OwnProps) {
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
|
||||
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
|
||||
|
||||
const [selectedFilter, setSelectedFilter] = useState<
|
||||
FilterType<FilterProperties> | undefined
|
||||
FilterConfigType | undefined
|
||||
>(undefined);
|
||||
|
||||
const [selectedFilterOperand, setSelectedFilterOperand] = useState<
|
||||
@ -57,10 +66,8 @@ export function FilterDropdownButton<FilterProperties>({
|
||||
);
|
||||
|
||||
const renderSearchResults = (
|
||||
filterSearchResults: NonNullable<
|
||||
OwnProps<FilterProperties>['filterSearchResults']
|
||||
>,
|
||||
selectedFilter: FilterType<FilterProperties>,
|
||||
filterSearchResults: NonNullable<OwnProps['filterSearchResults']>,
|
||||
selectedFilter: FilterConfigType,
|
||||
selectedFilterOperand: FilterOperandType,
|
||||
) => {
|
||||
if (filterSearchResults.loading) {
|
||||
@ -70,32 +77,24 @@ export function FilterDropdownButton<FilterProperties>({
|
||||
</DropdownButton.StyledDropdownItem>
|
||||
);
|
||||
}
|
||||
return filterSearchResults.results.map((value, index) => (
|
||||
|
||||
return filterSearchResults.results.map((result, index) => (
|
||||
<DropdownButton.StyledDropdownItem
|
||||
key={`fields-value-${index}`}
|
||||
onClick={() => {
|
||||
onFilterSelect({
|
||||
...selectedFilter,
|
||||
key: value.displayValue,
|
||||
operand: selectedFilterOperand,
|
||||
searchQuery: selectedFilter.searchQuery,
|
||||
searchTemplate: selectedFilter.searchTemplate,
|
||||
whereTemplate: selectedFilter.whereTemplate,
|
||||
key: selectedFilter.key,
|
||||
label: selectedFilter.label,
|
||||
value: value.displayValue,
|
||||
value: result.value,
|
||||
displayValue: result.render(result.value),
|
||||
icon: selectedFilter.icon,
|
||||
where:
|
||||
selectedFilter.whereTemplate(
|
||||
selectedFilterOperand,
|
||||
value.value,
|
||||
) || ({} as FilterProperties),
|
||||
searchResultMapper: selectedFilter.searchResultMapper,
|
||||
operand: selectedFilterOperand,
|
||||
});
|
||||
setIsUnfolded(false);
|
||||
setSelectedFilter(undefined);
|
||||
}}
|
||||
>
|
||||
{value.displayValue}
|
||||
{result.render(result.value)}
|
||||
</DropdownButton.StyledDropdownItem>
|
||||
));
|
||||
};
|
||||
@ -106,7 +105,7 @@ export function FilterDropdownButton<FilterProperties>({
|
||||
onClick={() => {
|
||||
setSelectedFilter(filter);
|
||||
setSelectedFilterOperand(filter.operands[0]);
|
||||
onFilterSearch(filter, '');
|
||||
onFilterSearch(filter.searchConfig, '');
|
||||
}}
|
||||
>
|
||||
<DropdownButton.StyledIcon>{filter.icon}</DropdownButton.StyledIcon>
|
||||
@ -115,7 +114,7 @@ export function FilterDropdownButton<FilterProperties>({
|
||||
));
|
||||
|
||||
function renderFilterDropdown(
|
||||
selectedFilter: FilterType<FilterProperties>,
|
||||
selectedFilter: FilterConfigType,
|
||||
selectedFilterOperand: FilterOperandType,
|
||||
) {
|
||||
return (
|
||||
@ -133,7 +132,7 @@ export function FilterDropdownButton<FilterProperties>({
|
||||
type="text"
|
||||
placeholder={selectedFilter.label}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
onFilterSearch(selectedFilter, event.target.value)
|
||||
onFilterSearch(selectedFilter.searchConfig, event.target.value)
|
||||
}
|
||||
/>
|
||||
</DropdownButton.StyledSearchField>
|
||||
|
||||
@ -3,13 +3,11 @@ import SortOrFilterChip from './SortOrFilterChip';
|
||||
import { FaArrowDown, FaArrowUp } from 'react-icons/fa';
|
||||
import { SelectedFilterType, SelectedSortType } from './interface';
|
||||
|
||||
type OwnProps<SortField, FilterProperties> = {
|
||||
type OwnProps<SortField> = {
|
||||
sorts: Array<SelectedSortType<SortField>>;
|
||||
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
|
||||
filters: Array<SelectedFilterType<FilterProperties>>;
|
||||
onRemoveFilter: (
|
||||
filterId: SelectedFilterType<FilterProperties>['key'],
|
||||
) => void;
|
||||
filters: Array<SelectedFilterType>;
|
||||
onRemoveFilter: (filterId: SelectedFilterType['key']) => void;
|
||||
onCancelClick: () => void;
|
||||
};
|
||||
|
||||
@ -42,13 +40,13 @@ const StyledCancelButton = styled.button`
|
||||
}
|
||||
`;
|
||||
|
||||
function SortAndFilterBar<SortField, FilterProperties>({
|
||||
function SortAndFilterBar<SortField>({
|
||||
sorts,
|
||||
onRemoveSort,
|
||||
filters,
|
||||
onRemoveFilter,
|
||||
onCancelClick,
|
||||
}: OwnProps<SortField, FilterProperties>) {
|
||||
}: OwnProps<SortField>) {
|
||||
return (
|
||||
<StyledBar>
|
||||
{sorts.map((sort) => {
|
||||
@ -67,7 +65,7 @@ function SortAndFilterBar<SortField, FilterProperties>({
|
||||
<SortOrFilterChip
|
||||
key={filter.key}
|
||||
labelKey={filter.label}
|
||||
labelValue={`${filter.operand.label} ${filter.value}`}
|
||||
labelValue={`${filter.operand.label} ${filter.displayValue}`}
|
||||
id={filter.key}
|
||||
icon={filter.icon}
|
||||
onRemove={() => onRemoveFilter(filter.key)}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
FilterType,
|
||||
FilterConfigType,
|
||||
SearchConfigType,
|
||||
SearchableType,
|
||||
SelectedFilterType,
|
||||
SelectedSortType,
|
||||
SortType,
|
||||
@ -10,21 +12,22 @@ import { SortDropdownButton } from './SortDropdownButton';
|
||||
import { FilterDropdownButton } from './FilterDropdownButton';
|
||||
import SortAndFilterBar from './SortAndFilterBar';
|
||||
|
||||
type OwnProps<SortField, FilterProperties> = {
|
||||
type OwnProps<SortField> = {
|
||||
viewName: string;
|
||||
viewIcon?: ReactNode;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
availableFilters?: FilterType<FilterProperties>[];
|
||||
availableFilters?: FilterConfigType[];
|
||||
filterSearchResults?: {
|
||||
results: { displayValue: string; value: any }[];
|
||||
results: {
|
||||
render: (value: SearchableType) => string;
|
||||
value: SearchableType;
|
||||
}[];
|
||||
loading: boolean;
|
||||
};
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
onFiltersUpdate?: (
|
||||
sorts: Array<SelectedFilterType<FilterProperties>>,
|
||||
) => void;
|
||||
onFiltersUpdate?: (sorts: Array<SelectedFilterType>) => void;
|
||||
onFilterSearch?: (
|
||||
filter: FilterType<FilterProperties> | null,
|
||||
filter: SearchConfigType<any> | null,
|
||||
searchValue: string,
|
||||
) => void;
|
||||
};
|
||||
@ -65,7 +68,7 @@ const StyledFilters = styled.div`
|
||||
margin-right: ${(props) => props.theme.spacing(2)};
|
||||
`;
|
||||
|
||||
function TableHeader<SortField, FilterProperties>({
|
||||
function TableHeader<SortField>({
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
@ -74,13 +77,11 @@ function TableHeader<SortField, FilterProperties>({
|
||||
onSortsUpdate,
|
||||
onFiltersUpdate,
|
||||
onFilterSearch,
|
||||
}: OwnProps<SortField, FilterProperties>) {
|
||||
}: OwnProps<SortField>) {
|
||||
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
|
||||
[],
|
||||
);
|
||||
const [filters, innerSetFilters] = useState<
|
||||
Array<SelectedFilterType<FilterProperties>>
|
||||
>([]);
|
||||
const [filters, innerSetFilters] = useState<Array<SelectedFilterType>>([]);
|
||||
|
||||
const sortSelect = useCallback(
|
||||
(newSort: SelectedSortType<SortField>) => {
|
||||
@ -101,7 +102,7 @@ function TableHeader<SortField, FilterProperties>({
|
||||
);
|
||||
|
||||
const filterSelect = useCallback(
|
||||
(filter: SelectedFilterType<FilterProperties>) => {
|
||||
(filter: SelectedFilterType) => {
|
||||
const newFilters = updateSortOrFilterByKey(filters, filter);
|
||||
|
||||
innerSetFilters(newFilters);
|
||||
@ -111,7 +112,7 @@ function TableHeader<SortField, FilterProperties>({
|
||||
);
|
||||
|
||||
const filterUnselect = useCallback(
|
||||
(filterId: SelectedFilterType<FilterProperties>['key']) => {
|
||||
(filterId: SelectedFilterType['key']) => {
|
||||
const newFilters = filters.filter((filter) => filter.key !== filterId);
|
||||
innerSetFilters(newFilters);
|
||||
onFiltersUpdate && onFiltersUpdate(newFilters);
|
||||
@ -120,7 +121,7 @@ function TableHeader<SortField, FilterProperties>({
|
||||
);
|
||||
|
||||
const filterSearch = useCallback(
|
||||
(filter: FilterType<FilterProperties> | null, searchValue: string) => {
|
||||
(filter: SearchConfigType<any> | null, searchValue: string) => {
|
||||
onFilterSearch && onFilterSearch(filter, searchValue);
|
||||
},
|
||||
[onFilterSearch],
|
||||
|
||||
@ -2,16 +2,16 @@ import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../../layout/styles/themes';
|
||||
import { FilterDropdownButton } from '../FilterDropdownButton';
|
||||
import styled from '@emotion/styled';
|
||||
import { FilterType, SelectedFilterType } from '../interface';
|
||||
import { FilterConfigType, SelectedFilterType } from '../interface';
|
||||
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 { mockData } from '../../../../pages/people/__tests__/__data__/mock-data';
|
||||
import { availableFilters } from '../../../../pages/people/people-table';
|
||||
import { Person } from '../../../../interfaces/person.interface';
|
||||
|
||||
const component = {
|
||||
title: 'FilterDropdownButton',
|
||||
@ -78,35 +78,6 @@ const mocks = [
|
||||
},
|
||||
];
|
||||
|
||||
const availableFilters = [
|
||||
{
|
||||
key: 'fullname',
|
||||
label: 'People',
|
||||
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,
|
||||
}),
|
||||
operands: [
|
||||
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
|
||||
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
|
||||
],
|
||||
},
|
||||
] satisfies FilterType<People_Bool_Exp>[];
|
||||
|
||||
const StyleDiv = styled.div`
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
@ -114,12 +85,12 @@ const StyleDiv = styled.div`
|
||||
|
||||
const InnerRegularFilterDropdownButton = ({
|
||||
setFilter: setFilters,
|
||||
}: OwnProps<People_Bool_Exp>) => {
|
||||
const [, innerSetFilters] = useState<SelectedFilterType<People_Bool_Exp>>();
|
||||
}: OwnProps<Person>) => {
|
||||
const [, innerSetFilters] = useState<SelectedFilterType<Person>>();
|
||||
const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch();
|
||||
|
||||
const outerSetFilters = useCallback(
|
||||
(filter: SelectedFilterType<People_Bool_Exp>) => {
|
||||
(filter: SelectedFilterType<Person>) => {
|
||||
innerSetFilters(filter);
|
||||
setFilters(filter);
|
||||
},
|
||||
@ -128,7 +99,7 @@ const InnerRegularFilterDropdownButton = ({
|
||||
return (
|
||||
<StyleDiv>
|
||||
<FilterDropdownButton
|
||||
availableFilters={availableFilters}
|
||||
availableFilters={availableFilters as FilterConfigType[]}
|
||||
isFilterSelected={true}
|
||||
onFilterSelect={outerSetFilters}
|
||||
filterSearchResults={filterSearchResults}
|
||||
@ -143,7 +114,7 @@ const InnerRegularFilterDropdownButton = ({
|
||||
|
||||
export const RegularFilterDropdownButton = ({
|
||||
setFilter: setFilters,
|
||||
}: OwnProps<People_Bool_Exp>) => {
|
||||
}: OwnProps<Person>) => {
|
||||
return (
|
||||
<MockedProvider mocks={mocks}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
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';
|
||||
import { SelectedFilterType } from '../interface';
|
||||
|
||||
const component = {
|
||||
title: 'SortAndFilterBar',
|
||||
@ -45,26 +45,28 @@ export const RegularSortAndFilterBar = ({
|
||||
filters={[
|
||||
{
|
||||
label: 'People',
|
||||
operand: { label: 'Include', id: 'include', keyWord: 'ilike' },
|
||||
operand: {
|
||||
label: 'Include',
|
||||
id: 'include',
|
||||
whereTemplate: (person) => {
|
||||
return { email: { _eq: person.email } };
|
||||
},
|
||||
},
|
||||
key: 'test_filter',
|
||||
icon: <FaArrowDown />,
|
||||
value: 'John Doe',
|
||||
where: {
|
||||
firstname: { _ilike: 'John Doe' },
|
||||
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',
|
||||
},
|
||||
searchQuery: GET_PEOPLE,
|
||||
searchTemplate: () => ({
|
||||
firstname: { _ilike: 'John Doe' },
|
||||
}),
|
||||
whereTemplate: () => {
|
||||
return { firstname: { _ilike: 'John Doe' } };
|
||||
},
|
||||
searchResultMapper: (data) => ({
|
||||
displayValue: 'John Doe',
|
||||
value: data.firstname,
|
||||
}),
|
||||
operands: [],
|
||||
},
|
||||
} satisfies SelectedFilterType,
|
||||
]}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { RegularFilterDropdownButton } from '../__stories__/FilterDropdownButton.stories';
|
||||
import { FaUsers } from 'react-icons/fa';
|
||||
|
||||
it('Checks the default top option is Include', async () => {
|
||||
const setFilters = jest.fn();
|
||||
@ -24,15 +23,9 @@ it('Checks the default top option is Include', async () => {
|
||||
|
||||
expect(setFilters).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
key: 'Alexandre Prot',
|
||||
value: 'Alexandre Prot',
|
||||
displayValue: 'Alexandre Prot',
|
||||
key: 'fullname',
|
||||
label: 'People',
|
||||
operand: {
|
||||
id: 'equal',
|
||||
keyWord: 'equal',
|
||||
label: 'Equal',
|
||||
},
|
||||
icon: <FaUsers />,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -65,15 +58,9 @@ it('Checks the selection of top option for Not Equal', async () => {
|
||||
|
||||
expect(setFilters).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
key: 'Alexandre Prot',
|
||||
value: 'Alexandre Prot',
|
||||
key: 'fullname',
|
||||
displayValue: 'Alexandre Prot',
|
||||
label: 'People',
|
||||
operand: {
|
||||
id: 'not-equal',
|
||||
keyWord: 'not_equal',
|
||||
label: 'Not equal',
|
||||
},
|
||||
icon: <FaUsers />,
|
||||
}),
|
||||
);
|
||||
const blueSortDropdownButton = getByText('Filter');
|
||||
@ -118,15 +105,9 @@ it('Calls the filters when typing a new name', async () => {
|
||||
|
||||
expect(setFilters).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
key: 'Jane Doe',
|
||||
value: 'Jane Doe',
|
||||
key: 'fullname',
|
||||
displayValue: 'Jane Doe',
|
||||
label: 'People',
|
||||
operand: {
|
||||
id: 'equal',
|
||||
keyWord: 'equal',
|
||||
label: 'Equal',
|
||||
},
|
||||
icon: <FaUsers />,
|
||||
}),
|
||||
);
|
||||
const blueSortDropdownButton = getByText('Filter');
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { Order_By } from '../../../generated/graphql';
|
||||
import { SelectedFilterType, SelectedSortType } from './interface';
|
||||
import { BoolExpType, SelectedFilterType, SelectedSortType } from './interface';
|
||||
|
||||
export const reduceFiltersToWhere = <T>(
|
||||
filters: Array<SelectedFilterType<T>>,
|
||||
): T => {
|
||||
export const reduceFiltersToWhere = <ValueType, WhereTemplateType>(
|
||||
filters: Array<SelectedFilterType<ValueType, WhereTemplateType>>,
|
||||
): BoolExpType<WhereTemplateType> => {
|
||||
const where = filters.reduce((acc, filter) => {
|
||||
const { where } = filter;
|
||||
return { ...acc, ...where };
|
||||
}, {} as T);
|
||||
return { ...acc, ...filter.operand.whereTemplate(filter.value) };
|
||||
}, {} as BoolExpType<WhereTemplateType>);
|
||||
return where;
|
||||
};
|
||||
|
||||
|
||||
@ -6,6 +6,15 @@ import {
|
||||
People_Bool_Exp,
|
||||
Users_Bool_Exp,
|
||||
} from '../../../generated/graphql';
|
||||
import {
|
||||
Company,
|
||||
GraphqlQueryCompany,
|
||||
} from '../../../interfaces/company.interface';
|
||||
import {
|
||||
GraphqlQueryPerson,
|
||||
Person,
|
||||
} from '../../../interfaces/person.interface';
|
||||
import { GraphqlQueryUser, User } from '../../../interfaces/user.interface';
|
||||
|
||||
export type SortType<OrderByTemplate> =
|
||||
| {
|
||||
@ -26,33 +35,64 @@ export type SelectedSortType<OrderByTemplate> = SortType<OrderByTemplate> & {
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export type FilterType<WhereTemplate, FilterValue = Record<string, any>> = {
|
||||
operands: FilterOperandType[];
|
||||
label: string;
|
||||
export type FilterableFieldsType = Person | Company;
|
||||
export type FilterWhereType = Person | Company | User;
|
||||
|
||||
type FilterConfigGqlType<WhereType> = WhereType extends Company
|
||||
? GraphqlQueryCompany
|
||||
: WhereType extends Person
|
||||
? GraphqlQueryPerson
|
||||
: WhereType extends User
|
||||
? GraphqlQueryUser
|
||||
: never;
|
||||
|
||||
export type BoolExpType<T> = T extends Company
|
||||
? Companies_Bool_Exp
|
||||
: T extends Person
|
||||
? People_Bool_Exp
|
||||
: never;
|
||||
|
||||
export type FilterConfigType<FilteredType = any, WhereType = any> = {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
whereTemplate: (
|
||||
operand: FilterOperandType,
|
||||
value: FilterValue,
|
||||
) => WhereTemplate | undefined;
|
||||
searchQuery: DocumentNode;
|
||||
searchTemplate: (
|
||||
operands: FilterOperandType<FilteredType, WhereType>[];
|
||||
searchConfig: WhereType extends SearchableType
|
||||
? SearchConfigType<WhereType>
|
||||
: null;
|
||||
selectedValueRender: (selected: WhereType) => string;
|
||||
};
|
||||
|
||||
export type SearchableType = Person | Company | User;
|
||||
|
||||
export type SearchConfigType<SearchType extends SearchableType> = {
|
||||
query: DocumentNode;
|
||||
template: (
|
||||
searchInput: string,
|
||||
) => People_Bool_Exp | Companies_Bool_Exp | Users_Bool_Exp;
|
||||
searchResultMapper: (data: any) => {
|
||||
displayValue: string;
|
||||
value: FilterValue;
|
||||
resultMapper: (data: FilterConfigGqlType<SearchType>) => {
|
||||
value: SearchType;
|
||||
render: (value: SearchType) => ReactNode;
|
||||
};
|
||||
};
|
||||
|
||||
export type FilterOperandType = {
|
||||
export type FilterOperandType<
|
||||
FilteredType = FilterableFieldsType,
|
||||
WhereType = any,
|
||||
> = {
|
||||
label: string;
|
||||
id: string;
|
||||
keyWord: 'ilike' | 'not_ilike' | 'equal' | 'not_equal';
|
||||
whereTemplate: (value: WhereType) => BoolExpType<FilteredType>;
|
||||
};
|
||||
|
||||
export type SelectedFilterType<WhereTemplate> = FilterType<WhereTemplate> & {
|
||||
value: string;
|
||||
operand: FilterOperandType;
|
||||
where: WhereTemplate;
|
||||
export type SelectedFilterType<
|
||||
FilteredType = FilterableFieldsType,
|
||||
WhereType = any,
|
||||
> = {
|
||||
key: string;
|
||||
value: WhereType;
|
||||
displayValue: string;
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
operand: FilterOperandType<FilteredType, WhereType>;
|
||||
};
|
||||
|
||||
@ -17,9 +17,6 @@ export type Company = {
|
||||
creationDate: Date;
|
||||
};
|
||||
|
||||
export type PartialCompany = Partial<Company> &
|
||||
Pick<Company, 'id' | 'name' | 'domain_name'>;
|
||||
|
||||
export type GraphqlQueryCompany = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@ -15,6 +15,10 @@ describe('mapPerson', () => {
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
name: '',
|
||||
domain_name: '',
|
||||
employees: 0,
|
||||
address: '',
|
||||
created_at: '',
|
||||
account_owner: null,
|
||||
},
|
||||
__typename: '',
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PartialCompany } from './company.interface';
|
||||
import { Company, GraphqlQueryCompany, mapCompany } from './company.interface';
|
||||
import { Pipe } from './pipe.interface';
|
||||
|
||||
export type Person = {
|
||||
@ -7,7 +7,7 @@ export type Person = {
|
||||
lastname: string;
|
||||
picture?: string;
|
||||
email: string;
|
||||
company: PartialCompany | null;
|
||||
company: Company | null;
|
||||
phone: string;
|
||||
creationDate: Date;
|
||||
pipe: Pipe | null;
|
||||
@ -16,12 +16,7 @@ export type Person = {
|
||||
|
||||
export type GraphqlQueryPerson = {
|
||||
city: string;
|
||||
company: {
|
||||
__typename: string;
|
||||
id: string;
|
||||
name: string;
|
||||
domain_name: string;
|
||||
};
|
||||
company: GraphqlQueryCompany | null;
|
||||
created_at: string;
|
||||
email: string;
|
||||
firstname: string;
|
||||
@ -56,13 +51,7 @@ export const mapPerson = (person: GraphqlQueryPerson): Person => ({
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
icon: '💰',
|
||||
},
|
||||
company: person.company
|
||||
? {
|
||||
id: person.company.id,
|
||||
name: person.company.name,
|
||||
domain_name: person.company.domain_name,
|
||||
}
|
||||
: null,
|
||||
company: person.company ? mapCompany(person.company) : null,
|
||||
});
|
||||
|
||||
export const mapGqlPerson = (person: Person): GraphqlMutationPerson => ({
|
||||
|
||||
@ -25,7 +25,10 @@ import {
|
||||
Companies_Bool_Exp,
|
||||
Companies_Order_By,
|
||||
} from '../../generated/graphql';
|
||||
import { SelectedFilterType } from '../../components/table/table-header/interface';
|
||||
import {
|
||||
FilterConfigType,
|
||||
SelectedFilterType,
|
||||
} from '../../components/table/table-header/interface';
|
||||
import { useSearch } from '../../services/search/search';
|
||||
import ActionBar from '../../components/table/action-bar/ActionBar';
|
||||
|
||||
@ -47,7 +50,7 @@ function Companies() {
|
||||
}, []);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
(filters: Array<SelectedFilterType<Companies_Bool_Exp>>) => {
|
||||
(filters: Array<SelectedFilterType<Company>>) => {
|
||||
setWhere(reduceFiltersToWhere(filters));
|
||||
},
|
||||
[],
|
||||
@ -108,7 +111,7 @@ function Companies() {
|
||||
viewName="All Companies"
|
||||
viewIcon={<FaList />}
|
||||
availableSorts={availableSorts}
|
||||
availableFilters={availableFilters}
|
||||
availableFilters={availableFilters as Array<FilterConfigType>}
|
||||
filterSearchResults={filterSearchResults}
|
||||
onSortsUpdate={updateSorts}
|
||||
onFiltersUpdate={updateFilters}
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CompaniesFilter should render the filter company_name 1`] = `
|
||||
Object {
|
||||
"name": Object {
|
||||
"_eq": "Airbnb",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CompaniesFilter should render the filter company_name 2`] = `
|
||||
Object {
|
||||
"_not": Object {
|
||||
"name": Object {
|
||||
"_eq": "Airbnb",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CompaniesFilter should render the filter domainName 1`] = `
|
||||
Object {
|
||||
"domain_name": Object {
|
||||
"_eq": "airbnb.com",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CompaniesFilter should render the filter domainName 2`] = `
|
||||
Object {
|
||||
"_not": Object {
|
||||
"domain_name": Object {
|
||||
"_eq": "airbnb.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CompaniesFilter should render the serch company_name with the searchValue 1`] = `
|
||||
Object {
|
||||
"name": Object {
|
||||
"_ilike": "%Search value%",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CompaniesFilter should render the serch domainName with the searchValue 1`] = `
|
||||
Object {
|
||||
"domain_name": Object {
|
||||
"_ilike": "%Search value%",
|
||||
},
|
||||
}
|
||||
`;
|
||||
@ -1,62 +0,0 @@
|
||||
import { FilterType } from '../../../components/table/table-header/interface';
|
||||
import { Companies_Bool_Exp } from '../../../generated/graphql';
|
||||
import { GraphqlQueryCompany } from '../../../interfaces/company.interface';
|
||||
import { GraphqlQueryPerson } from '../../../interfaces/person.interface';
|
||||
import {
|
||||
SEARCH_COMPANY_QUERY,
|
||||
SEARCH_PEOPLE_QUERY,
|
||||
} from '../../../services/search/search';
|
||||
import { mockData } from './__data__/mock-data';
|
||||
import { availableFilters } from '../companies-table';
|
||||
|
||||
function assertFilterUseCompanySearch<FilterValue>(
|
||||
filter: FilterType<Companies_Bool_Exp>,
|
||||
): filter is FilterType<Companies_Bool_Exp> & {
|
||||
searchResultMapper: (data: GraphqlQueryCompany) => {
|
||||
displayValue: string;
|
||||
value: FilterValue;
|
||||
};
|
||||
} {
|
||||
return filter.searchQuery === SEARCH_COMPANY_QUERY;
|
||||
}
|
||||
|
||||
function assertFilterUsePeopleSearch<FilterValue>(
|
||||
filter: FilterType<Companies_Bool_Exp>,
|
||||
): filter is FilterType<Companies_Bool_Exp> & {
|
||||
searchResultMapper: (data: GraphqlQueryPerson) => {
|
||||
displayValue: string;
|
||||
value: FilterValue;
|
||||
};
|
||||
} {
|
||||
return filter.searchQuery === SEARCH_PEOPLE_QUERY;
|
||||
}
|
||||
|
||||
const AirbnbCompany = mockData.find(
|
||||
(user) => user.name === 'Airbnb',
|
||||
) as GraphqlQueryCompany;
|
||||
|
||||
describe('CompaniesFilter', () => {
|
||||
for (const filter of availableFilters) {
|
||||
it(`should render the filter ${filter.key}`, () => {
|
||||
if (assertFilterUseCompanySearch(filter)) {
|
||||
const filterSelectedValue = filter.searchResultMapper(mockData[0]);
|
||||
for (const operand of filter.operands) {
|
||||
expect(
|
||||
filter.whereTemplate(operand, filterSelectedValue.value),
|
||||
).toMatchSnapshot();
|
||||
}
|
||||
}
|
||||
if (assertFilterUsePeopleSearch(filter)) {
|
||||
const filterSelectedValue = filter.searchResultMapper(AirbnbCompany);
|
||||
for (const operand of filter.operands) {
|
||||
expect(
|
||||
filter.whereTemplate(operand, filterSelectedValue.value),
|
||||
).toMatchSnapshot();
|
||||
}
|
||||
}
|
||||
});
|
||||
it(`should render the serch ${filter.key} with the searchValue`, () => {
|
||||
expect(filter.searchTemplate('Search value')).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -1,8 +1,5 @@
|
||||
import { CellContext, createColumnHelper } from '@tanstack/react-table';
|
||||
import {
|
||||
Company,
|
||||
GraphqlQueryCompany,
|
||||
} from '../../interfaces/company.interface';
|
||||
import { Company, mapCompany } from '../../interfaces/company.interface';
|
||||
import { updateCompany } from '../../services/companies';
|
||||
import ColumnHead from '../../components/table/ColumnHead';
|
||||
import CompanyChip from '../../components/chips/CompanyChip';
|
||||
@ -15,28 +12,24 @@ import {
|
||||
FaRegUser,
|
||||
FaUsers,
|
||||
FaBuilding,
|
||||
FaUser,
|
||||
} from 'react-icons/fa';
|
||||
import PersonChip, {
|
||||
PersonChipPropsType,
|
||||
} from '../../components/chips/PersonChip';
|
||||
import EditableChip from '../../components/table/editable-cell/EditableChip';
|
||||
import {
|
||||
FilterType,
|
||||
FilterConfigType,
|
||||
SearchConfigType,
|
||||
SortType,
|
||||
} from '../../components/table/table-header/interface';
|
||||
import {
|
||||
Companies_Bool_Exp,
|
||||
Companies_Order_By,
|
||||
Users_Bool_Exp,
|
||||
} from '../../generated/graphql';
|
||||
import { Companies_Order_By } from '../../generated/graphql';
|
||||
import {
|
||||
SEARCH_COMPANY_QUERY,
|
||||
SEARCH_USER_QUERY,
|
||||
} from '../../services/search/search';
|
||||
import EditableDate from '../../components/table/editable-cell/EditableDate';
|
||||
import EditableRelation from '../../components/table/editable-cell/EditableRelation';
|
||||
import { GraphqlQueryUser, PartialUser } from '../../interfaces/user.interface';
|
||||
import { User, mapUser } from '../../interfaces/user.interface';
|
||||
import { useMemo } from 'react';
|
||||
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
|
||||
import Checkbox from '../../components/form/Checkbox';
|
||||
@ -79,63 +72,67 @@ export const availableFilters = [
|
||||
key: 'company_name',
|
||||
label: 'Company',
|
||||
icon: <FaBuilding />,
|
||||
whereTemplate: (operand, { companyName }) => {
|
||||
if (operand.keyWord === 'equal') {
|
||||
return {
|
||||
name: { _eq: companyName },
|
||||
};
|
||||
}
|
||||
|
||||
if (operand.keyWord === 'not_equal') {
|
||||
return {
|
||||
_not: { name: { _eq: companyName } },
|
||||
};
|
||||
}
|
||||
searchConfig: {
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
template: (searchInput) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
resultMapper: (company) => ({
|
||||
render: (company) => company.name,
|
||||
value: mapCompany(company),
|
||||
}),
|
||||
},
|
||||
searchQuery: SEARCH_COMPANY_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||
displayValue: company.name,
|
||||
value: { companyName: company.name },
|
||||
}),
|
||||
selectedValueRender: (company) => company.name,
|
||||
operands: [
|
||||
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
|
||||
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
|
||||
{
|
||||
label: 'Equal',
|
||||
id: 'equal',
|
||||
whereTemplate: (company) => ({
|
||||
name: { _eq: company.name },
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Not equal',
|
||||
id: 'not-equal',
|
||||
whereTemplate: (company) => ({
|
||||
_not: { name: { _eq: company.name } },
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
} as FilterConfigType<Company, Company>,
|
||||
{
|
||||
key: 'domainName',
|
||||
key: 'company_domain_name',
|
||||
label: 'Url',
|
||||
icon: <FaLink />,
|
||||
whereTemplate: (operand, { domainName }) => {
|
||||
if (operand.keyWord === 'equal') {
|
||||
return {
|
||||
domain_name: { _eq: domainName },
|
||||
};
|
||||
}
|
||||
|
||||
if (operand.keyWord === 'not_equal') {
|
||||
return {
|
||||
_not: { domain_name: { _eq: domainName } },
|
||||
};
|
||||
}
|
||||
searchConfig: {
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
template: (searchInput) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
resultMapper: (company) => ({
|
||||
render: (company) => company.domain_name,
|
||||
value: mapCompany(company),
|
||||
}),
|
||||
},
|
||||
searchQuery: SEARCH_COMPANY_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
domain_name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||
displayValue: company.domain_name,
|
||||
value: { domainName: company.domain_name },
|
||||
}),
|
||||
selectedValueRender: (company) => company.domain_name,
|
||||
operands: [
|
||||
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
|
||||
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
|
||||
{
|
||||
label: 'Equal',
|
||||
id: 'equal',
|
||||
whereTemplate: (company) => ({
|
||||
domain_name: { _eq: company.domain_name },
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Not equal',
|
||||
id: 'not-equal',
|
||||
whereTemplate: (company) => ({
|
||||
_not: { domain_name: { _eq: company.domain_name } },
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies Array<FilterType<Companies_Bool_Exp>>;
|
||||
} as FilterConfigType<Company, Company>,
|
||||
];
|
||||
|
||||
const columnHelper = createColumnHelper<Company>();
|
||||
|
||||
@ -239,18 +236,18 @@ export const useCompaniesColumns = () => {
|
||||
<ColumnHead viewName="Account Owner" viewIcon={<FaRegUser />} />
|
||||
),
|
||||
cell: (props) => (
|
||||
<EditableRelation<PartialUser, PersonChipPropsType>
|
||||
<EditableRelation<User, PersonChipPropsType>
|
||||
relation={props.row.original.accountOwner}
|
||||
searchPlaceholder="Account Owner"
|
||||
ChipComponent={PersonChip}
|
||||
chipComponentPropsMapper={(
|
||||
accountOwner: PartialUser,
|
||||
accountOwner: User,
|
||||
): PersonChipPropsType => {
|
||||
return {
|
||||
name: accountOwner.displayName,
|
||||
};
|
||||
}}
|
||||
changeHandler={(relation: PartialUser) => {
|
||||
changeHandler={(relation: User) => {
|
||||
const company = props.row.original;
|
||||
if (company.accountOwner) {
|
||||
company.accountOwner.id = relation.id;
|
||||
@ -263,28 +260,17 @@ export const useCompaniesColumns = () => {
|
||||
}
|
||||
updateCompany(company);
|
||||
}}
|
||||
searchFilter={
|
||||
searchConfig={
|
||||
{
|
||||
key: 'account_owner_name',
|
||||
label: 'Account Owner',
|
||||
icon: <FaUser />,
|
||||
whereTemplate: () => {
|
||||
return {};
|
||||
},
|
||||
searchQuery: SEARCH_USER_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
query: SEARCH_USER_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
displayName: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (accountOwner: GraphqlQueryUser) => ({
|
||||
displayValue: accountOwner.displayName,
|
||||
value: {
|
||||
id: accountOwner.id,
|
||||
email: accountOwner.email,
|
||||
displayName: accountOwner.displayName,
|
||||
},
|
||||
resultMapper: (accountOwner) => ({
|
||||
render: (accountOwner) => accountOwner.displayName,
|
||||
value: mapUser(accountOwner),
|
||||
}),
|
||||
operands: [],
|
||||
} satisfies FilterType<Users_Bool_Exp>
|
||||
} satisfies SearchConfigType<User>
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -19,7 +19,10 @@ import {
|
||||
} from '../../services/people';
|
||||
import { useSearch } from '../../services/search/search';
|
||||
import { People_Bool_Exp } from '../../generated/graphql';
|
||||
import { SelectedFilterType } from '../../components/table/table-header/interface';
|
||||
import {
|
||||
FilterConfigType,
|
||||
SelectedFilterType,
|
||||
} from '../../components/table/table-header/interface';
|
||||
import {
|
||||
reduceFiltersToWhere,
|
||||
reduceSortsToOrderBy,
|
||||
@ -44,7 +47,7 @@ function People() {
|
||||
}, []);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
(filters: Array<SelectedFilterType<People_Bool_Exp>>) => {
|
||||
(filters: Array<SelectedFilterType<Person>>) => {
|
||||
setWhere(reduceFiltersToWhere(filters));
|
||||
},
|
||||
[],
|
||||
@ -106,7 +109,7 @@ function People() {
|
||||
viewName="All People"
|
||||
viewIcon={<FaList />}
|
||||
availableSorts={availableSorts}
|
||||
availableFilters={availableFilters}
|
||||
availableFilters={availableFilters as Array<FilterConfigType>}
|
||||
filterSearchResults={filterSearchResults}
|
||||
onSortsUpdate={updateSorts}
|
||||
onFiltersUpdate={updateFilters}
|
||||
|
||||
@ -7,130 +7,3 @@ Object {
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the filter city 2`] = `
|
||||
Object {
|
||||
"_not": Object {
|
||||
"city": Object {
|
||||
"_eq": "Paris",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the filter company_name 1`] = `
|
||||
Object {
|
||||
"company": Object {
|
||||
"name": Object {
|
||||
"_eq": "Airbnb",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the filter company_name 2`] = `
|
||||
Object {
|
||||
"_not": Object {
|
||||
"company": Object {
|
||||
"name": Object {
|
||||
"_eq": "Airbnb",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the filter email 1`] = `
|
||||
Object {
|
||||
"email": Object {
|
||||
"_eq": "john@linkedin.com",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the filter email 2`] = `
|
||||
Object {
|
||||
"_not": Object {
|
||||
"email": Object {
|
||||
"_eq": "john@linkedin.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the filter fullname 1`] = `
|
||||
Object {
|
||||
"_and": Array [
|
||||
Object {
|
||||
"firstname": Object {
|
||||
"_eq": "John",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"lastname": Object {
|
||||
"_eq": "Doe",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the filter fullname 2`] = `
|
||||
Object {
|
||||
"_not": Object {
|
||||
"_and": Array [
|
||||
Object {
|
||||
"firstname": Object {
|
||||
"_eq": "John",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"lastname": Object {
|
||||
"_eq": "Doe",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the serch city with the searchValue 1`] = `
|
||||
Object {
|
||||
"city": Object {
|
||||
"_ilike": "%Search value%",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the serch company_name with the searchValue 1`] = `
|
||||
Object {
|
||||
"name": Object {
|
||||
"_ilike": "%Search value%",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the serch email with the searchValue 1`] = `
|
||||
Object {
|
||||
"email": Object {
|
||||
"_ilike": "%Search value%",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PeopleFilter should render the serch fullname with the searchValue 1`] = `
|
||||
Object {
|
||||
"_or": Array [
|
||||
Object {
|
||||
"firstname": Object {
|
||||
"_ilike": "%Search value%",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"lastname": Object {
|
||||
"_ilike": "%Search value%",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,65 +1,19 @@
|
||||
import { FilterType } from '../../../components/table/table-header/interface';
|
||||
import { People_Bool_Exp } from '../../../generated/graphql';
|
||||
import { GraphqlQueryCompany } from '../../../interfaces/company.interface';
|
||||
import { GraphqlQueryPerson } from '../../../interfaces/person.interface';
|
||||
import {
|
||||
SEARCH_COMPANY_QUERY,
|
||||
SEARCH_PEOPLE_QUERY,
|
||||
} from '../../../services/search/search';
|
||||
import { mockData as mockCompanyData } from '../../companies/__tests__/__data__/mock-data';
|
||||
import { mockData as mockPeopleData } from './__data__/mock-data';
|
||||
import { availableFilters } from '../people-table';
|
||||
|
||||
function assertFilterUseCompanySearch<FilterValue>(
|
||||
filter: FilterType<People_Bool_Exp>,
|
||||
): filter is FilterType<People_Bool_Exp> & {
|
||||
searchResultMapper: (data: GraphqlQueryCompany) => {
|
||||
displayValue: string;
|
||||
value: FilterValue;
|
||||
};
|
||||
} {
|
||||
return filter.searchQuery === SEARCH_COMPANY_QUERY;
|
||||
}
|
||||
|
||||
function assertFilterUsePeopleSearch<FilterValue>(
|
||||
filter: FilterType<People_Bool_Exp>,
|
||||
): filter is FilterType<People_Bool_Exp> & {
|
||||
searchResultMapper: (data: GraphqlQueryPerson) => {
|
||||
displayValue: string;
|
||||
value: FilterValue;
|
||||
};
|
||||
} {
|
||||
return filter.searchQuery === SEARCH_PEOPLE_QUERY;
|
||||
}
|
||||
|
||||
const JohnDoeUser = mockPeopleData.find(
|
||||
(user) => user.email === 'john@linkedin.com',
|
||||
) as GraphqlQueryPerson;
|
||||
import { cityFilter } from '../people-table';
|
||||
|
||||
describe('PeopleFilter', () => {
|
||||
for (const filter of availableFilters) {
|
||||
it(`should render the filter ${filter.key}`, () => {
|
||||
if (assertFilterUseCompanySearch(filter)) {
|
||||
const filterSelectedValue = filter.searchResultMapper(
|
||||
mockCompanyData[0],
|
||||
);
|
||||
for (const operand of filter.operands) {
|
||||
expect(
|
||||
filter.whereTemplate(operand, filterSelectedValue.value),
|
||||
).toMatchSnapshot();
|
||||
}
|
||||
}
|
||||
if (assertFilterUsePeopleSearch(filter)) {
|
||||
const filterSelectedValue = filter.searchResultMapper(JohnDoeUser);
|
||||
for (const operand of filter.operands) {
|
||||
expect(
|
||||
filter.whereTemplate(operand, filterSelectedValue.value),
|
||||
).toMatchSnapshot();
|
||||
}
|
||||
}
|
||||
});
|
||||
it(`should render the serch ${filter.key} with the searchValue`, () => {
|
||||
expect(filter.searchTemplate('Search value')).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
it(`should render the filter ${cityFilter.key}`, () => {
|
||||
expect(
|
||||
cityFilter.operands[0].whereTemplate({
|
||||
id: 'test-id',
|
||||
city: 'Paris',
|
||||
email: 'john@doe.com',
|
||||
firstname: 'John',
|
||||
lastname: 'Doe',
|
||||
phone: '0123456789',
|
||||
creationDate: new Date(),
|
||||
pipe: null,
|
||||
company: null,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,25 +14,19 @@ import Checkbox from '../../components/form/Checkbox';
|
||||
import CompanyChip, {
|
||||
CompanyChipPropsType,
|
||||
} from '../../components/chips/CompanyChip';
|
||||
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
|
||||
import { Person, mapPerson } from '../../interfaces/person.interface';
|
||||
import EditableText from '../../components/table/editable-cell/EditableText';
|
||||
import {
|
||||
FilterType,
|
||||
FilterConfigType,
|
||||
SearchConfigType,
|
||||
SortType,
|
||||
} from '../../components/table/table-header/interface';
|
||||
import {
|
||||
Order_By,
|
||||
People_Bool_Exp,
|
||||
People_Order_By,
|
||||
} from '../../generated/graphql';
|
||||
import { Order_By, People_Order_By } from '../../generated/graphql';
|
||||
import {
|
||||
SEARCH_COMPANY_QUERY,
|
||||
SEARCH_PEOPLE_QUERY,
|
||||
} from '../../services/search/search';
|
||||
import {
|
||||
GraphqlQueryCompany,
|
||||
PartialCompany,
|
||||
} from '../../interfaces/company.interface';
|
||||
import { Company, mapCompany } from '../../interfaces/company.interface';
|
||||
import EditablePhone from '../../components/table/editable-cell/EditablePhone';
|
||||
import EditableFullName from '../../components/table/editable-cell/EditableFullName';
|
||||
import EditableDate from '../../components/table/editable-cell/EditableDate';
|
||||
@ -85,163 +79,155 @@ export const availableSorts = [
|
||||
},
|
||||
] satisfies Array<SortType<People_Order_By>>;
|
||||
|
||||
const fullnameFilter = {
|
||||
export const fullnameFilter = {
|
||||
key: 'fullname',
|
||||
label: 'People',
|
||||
icon: <FaUser />,
|
||||
whereTemplate: (operand, { firstname, lastname }) => {
|
||||
if (operand.keyWord === 'equal') {
|
||||
return {
|
||||
searchConfig: {
|
||||
query: SEARCH_PEOPLE_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
_or: [
|
||||
{ firstname: { _ilike: `%${searchInput}%` } },
|
||||
{ lastname: { _ilike: `%${searchInput}%` } },
|
||||
],
|
||||
}),
|
||||
resultMapper: (person) => ({
|
||||
render: (person) => `${person.firstname} ${person.lastname}`,
|
||||
value: mapPerson(person),
|
||||
}),
|
||||
},
|
||||
selectedValueRender: (person) => `${person.firstname} ${person.lastname}`,
|
||||
operands: [
|
||||
{
|
||||
label: 'Equal',
|
||||
id: 'equal',
|
||||
whereTemplate: (person) => ({
|
||||
_and: [
|
||||
{ firstname: { _eq: `${firstname}` } },
|
||||
{ lastname: { _eq: `${lastname}` } },
|
||||
{ firstname: { _eq: `${person.firstname}` } },
|
||||
{ lastname: { _eq: `${person.lastname}` } },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (operand.keyWord === 'not_equal') {
|
||||
return {
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Not equal',
|
||||
id: 'not-equal',
|
||||
whereTemplate: (person) => ({
|
||||
_not: {
|
||||
_and: [
|
||||
{ firstname: { _eq: `${firstname}` } },
|
||||
{ lastname: { _eq: `${lastname}` } },
|
||||
{ firstname: { _eq: `${person.firstname}` } },
|
||||
{ lastname: { _eq: `${person.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 },
|
||||
}),
|
||||
operands: [
|
||||
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
|
||||
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
|
||||
}),
|
||||
},
|
||||
],
|
||||
} satisfies FilterType<People_Bool_Exp>;
|
||||
} satisfies FilterConfigType<Person, Person>;
|
||||
|
||||
const companyFilter = {
|
||||
export const companyFilter = {
|
||||
key: 'company_name',
|
||||
label: 'Company',
|
||||
icon: <FaBuilding />,
|
||||
whereTemplate: (operand, { companyName }) => {
|
||||
if (operand.keyWord === 'equal') {
|
||||
return {
|
||||
company: { name: { _eq: companyName } },
|
||||
};
|
||||
}
|
||||
|
||||
if (operand.keyWord === 'not_equal') {
|
||||
return {
|
||||
_not: { company: { name: { _eq: companyName } } },
|
||||
};
|
||||
}
|
||||
searchConfig: {
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
resultMapper: (data) => ({
|
||||
value: mapCompany(data),
|
||||
render: (company) => company.name,
|
||||
}),
|
||||
},
|
||||
searchQuery: SEARCH_COMPANY_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||
displayValue: company.name,
|
||||
value: { companyName: company.name },
|
||||
}),
|
||||
selectedValueRender: (company) => company.name,
|
||||
operands: [
|
||||
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
|
||||
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
|
||||
{
|
||||
label: 'Equal',
|
||||
id: 'equal',
|
||||
whereTemplate: (company) => ({
|
||||
company: { name: { _eq: company.name } },
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Not equal',
|
||||
id: 'not-equal',
|
||||
whereTemplate: (company) => ({
|
||||
_not: { company: { name: { _eq: company.name } } },
|
||||
}),
|
||||
},
|
||||
],
|
||||
} satisfies FilterType<People_Bool_Exp>;
|
||||
} satisfies FilterConfigType<Person, Company>;
|
||||
|
||||
const emailFilter = {
|
||||
export const emailFilter = {
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
icon: <FaEnvelope />,
|
||||
whereTemplate: (operand, { email }) => {
|
||||
if (operand.keyWord === 'equal') {
|
||||
return {
|
||||
email: { _eq: email },
|
||||
};
|
||||
}
|
||||
|
||||
if (operand.keyWord === 'not_equal') {
|
||||
return {
|
||||
_not: { email: { _eq: email } },
|
||||
};
|
||||
}
|
||||
searchConfig: {
|
||||
query: SEARCH_PEOPLE_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
email: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
resultMapper: (person) => ({
|
||||
render: (person) => person.email,
|
||||
value: mapPerson(person),
|
||||
}),
|
||||
},
|
||||
searchQuery: SEARCH_PEOPLE_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
email: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (person: GraphqlQueryPerson) => ({
|
||||
displayValue: person.email,
|
||||
value: { email: person.email },
|
||||
}),
|
||||
operands: [
|
||||
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
|
||||
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
|
||||
{
|
||||
label: 'Equal',
|
||||
id: 'equal',
|
||||
whereTemplate: (person) => ({
|
||||
email: { _eq: person.email },
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Not equal',
|
||||
id: 'not-equal',
|
||||
whereTemplate: (person) => ({
|
||||
_not: { email: { _eq: person.email } },
|
||||
}),
|
||||
},
|
||||
],
|
||||
} satisfies FilterType<People_Bool_Exp>;
|
||||
selectedValueRender: (person) => person.email,
|
||||
} satisfies FilterConfigType<Person, Person>;
|
||||
|
||||
const cityFilter = {
|
||||
export const cityFilter = {
|
||||
key: 'city',
|
||||
label: 'City',
|
||||
icon: <FaMapPin />,
|
||||
whereTemplate: (operand, { city }) => {
|
||||
if (operand.keyWord === 'equal') {
|
||||
return {
|
||||
city: { _eq: city },
|
||||
};
|
||||
}
|
||||
|
||||
if (operand.keyWord === 'not_equal') {
|
||||
return {
|
||||
_not: { city: { _eq: city } },
|
||||
};
|
||||
}
|
||||
searchConfig: {
|
||||
query: SEARCH_PEOPLE_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
city: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
resultMapper: (person) => ({
|
||||
render: (person) => person.city,
|
||||
value: mapPerson(person),
|
||||
}),
|
||||
},
|
||||
searchQuery: SEARCH_PEOPLE_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
city: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (person: GraphqlQueryPerson) => ({
|
||||
displayValue: person.city,
|
||||
value: { city: person.city },
|
||||
}),
|
||||
operands: [
|
||||
{ label: 'Equal', id: 'equal', keyWord: 'equal' },
|
||||
{ label: 'Not equal', id: 'not-equal', keyWord: 'not_equal' },
|
||||
{
|
||||
label: 'Equal',
|
||||
id: 'equal',
|
||||
whereTemplate: (person) => ({
|
||||
city: { _eq: person.city },
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Not equal',
|
||||
id: 'not-equal',
|
||||
whereTemplate: (person) => ({
|
||||
_not: { city: { _eq: person.city } },
|
||||
}),
|
||||
},
|
||||
],
|
||||
} satisfies FilterType<People_Bool_Exp>;
|
||||
selectedValueRender: (person) => person.email,
|
||||
} satisfies FilterConfigType<Person, Person>;
|
||||
|
||||
export const availableFilters = [
|
||||
fullnameFilter,
|
||||
companyFilter,
|
||||
emailFilter,
|
||||
cityFilter,
|
||||
// {
|
||||
// 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%' } },
|
||||
// },
|
||||
] satisfies FilterType<People_Bool_Exp>[];
|
||||
];
|
||||
|
||||
const columnHelper = createColumnHelper<Person>();
|
||||
|
||||
@ -300,53 +286,36 @@ export const usePeopleColumns = () => {
|
||||
<ColumnHead viewName="Company" viewIcon={<FaRegBuilding />} />
|
||||
),
|
||||
cell: (props) => (
|
||||
<EditableRelation<PartialCompany, CompanyChipPropsType>
|
||||
<EditableRelation<Company, CompanyChipPropsType>
|
||||
relation={props.row.original.company}
|
||||
searchPlaceholder="Company"
|
||||
ChipComponent={CompanyChip}
|
||||
chipComponentPropsMapper={(
|
||||
company: PartialCompany,
|
||||
): CompanyChipPropsType => {
|
||||
chipComponentPropsMapper={(company): CompanyChipPropsType => {
|
||||
return {
|
||||
name: company.name,
|
||||
picture: `https://www.google.com/s2/favicons?domain=${company.domain_name}&sz=256`,
|
||||
};
|
||||
}}
|
||||
changeHandler={(relation: PartialCompany) => {
|
||||
changeHandler={(relation) => {
|
||||
const person = props.row.original;
|
||||
if (person.company) {
|
||||
person.company.id = relation.id;
|
||||
} else {
|
||||
person.company = {
|
||||
id: relation.id,
|
||||
name: relation.name,
|
||||
domain_name: relation.domain_name,
|
||||
};
|
||||
person.company = relation;
|
||||
}
|
||||
updatePerson(person);
|
||||
}}
|
||||
searchFilter={
|
||||
searchConfig={
|
||||
{
|
||||
key: 'company_name',
|
||||
label: 'Company',
|
||||
icon: <FaBuilding />,
|
||||
whereTemplate: () => {
|
||||
return {};
|
||||
},
|
||||
searchQuery: SEARCH_COMPANY_QUERY,
|
||||
searchTemplate: (searchInput: string) => ({
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
name: { _ilike: `%${searchInput}%` },
|
||||
}),
|
||||
searchResultMapper: (company: GraphqlQueryCompany) => ({
|
||||
displayValue: company.name,
|
||||
value: {
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
domain_name: company.domain_name,
|
||||
},
|
||||
resultMapper: (company) => ({
|
||||
render: (company) => company.name,
|
||||
value: mapCompany(company),
|
||||
}),
|
||||
operands: [],
|
||||
} satisfies FilterType<People_Bool_Exp>
|
||||
} satisfies SearchConfigType<Company>
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
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';
|
||||
import {
|
||||
SearchConfigType,
|
||||
SearchableType,
|
||||
} from '../../components/table/table-header/interface';
|
||||
|
||||
export const SEARCH_PEOPLE_QUERY = gql`
|
||||
query SearchQuery($where: people_bool_exp, $limit: Int) {
|
||||
@ -57,12 +58,18 @@ const debounce = <FuncArgs extends any[]>(
|
||||
};
|
||||
};
|
||||
|
||||
export const useSearch = (): [
|
||||
{ results: { displayValue: string; value: any }[]; loading: boolean },
|
||||
export const useSearch = <T extends SearchableType>(): [
|
||||
{
|
||||
results: {
|
||||
render: (value: T) => string;
|
||||
value: T;
|
||||
}[];
|
||||
loading: boolean;
|
||||
},
|
||||
React.Dispatch<React.SetStateAction<string>>,
|
||||
React.Dispatch<React.SetStateAction<FilterType<People_Bool_Exp> | null>>,
|
||||
React.Dispatch<React.SetStateAction<SearchConfigType<T> | null>>,
|
||||
] => {
|
||||
const [filter, setFilter] = useState<FilterType<People_Bool_Exp> | null>(
|
||||
const [searchConfig, setSearchConfig] = useState<SearchConfigType<T> | null>(
|
||||
null,
|
||||
);
|
||||
const [searchInput, setSearchInput] = useState<string>('');
|
||||
@ -74,26 +81,28 @@ export const useSearch = (): [
|
||||
|
||||
const where = useMemo(() => {
|
||||
return (
|
||||
filter && filter.searchTemplate && filter.searchTemplate(searchInput)
|
||||
searchConfig &&
|
||||
searchConfig.template &&
|
||||
searchConfig.template(searchInput)
|
||||
);
|
||||
}, [filter, searchInput]);
|
||||
}, [searchConfig, searchInput]);
|
||||
|
||||
const searchFilterQueryResults = useQuery(
|
||||
filter?.searchQuery || EMPTY_QUERY,
|
||||
searchConfig?.query || EMPTY_QUERY,
|
||||
{
|
||||
variables: {
|
||||
where,
|
||||
limit: 5,
|
||||
},
|
||||
skip: !filter,
|
||||
skip: !searchConfig,
|
||||
},
|
||||
);
|
||||
|
||||
const searchFilterResults = useMemo<{
|
||||
results: { displayValue: string; value: any }[];
|
||||
results: { render: (value: T) => string; value: any }[];
|
||||
loading: boolean;
|
||||
}>(() => {
|
||||
if (filter == null) {
|
||||
if (searchConfig == null) {
|
||||
return {
|
||||
loading: false,
|
||||
results: [],
|
||||
@ -108,10 +117,10 @@ export const useSearch = (): [
|
||||
return {
|
||||
loading: false,
|
||||
results: searchFilterQueryResults.data.searchResults.map(
|
||||
filter.searchResultMapper,
|
||||
searchConfig.resultMapper,
|
||||
),
|
||||
};
|
||||
}, [filter, searchFilterQueryResults]);
|
||||
}, [searchConfig, searchFilterQueryResults]);
|
||||
|
||||
return [searchFilterResults, debouncedsetSearchInput, setFilter];
|
||||
return [searchFilterResults, debouncedsetSearchInput, setSearchConfig];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user