Simplifies search through relations usage (#126)

This commit is contained in:
Charles Bochet
2023-05-17 23:10:00 +02:00
committed by GitHub
parent 434e020846
commit cdc9e24ac0
15 changed files with 455 additions and 491 deletions

View File

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import styled from '@emotion/styled';
import { import {
ColumnDef, ColumnDef,
@ -7,15 +8,10 @@ import {
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import TableHeader from './table-header/TableHeader'; import TableHeader from './table-header/TableHeader';
import styled from '@emotion/styled';
import { import {
FilterConfigType, FilterConfigType,
SelectedFilterType, SelectedFilterType,
} from '../../interfaces/filters/interface'; } from '../../interfaces/filters/interface';
import {
SearchableType,
SearchConfigType,
} from '../../interfaces/search/interface';
import { SortType, SelectedSortType } from '../../interfaces/sorts/interface'; import { SortType, SelectedSortType } from '../../interfaces/sorts/interface';
declare module 'react' { declare module 'react' {
@ -34,19 +30,8 @@ type OwnProps<
viewIcon?: React.ReactNode; viewIcon?: React.ReactNode;
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<SortField>>;
availableFilters?: FilterConfigType<TData>[]; availableFilters?: FilterConfigType<TData>[];
filterSearchResults?: {
results: {
render: (value: SearchableType) => string;
value: SearchableType;
}[];
loading: boolean;
};
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: (sorts: Array<SelectedFilterType<TData>>) => void; onFiltersUpdate?: (sorts: Array<SelectedFilterType<TData>>) => void;
onFilterSearch?: (
filter: SearchConfigType<any> | null,
searchValue: string,
) => void;
onRowSelectionChange?: (rowSelection: string[]) => void; onRowSelectionChange?: (rowSelection: string[]) => void;
}; };
@ -112,10 +97,8 @@ const Table = <
viewIcon, viewIcon,
availableSorts, availableSorts,
availableFilters, availableFilters,
filterSearchResults,
onSortsUpdate, onSortsUpdate,
onFiltersUpdate, onFiltersUpdate,
onFilterSearch,
onRowSelectionChange, onRowSelectionChange,
}: OwnProps<TData, SortField>, }: OwnProps<TData, SortField>,
ref: React.ForwardedRef<{ resetRowSelection: () => void } | undefined>, ref: React.ForwardedRef<{ resetRowSelection: () => void } | undefined>,
@ -156,10 +139,8 @@ const Table = <
viewIcon={viewIcon} viewIcon={viewIcon}
availableSorts={availableSorts} availableSorts={availableSorts}
availableFilters={availableFilters as FilterConfigType<any>[]} availableFilters={availableFilters as FilterConfigType<any>[]}
filterSearchResults={filterSearchResults}
onSortsUpdate={onSortsUpdate} onSortsUpdate={onSortsUpdate}
onFiltersUpdate={onFiltersUpdate} onFiltersUpdate={onFiltersUpdate}
onFilterSearch={onFilterSearch}
/> />
<StyledTableScrollableContainer> <StyledTableScrollableContainer>
<StyledTable> <StyledTable>

View File

@ -7,31 +7,19 @@ import {
SelectedFilterType, SelectedFilterType,
} from '../../../interfaces/filters/interface'; } from '../../../interfaces/filters/interface';
import { import {
SearchConfigType, SearchResultsType,
SearchableType, useSearch,
} from '../../../interfaces/search/interface'; } from '../../../services/api/search/search';
import { SearchableType } from '../../../interfaces/search/interface';
type OwnProps<TData extends FilterableFieldsType> = { type OwnProps<TData extends FilterableFieldsType> = {
isFilterSelected: boolean; isFilterSelected: boolean;
availableFilters: FilterConfigType<TData>[]; availableFilters: FilterConfigType<TData>[];
filterSearchResults?: {
results: {
render: (value: SearchableType) => string;
value: SearchableType;
}[];
loading: boolean;
};
onFilterSelect: (filter: SelectedFilterType<TData>) => void; onFilterSelect: (filter: SelectedFilterType<TData>) => void;
onFilterSearch: (
filter: SearchConfigType<any> | null,
searchValue: string,
) => void;
}; };
export const FilterDropdownButton = <TData extends FilterableFieldsType>({ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
availableFilters, availableFilters,
filterSearchResults,
onFilterSearch,
onFilterSelect, onFilterSelect,
isFilterSelected, isFilterSelected,
}: OwnProps<TData>) => { }: OwnProps<TData>) => {
@ -47,12 +35,14 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
FilterOperandType<TData> | undefined FilterOperandType<TData> | undefined
>(undefined); >(undefined);
const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch();
const resetState = useCallback(() => { const resetState = useCallback(() => {
setIsOptionUnfolded(false); setIsOptionUnfolded(false);
setSelectedFilter(undefined); setSelectedFilter(undefined);
setSelectedFilterOperand(undefined); setSelectedFilterOperand(undefined);
onFilterSearch(null, ''); setFilterSearch(null);
}, [onFilterSearch]); }, [setFilterSearch]);
const renderSelectOptionItems = selectedFilter?.operands.map( const renderSelectOptionItems = selectedFilter?.operands.map(
(filterOperand, index) => ( (filterOperand, index) => (
@ -69,7 +59,7 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
); );
const renderSearchResults = ( const renderSearchResults = (
filterSearchResults: NonNullable<OwnProps<TData>['filterSearchResults']>, filterSearchResults: SearchResultsType<SearchableType>,
selectedFilter: FilterConfigType<TData>, selectedFilter: FilterConfigType<TData>,
selectedFilterOperand: FilterOperandType<TData>, selectedFilterOperand: FilterOperandType<TData>,
) => { ) => {
@ -108,7 +98,8 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
onClick={() => { onClick={() => {
setSelectedFilter(filter); setSelectedFilter(filter);
setSelectedFilterOperand(filter.operands[0]); setSelectedFilterOperand(filter.operands[0]);
onFilterSearch(filter.searchConfig, ''); setFilterSearch(filter.searchConfig);
setSearchInput('');
}} }}
> >
<DropdownButton.StyledIcon>{filter.icon}</DropdownButton.StyledIcon> <DropdownButton.StyledIcon>{filter.icon}</DropdownButton.StyledIcon>
@ -134,9 +125,10 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
<input <input
type="text" type="text"
placeholder={selectedFilter.label} placeholder={selectedFilter.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange={(event: ChangeEvent<HTMLInputElement>) => {
onFilterSearch(selectedFilter.searchConfig, event.target.value) setFilterSearch(selectedFilter.searchConfig);
} setSearchInput(event.target.value);
}}
/> />
</DropdownButton.StyledSearchField> </DropdownButton.StyledSearchField>
{filterSearchResults && {filterSearchResults &&

View File

@ -9,10 +9,6 @@ import {
FilterConfigType, FilterConfigType,
SelectedFilterType, SelectedFilterType,
} from '../../../interfaces/filters/interface'; } from '../../../interfaces/filters/interface';
import {
SearchableType,
SearchConfigType,
} from '../../../interfaces/search/interface';
import { import {
SortType, SortType,
SelectedSortType, SelectedSortType,
@ -23,19 +19,8 @@ type OwnProps<SortField, TData extends FilterableFieldsType> = {
viewIcon?: ReactNode; viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<SortField>>;
availableFilters?: FilterConfigType<TData>[]; availableFilters?: FilterConfigType<TData>[];
filterSearchResults?: {
results: {
render: (value: SearchableType) => string;
value: SearchableType;
}[];
loading: boolean;
};
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: (sorts: Array<SelectedFilterType<TData>>) => void; onFiltersUpdate?: (sorts: Array<SelectedFilterType<TData>>) => void;
onFilterSearch?: (
filter: SearchConfigType<any> | null,
searchValue: string,
) => void;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -79,10 +64,8 @@ function TableHeader<SortField, TData extends FilterableFieldsType>({
viewIcon, viewIcon,
availableSorts, availableSorts,
availableFilters, availableFilters,
filterSearchResults,
onSortsUpdate, onSortsUpdate,
onFiltersUpdate, onFiltersUpdate,
onFilterSearch,
}: OwnProps<SortField, TData>) { }: OwnProps<SortField, TData>) {
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>( const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
[], [],
@ -128,13 +111,6 @@ function TableHeader<SortField, TData extends FilterableFieldsType>({
[onFiltersUpdate, filters], [onFiltersUpdate, filters],
); );
const filterSearch = useCallback(
(filter: SearchConfigType<any> | null, searchValue: string) => {
onFilterSearch && onFilterSearch(filter, searchValue);
},
[onFilterSearch],
);
return ( return (
<StyledContainer> <StyledContainer>
<StyledTableHeader> <StyledTableHeader>
@ -146,9 +122,7 @@ function TableHeader<SortField, TData extends FilterableFieldsType>({
<FilterDropdownButton <FilterDropdownButton
isFilterSelected={filters.length > 0} isFilterSelected={filters.length > 0}
availableFilters={availableFilters || []} availableFilters={availableFilters || []}
filterSearchResults={filterSearchResults}
onFilterSelect={filterSelect} onFilterSelect={filterSelect}
onFilterSearch={filterSearch}
/> />
<SortDropdownButton<SortField> <SortDropdownButton<SortField>
isSortSelected={sorts.length > 0} isSortSelected={sorts.length > 0}

View File

@ -3,13 +3,10 @@ import { lightTheme } from '../../../../layout/styles/themes';
import { FilterDropdownButton } from '../FilterDropdownButton'; import { FilterDropdownButton } from '../FilterDropdownButton';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { import { SEARCH_PEOPLE_QUERY } from '../../../../services/api/search/search';
SEARCH_PEOPLE_QUERY,
useSearch,
} from '../../../../services/api/search/search';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { mockData } from '../../../../pages/people/__tests__/__data__/mock-data'; import { mockData } from '../../../../pages/people/__tests__/__data__/mock-data';
import { availableFilters } from '../../../../pages/people/people-table'; import { availableFilters } from '../../../../pages/people/people-filters';
import { Person } from '../../../../interfaces/entities/person.interface'; import { Person } from '../../../../interfaces/entities/person.interface';
import { import {
FilterableFieldsType, FilterableFieldsType,
@ -90,7 +87,6 @@ const InnerRegularFilterDropdownButton = ({
setFilter: setFilters, setFilter: setFilters,
}: OwnProps<Person>) => { }: OwnProps<Person>) => {
const [, innerSetFilters] = useState<SelectedFilterType<Person>>(); const [, innerSetFilters] = useState<SelectedFilterType<Person>>();
const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch();
const outerSetFilters = useCallback( const outerSetFilters = useCallback(
(filter: SelectedFilterType<Person>) => { (filter: SelectedFilterType<Person>) => {
@ -105,11 +101,6 @@ const InnerRegularFilterDropdownButton = ({
availableFilters={availableFilters} availableFilters={availableFilters}
isFilterSelected={true} isFilterSelected={true}
onFilterSelect={outerSetFilters} onFilterSelect={outerSetFilters}
filterSearchResults={filterSearchResults}
onFilterSearch={(filter, searchValue) => {
setSearhInput(searchValue);
setFilterSearch(filter);
}}
/> />
</StyleDiv> </StyleDiv>
); );

View File

@ -3,11 +3,28 @@ import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes'; import { lightTheme } from '../../../../layout/styles/themes';
import { FaRegBuilding, FaCalendar } from 'react-icons/fa'; import { FaRegBuilding, FaCalendar } from 'react-icons/fa';
import { SortType } from '../../../../interfaces/sorts/interface'; import { SortType } from '../../../../interfaces/sorts/interface';
import { MockedProvider } from '@apollo/client/testing';
import { EMPTY_QUERY } from '../../../../services/api/search/search';
const component = { const component = {
title: 'TableHeader', title: 'TableHeader',
component: TableHeader, component: TableHeader,
}; };
const mocks = [
{
request: {
query: EMPTY_QUERY,
variables: {
where: undefined,
},
},
result: {
data: {
searchResults: [],
},
},
},
];
export default component; export default component;
@ -21,12 +38,14 @@ export const RegularTableHeader = () => {
}, },
]; ];
return ( return (
<ThemeProvider theme={lightTheme}> <MockedProvider mocks={mocks}>
<TableHeader <ThemeProvider theme={lightTheme}>
viewName="Test" <TableHeader
viewIcon={<FaRegBuilding />} viewName="Test"
availableSorts={availableSorts} viewIcon={<FaRegBuilding />}
/> availableSorts={availableSorts}
</ThemeProvider> />
</ThemeProvider>
</MockedProvider>
); );
}; };

View File

@ -1,7 +1,7 @@
import { FaRegBuilding, FaList } from 'react-icons/fa';
import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
import styled from '@emotion/styled';
import { useState, useCallback, useEffect, useRef } from 'react'; import { useState, useCallback, useEffect, useRef } from 'react';
import { FaRegBuilding, FaList } from 'react-icons/fa';
import styled from '@emotion/styled';
import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { import {
CompaniesSelectedSortType, CompaniesSelectedSortType,
@ -15,22 +15,18 @@ import {
Company, Company,
mapToCompany, mapToCompany,
} from '../../interfaces/entities/company.interface'; } from '../../interfaces/entities/company.interface';
import {
useCompaniesColumns,
availableFilters,
availableSorts,
} from './companies-table';
import { import {
reduceFiltersToWhere, reduceFiltersToWhere,
reduceSortsToOrderBy, reduceSortsToOrderBy,
} from '../../components/table/table-header/helpers'; } from '../../components/table/table-header/helpers';
import { import { Companies_Order_By } from '../../generated/graphql';
Companies_Bool_Exp,
Companies_Order_By,
} from '../../generated/graphql';
import { useSearch } from '../../services/api/search/search';
import ActionBar from '../../components/table/action-bar/ActionBar'; import ActionBar from '../../components/table/action-bar/ActionBar';
import { SelectedFilterType } from '../../interfaces/filters/interface'; import { SelectedFilterType } from '../../interfaces/filters/interface';
import { BoolExpType } from '../../interfaces/entities/generic.interface';
import { useCompaniesColumns } from './companies-columns';
import { availableSorts } from './companies-sorts';
import { availableFilters } from './companies-filters';
const StyledCompaniesContainer = styled.div` const StyledCompaniesContainer = styled.div`
display: flex; display: flex;
@ -39,12 +35,10 @@ const StyledCompaniesContainer = styled.div`
function Companies() { function Companies() {
const [orderBy, setOrderBy] = useState<Companies_Order_By[]>(defaultOrderBy); const [orderBy, setOrderBy] = useState<Companies_Order_By[]>(defaultOrderBy);
const [where, setWhere] = useState<Companies_Bool_Exp>({}); const [where, setWhere] = useState<BoolExpType<Company>>({});
const [internalData, setInternalData] = useState<Array<Company>>([]); const [internalData, setInternalData] = useState<Array<Company>>([]);
const [selectedRowIds, setSelectedRowIds] = useState<Array<string>>([]); const [selectedRowIds, setSelectedRowIds] = useState<Array<string>>([]);
const [filterSearchResults, setSearhInput, setFilterSearch] = useSearch();
const updateSorts = useCallback((sorts: Array<CompaniesSelectedSortType>) => { const updateSorts = useCallback((sorts: Array<CompaniesSelectedSortType>) => {
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy); setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
}, []); }, []);
@ -113,13 +107,8 @@ function Companies() {
viewIcon={<FaList />} viewIcon={<FaList />}
availableSorts={availableSorts} availableSorts={availableSorts}
availableFilters={availableFilters} availableFilters={availableFilters}
filterSearchResults={filterSearchResults}
onSortsUpdate={updateSorts} onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters} onFiltersUpdate={updateFilters}
onFilterSearch={(filter, searchValue) => {
setSearhInput(searchValue);
setFilterSearch(filter);
}}
onRowSelectionChange={setSelectedRowIds} onRowSelectionChange={setSelectedRowIds}
/> />
</StyledCompaniesContainer> </StyledCompaniesContainer>

View File

@ -1,12 +1,4 @@
import { CellContext, createColumnHelper } from '@tanstack/react-table'; import { useMemo } from 'react';
import {
Company,
mapToCompany,
} from '../../interfaces/entities/company.interface';
import { updateCompany } from '../../services/api/companies';
import ColumnHead from '../../components/table/ColumnHead';
import CompanyChip from '../../components/chips/CompanyChip';
import EditableText from '../../components/editable-cell/EditableText';
import { import {
FaRegBuilding, FaRegBuilding,
FaCalendar, FaCalendar,
@ -14,126 +6,27 @@ import {
FaMapPin, FaMapPin,
FaRegUser, FaRegUser,
FaUsers, FaUsers,
FaBuilding,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { CellContext, createColumnHelper } from '@tanstack/react-table';
import { SEARCH_USER_QUERY } from '../../services/api/search/search';
import { SearchConfigType } from '../../interfaces/search/interface';
import { Company } from '../../interfaces/entities/company.interface';
import { updateCompany } from '../../services/api/companies';
import { User, mapToUser } from '../../interfaces/entities/user.interface';
import ColumnHead from '../../components/table/ColumnHead';
import Checkbox from '../../components/form/Checkbox';
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
import EditableDate from '../../components/editable-cell/EditableDate';
import EditableRelation from '../../components/editable-cell/EditableRelation';
import EditableChip from '../../components/editable-cell/EditableChip';
import EditableText from '../../components/editable-cell/EditableText';
import PersonChip, { import PersonChip, {
PersonChipPropsType, PersonChipPropsType,
} from '../../components/chips/PersonChip'; } from '../../components/chips/PersonChip';
import EditableChip from '../../components/editable-cell/EditableChip'; import CompanyChip from '../../components/chips/CompanyChip';
import { Companies_Order_By } from '../../generated/graphql';
import {
SEARCH_COMPANY_QUERY,
SEARCH_USER_QUERY,
} from '../../services/api/search/search';
import EditableDate from '../../components/editable-cell/EditableDate';
import EditableRelation from '../../components/editable-cell/EditableRelation';
import { User, mapToUser } from '../../interfaces/entities/user.interface';
import { useMemo } from 'react';
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
import Checkbox from '../../components/form/Checkbox';
import { SortType } from '../../interfaces/sorts/interface';
import { FilterConfigType } from '../../interfaces/filters/interface';
import { SearchConfigType } from '../../interfaces/search/interface';
export const availableSorts = [
{
key: 'name',
label: 'Name',
icon: <FaBuilding />,
_type: 'default_sort',
},
{
key: 'employees',
label: 'Employees',
icon: <FaUsers />,
_type: 'default_sort',
},
{
key: 'domain_name',
label: 'Url',
icon: <FaLink />,
_type: 'default_sort',
},
{
key: 'address',
label: 'Address',
icon: <FaMapPin />,
_type: 'default_sort',
},
{
key: 'created_at',
label: 'Creation',
icon: <FaCalendar />,
_type: 'default_sort',
},
] satisfies Array<SortType<Companies_Order_By>>;
export const availableFilters = [
{
key: 'company_name',
label: 'Company',
icon: <FaBuilding />,
searchConfig: {
query: SEARCH_COMPANY_QUERY,
template: (searchInput) => ({
name: { _ilike: `%${searchInput}%` },
}),
resultMapper: (company) => ({
render: (company) => company.name,
value: mapToCompany(company),
}),
},
selectedValueRender: (company) => company.name || '',
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (company) => ({
name: { _eq: company.name },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (company) => ({
_not: { name: { _eq: company.name } },
}),
},
],
} satisfies FilterConfigType<Company, Company>,
{
key: 'company_domain_name',
label: 'Url',
icon: <FaLink />,
searchConfig: {
query: SEARCH_COMPANY_QUERY,
template: (searchInput) => ({
name: { _ilike: `%${searchInput}%` },
}),
resultMapper: (company) => ({
render: (company) => company.domainName,
value: mapToCompany(company),
}),
},
selectedValueRender: (company) => company.domainName || '',
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (company) => ({
domain_name: { _eq: company.domainName },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (company) => ({
_not: { domain_name: { _eq: company.domainName } },
}),
},
],
} satisfies FilterConfigType<Company, Company>,
];
const columnHelper = createColumnHelper<Company>(); const columnHelper = createColumnHelper<Company>();

View File

@ -0,0 +1,74 @@
import {
Company,
mapToCompany,
} from '../../interfaces/entities/company.interface';
import { FaLink, FaBuilding } from 'react-icons/fa';
import { SEARCH_COMPANY_QUERY } from '../../services/api/search/search';
import { FilterConfigType } from '../../interfaces/filters/interface';
export const availableFilters = [
{
key: 'company_name',
label: 'Company',
icon: <FaBuilding />,
searchConfig: {
query: SEARCH_COMPANY_QUERY,
template: (searchInput) => ({
name: { _ilike: `%${searchInput}%` },
}),
resultMapper: (company) => ({
render: (company) => company.name,
value: mapToCompany(company),
}),
},
selectedValueRender: (company) => company.name || '',
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (company) => ({
name: { _eq: company.name },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (company) => ({
_not: { name: { _eq: company.name } },
}),
},
],
} satisfies FilterConfigType<Company, Company>,
{
key: 'company_domain_name',
label: 'Url',
icon: <FaLink />,
searchConfig: {
query: SEARCH_COMPANY_QUERY,
template: (searchInput) => ({
name: { _ilike: `%${searchInput}%` },
}),
resultMapper: (company) => ({
render: (company) => company.domainName,
value: mapToCompany(company),
}),
},
selectedValueRender: (company) => company.domainName || '',
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (company) => ({
domain_name: { _eq: company.domainName },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (company) => ({
_not: { domain_name: { _eq: company.domainName } },
}),
},
],
} satisfies FilterConfigType<Company, Company>,
];

View File

@ -0,0 +1,42 @@
import {
FaCalendar,
FaLink,
FaMapPin,
FaUsers,
FaBuilding,
} from 'react-icons/fa';
import { Companies_Order_By } from '../../generated/graphql';
import { SortType } from '../../interfaces/sorts/interface';
export const availableSorts = [
{
key: 'name',
label: 'Name',
icon: <FaBuilding />,
_type: 'default_sort',
},
{
key: 'employees',
label: 'Employees',
icon: <FaUsers />,
_type: 'default_sort',
},
{
key: 'domain_name',
label: 'Url',
icon: <FaLink />,
_type: 'default_sort',
},
{
key: 'address',
label: 'Address',
icon: <FaMapPin />,
_type: 'default_sort',
},
{
key: 'created_at',
label: 'Creation',
icon: <FaCalendar />,
_type: 'default_sort',
},
] satisfies Array<SortType<Companies_Order_By>>;

View File

@ -1,18 +1,15 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { FaRegUser, FaList } from 'react-icons/fa'; import { FaRegUser, FaList } from 'react-icons/fa';
import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
import Table from '../../components/table/Table';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {
availableFilters, import WithTopBarContainer from '../../layout/containers/WithTopBarContainer';
availableSorts, import Table from '../../components/table/Table';
usePeopleColumns,
} from './people-table';
import { import {
Person, Person,
mapToPerson, mapToPerson,
} from '../../interfaces/entities/person.interface'; } from '../../interfaces/entities/person.interface';
import { useCallback, useEffect, useRef, useState } from 'react';
import { import {
PeopleSelectedSortType, PeopleSelectedSortType,
defaultOrderBy, defaultOrderBy,
@ -20,14 +17,16 @@ import {
insertPerson, insertPerson,
usePeopleQuery, usePeopleQuery,
} from '../../services/api/people'; } from '../../services/api/people';
import { useSearch } from '../../services/api/search/search';
import { People_Bool_Exp } from '../../generated/graphql';
import { import {
reduceFiltersToWhere, reduceFiltersToWhere,
reduceSortsToOrderBy, reduceSortsToOrderBy,
} from '../../components/table/table-header/helpers'; } from '../../components/table/table-header/helpers';
import ActionBar from '../../components/table/action-bar/ActionBar'; import ActionBar from '../../components/table/action-bar/ActionBar';
import { SelectedFilterType } from '../../interfaces/filters/interface'; import { SelectedFilterType } from '../../interfaces/filters/interface';
import { BoolExpType } from '../../interfaces/entities/generic.interface';
import { usePeopleColumns } from './people-columns';
import { availableSorts } from './people-sorts';
import { availableFilters } from './people-filters';
const StyledPeopleContainer = styled.div` const StyledPeopleContainer = styled.div`
display: flex; display: flex;
@ -37,8 +36,7 @@ const StyledPeopleContainer = styled.div`
function People() { function People() {
const [orderBy, setOrderBy] = useState(defaultOrderBy); const [orderBy, setOrderBy] = useState(defaultOrderBy);
const [where, setWhere] = useState<People_Bool_Exp>({}); const [where, setWhere] = useState<BoolExpType<Person>>({});
const [filterSearchResults, setSearchInput, setFilterSearch] = useSearch();
const [internalData, setInternalData] = useState<Array<Person>>([]); const [internalData, setInternalData] = useState<Array<Person>>([]);
const [selectedRowIds, setSelectedRowIds] = useState<Array<string>>([]); const [selectedRowIds, setSelectedRowIds] = useState<Array<string>>([]);
@ -111,13 +109,8 @@ function People() {
viewIcon={<FaList />} viewIcon={<FaList />}
availableSorts={availableSorts} availableSorts={availableSorts}
availableFilters={availableFilters} availableFilters={availableFilters}
filterSearchResults={filterSearchResults}
onSortsUpdate={updateSorts} onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters} onFiltersUpdate={updateFilters}
onFilterSearch={(filter, searchValue) => {
setSearchInput(searchValue);
setFilterSearch(filter);
}}
onRowSelectionChange={setSelectedRowIds} onRowSelectionChange={setSelectedRowIds}
/> />
</StyledPeopleContainer> </StyledPeopleContainer>

View File

@ -1,4 +1,4 @@
import { cityFilter } from '../people-table'; import { cityFilter } from '../people-filters';
describe('PeopleFilter', () => { describe('PeopleFilter', () => {
it(`should render the filter ${cityFilter.key}`, () => { it(`should render the filter ${cityFilter.key}`, () => {
@ -11,8 +11,9 @@ describe('PeopleFilter', () => {
lastname: 'Doe', lastname: 'Doe',
phone: '0123456789', phone: '0123456789',
creationDate: new Date(), creationDate: new Date(),
pipe: null, pipes: [],
company: null, company: null,
__typename: 'people',
}), }),
).toMatchSnapshot(); ).toMatchSnapshot();
}); });

View File

@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { import {
FaRegBuilding, FaRegBuilding,
FaCalendar, FaCalendar,
@ -5,233 +6,30 @@ import {
FaRegUser, FaRegUser,
FaMapPin, FaMapPin,
FaPhone, FaPhone,
FaUser,
FaBuilding,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { CellContext, createColumnHelper } from '@tanstack/react-table'; import { CellContext, createColumnHelper } from '@tanstack/react-table';
import ColumnHead from '../../components/table/ColumnHead';
import Checkbox from '../../components/form/Checkbox'; import { SEARCH_COMPANY_QUERY } from '../../services/api/search/search';
import CompanyChip, { import { SearchConfigType } from '../../interfaces/search/interface';
CompanyChipPropsType,
} from '../../components/chips/CompanyChip';
import {
Person,
mapToPerson,
} from '../../interfaces/entities/person.interface';
import EditableText from '../../components/editable-cell/EditableText';
import { Order_By, People_Order_By } from '../../generated/graphql';
import {
SEARCH_COMPANY_QUERY,
SEARCH_PEOPLE_QUERY,
} from '../../services/api/search/search';
import { import {
Company, Company,
mapToCompany, mapToCompany,
} from '../../interfaces/entities/company.interface'; } from '../../interfaces/entities/company.interface';
import { Person } from '../../interfaces/entities/person.interface';
import { updatePerson } from '../../services/api/people';
import ColumnHead from '../../components/table/ColumnHead';
import Checkbox from '../../components/form/Checkbox';
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox';
import EditablePhone from '../../components/editable-cell/EditablePhone'; import EditablePhone from '../../components/editable-cell/EditablePhone';
import EditableFullName from '../../components/editable-cell/EditableFullName'; import EditableFullName from '../../components/editable-cell/EditableFullName';
import EditableDate from '../../components/editable-cell/EditableDate'; import EditableDate from '../../components/editable-cell/EditableDate';
import EditableText from '../../components/editable-cell/EditableText';
import EditableRelation from '../../components/editable-cell/EditableRelation'; import EditableRelation from '../../components/editable-cell/EditableRelation';
import { updatePerson } from '../../services/api/people'; import CompanyChip, {
import { useMemo } from 'react'; CompanyChipPropsType,
import { SelectAllCheckbox } from '../../components/table/SelectAllCheckbox'; } from '../../components/chips/CompanyChip';
import { SortType } from '../../interfaces/sorts/interface';
import { FilterConfigType } from '../../interfaces/filters/interface';
import { SearchConfigType } from '../../interfaces/search/interface';
export const availableSorts = [
{
key: 'fullname',
label: 'People',
icon: <FaRegUser />,
_type: 'custom_sort',
orderByTemplate: (order: Order_By) => ({
firstname: order,
lastname: order,
}),
},
{
key: 'company_name',
label: 'Company',
icon: <FaRegBuilding />,
_type: 'custom_sort',
orderByTemplate: (order: Order_By) => ({ company: { name: order } }),
},
{
key: 'email',
label: 'Email',
icon: <FaEnvelope />,
_type: 'default_sort',
},
{
key: 'phone',
label: 'Phone',
icon: <FaPhone />,
_type: 'default_sort',
},
{
key: 'created_at',
label: 'Created at',
icon: <FaCalendar />,
_type: 'default_sort',
},
{
key: 'city',
label: 'City',
icon: <FaMapPin />,
_type: 'default_sort',
},
] satisfies Array<SortType<People_Order_By>>;
export const fullnameFilter = {
key: 'fullname',
label: 'People',
icon: <FaUser />,
searchConfig: {
query: SEARCH_PEOPLE_QUERY,
template: (searchInput: string) => ({
_or: [
{ firstname: { _ilike: `%${searchInput}%` } },
{ lastname: { _ilike: `%${searchInput}%` } },
],
}),
resultMapper: (person) => ({
render: (person) => `${person.firstname} ${person.lastname}`,
value: mapToPerson(person),
}),
},
selectedValueRender: (person) => `${person.firstname} ${person.lastname}`,
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
_and: [
{ firstname: { _eq: `${person.firstname}` } },
{ lastname: { _eq: `${person.lastname}` } },
],
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (person) => ({
_not: {
_and: [
{ firstname: { _eq: `${person.firstname}` } },
{ lastname: { _eq: `${person.lastname}` } },
],
},
}),
},
],
} satisfies FilterConfigType<Person, Person>;
export const companyFilter = {
key: 'company_name',
label: 'Company',
icon: <FaBuilding />,
searchConfig: {
query: SEARCH_COMPANY_QUERY,
template: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` },
}),
resultMapper: (data) => ({
value: mapToCompany(data),
render: (company) => company.name,
}),
},
selectedValueRender: (company) => company.name || '',
operands: [
{
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 FilterConfigType<Person, Company>;
export const emailFilter = {
key: 'email',
label: 'Email',
icon: <FaEnvelope />,
searchConfig: {
query: SEARCH_PEOPLE_QUERY,
template: (searchInput: string) => ({
email: { _ilike: `%${searchInput}%` },
}),
resultMapper: (person) => ({
render: (person) => person.email,
value: mapToPerson(person),
}),
},
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
email: { _eq: person.email },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (person) => ({
_not: { email: { _eq: person.email } },
}),
},
],
selectedValueRender: (person) => person.email || '',
} satisfies FilterConfigType<Person, Person>;
export const cityFilter = {
key: 'city',
label: 'City',
icon: <FaMapPin />,
searchConfig: {
query: SEARCH_PEOPLE_QUERY,
template: (searchInput: string) => ({
city: { _ilike: `%${searchInput}%` },
}),
resultMapper: (person) => ({
render: (person) => person.city,
value: mapToPerson(person),
}),
},
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
city: { _eq: person.city },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (person) => ({
_not: { city: { _eq: person.city } },
}),
},
],
selectedValueRender: (person) => person.email || '',
} satisfies FilterConfigType<Person, Person>;
export const availableFilters = [
fullnameFilter,
companyFilter,
emailFilter,
cityFilter,
] satisfies FilterConfigType<Person, any>[];
const columnHelper = createColumnHelper<Person>(); const columnHelper = createColumnHelper<Person>();

View File

@ -0,0 +1,164 @@
import { FaEnvelope, FaMapPin, FaUser, FaBuilding } from 'react-icons/fa';
import {
Person,
mapToPerson,
} from '../../interfaces/entities/person.interface';
import {
SEARCH_COMPANY_QUERY,
SEARCH_PEOPLE_QUERY,
} from '../../services/api/search/search';
import {
Company,
mapToCompany,
} from '../../interfaces/entities/company.interface';
import { FilterConfigType } from '../../interfaces/filters/interface';
export const fullnameFilter = {
key: 'fullname',
label: 'People',
icon: <FaUser />,
searchConfig: {
query: SEARCH_PEOPLE_QUERY,
template: (searchInput: string) => ({
_or: [
{ firstname: { _ilike: `%${searchInput}%` } },
{ lastname: { _ilike: `%${searchInput}%` } },
],
}),
resultMapper: (person) => ({
render: (person) => `${person.firstname} ${person.lastname}`,
value: mapToPerson(person),
}),
},
selectedValueRender: (person) => `${person.firstname} ${person.lastname}`,
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
_and: [
{ firstname: { _eq: `${person.firstname}` } },
{ lastname: { _eq: `${person.lastname}` } },
],
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (person) => ({
_not: {
_and: [
{ firstname: { _eq: `${person.firstname}` } },
{ lastname: { _eq: `${person.lastname}` } },
],
},
}),
},
],
} satisfies FilterConfigType<Person, Person>;
export const companyFilter = {
key: 'company_name',
label: 'Company',
icon: <FaBuilding />,
searchConfig: {
query: SEARCH_COMPANY_QUERY,
template: (searchInput: string) => ({
name: { _ilike: `%${searchInput}%` },
}),
resultMapper: (data) => ({
value: mapToCompany(data),
render: (company) => company.name,
}),
},
selectedValueRender: (company) => company.name || '',
operands: [
{
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 FilterConfigType<Person, Company>;
export const emailFilter = {
key: 'email',
label: 'Email',
icon: <FaEnvelope />,
searchConfig: {
query: SEARCH_PEOPLE_QUERY,
template: (searchInput: string) => ({
email: { _ilike: `%${searchInput}%` },
}),
resultMapper: (person) => ({
render: (person) => person.email,
value: mapToPerson(person),
}),
},
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
email: { _eq: person.email },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (person) => ({
_not: { email: { _eq: person.email } },
}),
},
],
selectedValueRender: (person) => person.email || '',
} satisfies FilterConfigType<Person, Person>;
export const cityFilter = {
key: 'city',
label: 'City',
icon: <FaMapPin />,
searchConfig: {
query: SEARCH_PEOPLE_QUERY,
template: (searchInput: string) => ({
city: { _ilike: `%${searchInput}%` },
}),
resultMapper: (person) => ({
render: (person) => person.city,
value: mapToPerson(person),
}),
},
operands: [
{
label: 'Equal',
id: 'equal',
whereTemplate: (person) => ({
city: { _eq: person.city },
}),
},
{
label: 'Not equal',
id: 'not-equal',
whereTemplate: (person) => ({
_not: { city: { _eq: person.city } },
}),
},
],
selectedValueRender: (person) => person.email || '',
} satisfies FilterConfigType<Person, Person>;
export const availableFilters = [
fullnameFilter,
companyFilter,
emailFilter,
cityFilter,
] satisfies FilterConfigType<Person, any>[];

View File

@ -0,0 +1,54 @@
import {
FaRegBuilding,
FaCalendar,
FaEnvelope,
FaRegUser,
FaMapPin,
FaPhone,
} from 'react-icons/fa';
import { Order_By, People_Order_By } from '../../generated/graphql';
import { SortType } from '../../interfaces/sorts/interface';
export const availableSorts = [
{
key: 'fullname',
label: 'People',
icon: <FaRegUser />,
_type: 'custom_sort',
orderByTemplate: (order: Order_By) => ({
firstname: order,
lastname: order,
}),
},
{
key: 'company_name',
label: 'Company',
icon: <FaRegBuilding />,
_type: 'custom_sort',
orderByTemplate: (order: Order_By) => ({ company: { name: order } }),
},
{
key: 'email',
label: 'Email',
icon: <FaEnvelope />,
_type: 'default_sort',
},
{
key: 'phone',
label: 'Phone',
icon: <FaPhone />,
_type: 'default_sort',
},
{
key: 'created_at',
label: 'Created at',
icon: <FaCalendar />,
_type: 'default_sort',
},
{
key: 'city',
label: 'City',
icon: <FaMapPin />,
_type: 'default_sort',
},
] satisfies Array<SortType<People_Order_By>>;

View File

@ -29,7 +29,7 @@ export const SEARCH_USER_QUERY = gql`
} }
`; `;
const EMPTY_QUERY = gql` export const EMPTY_QUERY = gql`
query EmptyQuery { query EmptyQuery {
_ _
} }
@ -58,14 +58,16 @@ const debounce = <FuncArgs extends any[]>(
}; };
}; };
export type SearchResultsType<T extends SearchableType> = {
results: {
render: (value: T) => string;
value: T;
}[];
loading: boolean;
};
export const useSearch = <T extends SearchableType>(): [ export const useSearch = <T extends SearchableType>(): [
{ SearchResultsType<T>,
results: {
render: (value: T) => string;
value: T;
}[];
loading: boolean;
},
React.Dispatch<React.SetStateAction<string>>, React.Dispatch<React.SetStateAction<string>>,
React.Dispatch<React.SetStateAction<SearchConfigType<T> | null>>, React.Dispatch<React.SetStateAction<SearchConfigType<T> | null>>,
] => { ] => {
@ -87,18 +89,15 @@ export const useSearch = <T extends SearchableType>(): [
); );
}, [searchConfig, searchInput]); }, [searchConfig, searchInput]);
const searchFilterQueryResults = useQuery( const searchQueryResults = useQuery(searchConfig?.query || EMPTY_QUERY, {
searchConfig?.query || EMPTY_QUERY, variables: {
{ where,
variables: { limit: 5,
where,
limit: 5,
},
skip: !searchConfig,
}, },
); skip: !searchConfig,
});
const searchFilterResults = useMemo<{ const searchResults = useMemo<{
results: { render: (value: T) => string; value: any }[]; results: { render: (value: T) => string; value: any }[];
loading: boolean; loading: boolean;
}>(() => { }>(() => {
@ -108,7 +107,7 @@ export const useSearch = <T extends SearchableType>(): [
results: [], results: [],
}; };
} }
if (searchFilterQueryResults.loading) { if (searchQueryResults.loading) {
return { return {
loading: true, loading: true,
results: [], results: [],
@ -116,11 +115,11 @@ export const useSearch = <T extends SearchableType>(): [
} }
return { return {
loading: false, loading: false,
results: searchFilterQueryResults.data.searchResults.map( results: searchQueryResults.data.searchResults.map(
searchConfig.resultMapper, searchConfig.resultMapper,
), ),
}; };
}, [searchConfig, searchFilterQueryResults]); }, [searchConfig, searchQueryResults]);
return [searchFilterResults, debouncedsetSearchInput, setSearchConfig]; return [searchResults, debouncedsetSearchInput, setSearchConfig];
}; };