Sammy/t 240 frontend filtering search is refactored (#122)

* refactor: use AnyEntity instead of any

* refactor: remove any and brand company type

* refactor: add typename for user and people

* bugfix: await company to be created before displaying it

* feature: await deletion before removing the lines

* refactor: remove default tyep for filters

* refactor: remove default type AnyEntity

* refactor: remove USers from filterable types

* refactor: do not depend on Filter types in Table

* Add tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Sammy Teillet
2023-05-17 21:49:34 +02:00
committed by GitHub
parent bc49815ff0
commit baca6150f5
25 changed files with 254 additions and 106 deletions

View File

@ -23,7 +23,10 @@ declare module 'react' {
): (props: P & React.RefAttributes<T>) => React.ReactElement | null; ): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
} }
type OwnProps<TData, SortField> = { type OwnProps<
TData extends { id: string; __typename: 'companies' | 'people' },
SortField,
> = {
data: Array<TData>; data: Array<TData>;
columns: Array<ColumnDef<TData, any>>; columns: Array<ColumnDef<TData, any>>;
viewName: string; viewName: string;
@ -38,7 +41,7 @@ type OwnProps<TData, SortField> = {
loading: boolean; loading: boolean;
}; };
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: (sorts: Array<SelectedFilterType>) => void; onFiltersUpdate?: (sorts: Array<SelectedFilterType<TData>>) => void;
onFilterSearch?: ( onFilterSearch?: (
filter: SearchConfigType<any> | null, filter: SearchConfigType<any> | null,
searchValue: string, searchValue: string,
@ -97,7 +100,10 @@ const StyledTableScrollableContainer = styled.div`
flex: 1; flex: 1;
`; `;
const Table = <TData extends { id: string }, SortField>( const Table = <
TData extends { id: string; __typename: 'companies' | 'people' },
SortField,
>(
{ {
data, data,
columns, columns,

View File

@ -72,6 +72,7 @@ const Template: StoryFn<
export const EditableRelationStory = Template.bind({}); export const EditableRelationStory = Template.bind({});
EditableRelationStory.args = { EditableRelationStory.args = {
relation: { relation: {
__typename: 'companies',
id: '123', id: '123',
name: 'Heroku', name: 'Heroku',
domain_name: 'heroku.com', domain_name: 'heroku.com',

View File

@ -50,6 +50,7 @@ it('Checks the EditableRelation editing event bubbles up', async () => {
await waitFor(() => { await waitFor(() => {
expect(func).toBeCalledWith({ expect(func).toBeCalledWith({
__typename: 'companies',
accountOwner: undefined, accountOwner: undefined,
address: undefined, address: undefined,
domainName: 'abnb.com', domainName: 'abnb.com',

View File

@ -3,14 +3,15 @@ import DropdownButton from './DropdownButton';
import { import {
FilterConfigType, FilterConfigType,
FilterOperandType, FilterOperandType,
FilterableFieldsType,
SearchConfigType, SearchConfigType,
SearchableType, SearchableType,
SelectedFilterType, SelectedFilterType,
} from './interface'; } from './interface';
type OwnProps = { type OwnProps<TData extends FilterableFieldsType> = {
isFilterSelected: boolean; isFilterSelected: boolean;
availableFilters: FilterConfigType[]; availableFilters: FilterConfigType<TData>[];
filterSearchResults?: { filterSearchResults?: {
results: { results: {
render: (value: SearchableType) => string; render: (value: SearchableType) => string;
@ -18,30 +19,30 @@ type OwnProps = {
}[]; }[];
loading: boolean; loading: boolean;
}; };
onFilterSelect: (filter: SelectedFilterType) => void; onFilterSelect: (filter: SelectedFilterType<TData>) => void;
onFilterSearch: ( onFilterSearch: (
filter: SearchConfigType<any> | null, filter: SearchConfigType<any> | null,
searchValue: string, searchValue: string,
) => void; ) => void;
}; };
export function FilterDropdownButton({ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
availableFilters, availableFilters,
filterSearchResults, filterSearchResults,
onFilterSearch, onFilterSearch,
onFilterSelect, onFilterSelect,
isFilterSelected, isFilterSelected,
}: OwnProps) { }: OwnProps<TData>) => {
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false); const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedFilter, setSelectedFilter] = useState< const [selectedFilter, setSelectedFilter] = useState<
FilterConfigType | undefined FilterConfigType<TData> | undefined
>(undefined); >(undefined);
const [selectedFilterOperand, setSelectedFilterOperand] = useState< const [selectedFilterOperand, setSelectedFilterOperand] = useState<
FilterOperandType | undefined FilterOperandType<TData> | undefined
>(undefined); >(undefined);
const resetState = useCallback(() => { const resetState = useCallback(() => {
@ -66,9 +67,9 @@ export function FilterDropdownButton({
); );
const renderSearchResults = ( const renderSearchResults = (
filterSearchResults: NonNullable<OwnProps['filterSearchResults']>, filterSearchResults: NonNullable<OwnProps<TData>['filterSearchResults']>,
selectedFilter: FilterConfigType, selectedFilter: FilterConfigType<TData>,
selectedFilterOperand: FilterOperandType, selectedFilterOperand: FilterOperandType<TData>,
) => { ) => {
if (filterSearchResults.loading) { if (filterSearchResults.loading) {
return ( return (
@ -114,8 +115,8 @@ export function FilterDropdownButton({
)); ));
function renderFilterDropdown( function renderFilterDropdown(
selectedFilter: FilterConfigType, selectedFilter: FilterConfigType<TData>,
selectedFilterOperand: FilterOperandType, selectedFilterOperand: FilterOperandType<TData>,
) { ) {
return ( return (
<> <>
@ -161,4 +162,4 @@ export function FilterDropdownButton({
: renderSelectFilterITems} : renderSelectFilterITems}
</DropdownButton> </DropdownButton>
); );
} };

View File

@ -1,13 +1,17 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import SortOrFilterChip from './SortOrFilterChip'; import SortOrFilterChip from './SortOrFilterChip';
import { FaArrowDown, FaArrowUp } from 'react-icons/fa'; import { FaArrowDown, FaArrowUp } from 'react-icons/fa';
import { SelectedFilterType, SelectedSortType } from './interface'; import {
FilterableFieldsType,
SelectedFilterType,
SelectedSortType,
} from './interface';
type OwnProps<SortField> = { type OwnProps<SortField, TData extends FilterableFieldsType> = {
sorts: Array<SelectedSortType<SortField>>; sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void; onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
filters: Array<SelectedFilterType>; filters: Array<SelectedFilterType<TData>>;
onRemoveFilter: (filterId: SelectedFilterType['key']) => void; onRemoveFilter: (filterId: SelectedFilterType<TData>['key']) => void;
onCancelClick: () => void; onCancelClick: () => void;
}; };
@ -40,13 +44,13 @@ const StyledCancelButton = styled.button`
} }
`; `;
function SortAndFilterBar<SortField>({ function SortAndFilterBar<SortField, TData extends FilterableFieldsType>({
sorts, sorts,
onRemoveSort, onRemoveSort,
filters, filters,
onRemoveFilter, onRemoveFilter,
onCancelClick, onCancelClick,
}: OwnProps<SortField>) { }: OwnProps<SortField, TData>) {
return ( return (
<StyledBar> <StyledBar>
{sorts.map((sort) => { {sorts.map((sort) => {

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { import {
FilterConfigType, FilterConfigType,
FilterableFieldsType,
SearchConfigType, SearchConfigType,
SearchableType, SearchableType,
SelectedFilterType, SelectedFilterType,
@ -12,11 +13,11 @@ import { SortDropdownButton } from './SortDropdownButton';
import { FilterDropdownButton } from './FilterDropdownButton'; import { FilterDropdownButton } from './FilterDropdownButton';
import SortAndFilterBar from './SortAndFilterBar'; import SortAndFilterBar from './SortAndFilterBar';
type OwnProps<SortField> = { type OwnProps<SortField, TData extends FilterableFieldsType> = {
viewName: string; viewName: string;
viewIcon?: ReactNode; viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<SortField>>;
availableFilters?: FilterConfigType[]; availableFilters?: FilterConfigType<TData>[];
filterSearchResults?: { filterSearchResults?: {
results: { results: {
render: (value: SearchableType) => string; render: (value: SearchableType) => string;
@ -25,7 +26,7 @@ type OwnProps<SortField> = {
loading: boolean; loading: boolean;
}; };
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onFiltersUpdate?: (sorts: Array<SelectedFilterType>) => void; onFiltersUpdate?: (sorts: Array<SelectedFilterType<TData>>) => void;
onFilterSearch?: ( onFilterSearch?: (
filter: SearchConfigType<any> | null, filter: SearchConfigType<any> | null,
searchValue: string, searchValue: string,
@ -68,7 +69,7 @@ const StyledFilters = styled.div`
margin-right: ${(props) => props.theme.spacing(2)}; margin-right: ${(props) => props.theme.spacing(2)};
`; `;
function TableHeader<SortField>({ function TableHeader<SortField, TData extends FilterableFieldsType>({
viewName, viewName,
viewIcon, viewIcon,
availableSorts, availableSorts,
@ -77,11 +78,13 @@ function TableHeader<SortField>({
onSortsUpdate, onSortsUpdate,
onFiltersUpdate, onFiltersUpdate,
onFilterSearch, onFilterSearch,
}: OwnProps<SortField>) { }: OwnProps<SortField, TData>) {
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>( const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
[], [],
); );
const [filters, innerSetFilters] = useState<Array<SelectedFilterType>>([]); const [filters, innerSetFilters] = useState<Array<SelectedFilterType<TData>>>(
[],
);
const sortSelect = useCallback( const sortSelect = useCallback(
(newSort: SelectedSortType<SortField>) => { (newSort: SelectedSortType<SortField>) => {
@ -102,7 +105,7 @@ function TableHeader<SortField>({
); );
const filterSelect = useCallback( const filterSelect = useCallback(
(filter: SelectedFilterType) => { (filter: SelectedFilterType<TData>) => {
const newFilters = updateSortOrFilterByKey(filters, filter); const newFilters = updateSortOrFilterByKey(filters, filter);
innerSetFilters(newFilters); innerSetFilters(newFilters);
@ -112,7 +115,7 @@ function TableHeader<SortField>({
); );
const filterUnselect = useCallback( const filterUnselect = useCallback(
(filterId: SelectedFilterType['key']) => { (filterId: SelectedFilterType<TData>['key']) => {
const newFilters = filters.filter((filter) => filter.key !== filterId); const newFilters = filters.filter((filter) => filter.key !== filterId);
innerSetFilters(newFilters); innerSetFilters(newFilters);
onFiltersUpdate && onFiltersUpdate(newFilters); onFiltersUpdate && onFiltersUpdate(newFilters);

View File

@ -2,7 +2,7 @@ import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes'; 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 { FilterConfigType, SelectedFilterType } from '../interface'; import { FilterableFieldsType, SelectedFilterType } from '../interface';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { import {
SEARCH_PEOPLE_QUERY, SEARCH_PEOPLE_QUERY,
@ -20,7 +20,7 @@ const component = {
export default component; export default component;
type OwnProps<FilterProperties> = { type OwnProps<FilterProperties extends FilterableFieldsType> = {
setFilter: (filters: SelectedFilterType<FilterProperties>) => void; setFilter: (filters: SelectedFilterType<FilterProperties>) => void;
}; };
@ -98,8 +98,8 @@ const InnerRegularFilterDropdownButton = ({
); );
return ( return (
<StyleDiv> <StyleDiv>
<FilterDropdownButton <FilterDropdownButton<Person>
availableFilters={availableFilters as FilterConfigType[]} availableFilters={availableFilters}
isFilterSelected={true} isFilterSelected={true}
onFilterSelect={outerSetFilters} onFilterSelect={outerSetFilters}
filterSearchResults={filterSearchResults} filterSearchResults={filterSearchResults}

View File

@ -3,6 +3,7 @@ import { ThemeProvider } from '@emotion/react';
import { lightTheme } from '../../../../layout/styles/themes'; import { lightTheme } from '../../../../layout/styles/themes';
import { FaArrowDown } from 'react-icons/fa'; import { FaArrowDown } from 'react-icons/fa';
import { SelectedFilterType } from '../interface'; import { SelectedFilterType } from '../interface';
import { Person } from '../../../../interfaces/person.interface';
const component = { const component = {
title: 'SortAndFilterBar', title: 'SortAndFilterBar',
@ -20,6 +21,31 @@ export const RegularSortAndFilterBar = ({
removeFunction, removeFunction,
cancelFunction, cancelFunction,
}: OwnProps) => { }: OwnProps) => {
const personFilter = {
label: 'People',
operand: {
label: 'Include',
id: 'include',
whereTemplate: (person: Person) => {
return { email: { _eq: person.email } };
},
},
key: 'test_filter',
icon: <FaArrowDown />,
displayValue: 'john@doedoe.com',
value: {
__typename: 'people',
id: 'test',
email: 'john@doedoe.com',
firstname: 'John',
lastname: 'Doe',
phone: '123456789',
company: null,
creationDate: new Date(),
pipes: null,
city: 'Paris',
},
} satisfies SelectedFilterType<Person, Person>;
return ( return (
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<SortAndFilterBar <SortAndFilterBar
@ -42,32 +68,7 @@ export const RegularSortAndFilterBar = ({
onRemoveSort={removeFunction} onRemoveSort={removeFunction}
onRemoveFilter={removeFunction} onRemoveFilter={removeFunction}
onCancelClick={cancelFunction} onCancelClick={cancelFunction}
filters={[ filters={[personFilter] as SelectedFilterType<Person>[]}
{
label: 'People',
operand: {
label: 'Include',
id: 'include',
whereTemplate: (person) => {
return { email: { _eq: person.email } };
},
},
key: 'test_filter',
icon: <FaArrowDown />,
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',
},
} satisfies SelectedFilterType,
]}
/> />
</ThemeProvider> </ThemeProvider>
); );

View File

@ -1,7 +1,16 @@
import { Order_By } from '../../../generated/graphql'; import { Order_By } from '../../../generated/graphql';
import { BoolExpType, SelectedFilterType, SelectedSortType } from './interface'; import {
BoolExpType,
FilterWhereType,
FilterableFieldsType,
SelectedFilterType,
SelectedSortType,
} from './interface';
export const reduceFiltersToWhere = <ValueType, WhereTemplateType>( export const reduceFiltersToWhere = <
ValueType extends FilterableFieldsType,
WhereTemplateType extends FilterWhereType,
>(
filters: Array<SelectedFilterType<ValueType, WhereTemplateType>>, filters: Array<SelectedFilterType<ValueType, WhereTemplateType>>,
): BoolExpType<WhereTemplateType> => { ): BoolExpType<WhereTemplateType> => {
const where = filters.reduce((acc, filter) => { const where = filters.reduce((acc, filter) => {

View File

@ -35,8 +35,13 @@ export type SelectedSortType<OrderByTemplate> = SortType<OrderByTemplate> & {
order: 'asc' | 'desc'; order: 'asc' | 'desc';
}; };
type AnyEntity = {
id: string;
__typename: string;
} & Record<string, any>;
export type FilterableFieldsType = Person | Company; export type FilterableFieldsType = Person | Company;
export type FilterWhereType = Person | Company | User; export type FilterWhereType = Person | Company | User | AnyEntity;
type FilterConfigGqlType<WhereType> = WhereType extends Company type FilterConfigGqlType<WhereType> = WhereType extends Company
? GraphqlQueryCompany ? GraphqlQueryCompany
@ -50,9 +55,14 @@ export type BoolExpType<T> = T extends Company
? Companies_Bool_Exp ? Companies_Bool_Exp
: T extends Person : T extends Person
? People_Bool_Exp ? People_Bool_Exp
: T extends User
? Users_Bool_Exp
: never; : never;
export type FilterConfigType<FilteredType = any, WhereType = any> = { export type FilterConfigType<
FilteredType extends FilterableFieldsType,
WhereType extends FilterWhereType = any,
> = {
key: string; key: string;
label: string; label: string;
icon: ReactNode; icon: ReactNode;
@ -77,17 +87,33 @@ export type SearchConfigType<SearchType extends SearchableType> = {
}; };
export type FilterOperandType< export type FilterOperandType<
FilteredType = FilterableFieldsType, FilteredType extends FilterableFieldsType,
WhereType = any, WhereType extends FilterWhereType = AnyEntity,
> =
| FilterOperandExactMatchType<FilteredType, WhereType>
| FilterOperandComparativeType<FilteredType, WhereType>;
type FilterOperandExactMatchType<
FilteredType extends FilterableFieldsType,
WhereType extends FilterWhereType,
> = { > = {
label: string; label: 'Equal' | 'Not equal';
id: string; id: 'equal' | 'not-equal';
whereTemplate: (value: WhereType) => BoolExpType<FilteredType>;
};
type FilterOperandComparativeType<
FilteredType extends FilterableFieldsType,
WhereType extends FilterWhereType,
> = {
label: 'Like' | 'Not like' | 'Include';
id: 'like' | 'not_like' | 'include';
whereTemplate: (value: WhereType) => BoolExpType<FilteredType>; whereTemplate: (value: WhereType) => BoolExpType<FilteredType>;
}; };
export type SelectedFilterType< export type SelectedFilterType<
FilteredType = FilterableFieldsType, FilteredType extends FilterableFieldsType,
WhereType = any, WhereType extends FilterWhereType = AnyEntity,
> = { > = {
key: string; key: string;
value: WhereType; value: WhereType;

View File

@ -36,6 +36,7 @@ describe('Company mappers', () => {
const company = mapToCompany(graphQLCompany); const company = mapToCompany(graphQLCompany);
expect(company).toStrictEqual({ expect(company).toStrictEqual({
__typename: 'companies',
id: graphQLCompany.id, id: graphQLCompany.id,
name: graphQLCompany.name, name: graphQLCompany.name,
domainName: graphQLCompany.domain_name, domainName: graphQLCompany.domain_name,
@ -43,6 +44,7 @@ describe('Company mappers', () => {
employees: graphQLCompany.employees, employees: graphQLCompany.employees,
address: graphQLCompany.address, address: graphQLCompany.address,
accountOwner: { accountOwner: {
__typename: 'users',
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87', id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
email: 'john@example.com', email: 'john@example.com',
displayName: 'John Doe', displayName: 'John Doe',
@ -66,9 +68,11 @@ describe('Company mappers', () => {
id: '522d4ec4-c46b-4360-a0a7-df8df170be81', id: '522d4ec4-c46b-4360-a0a7-df8df170be81',
email: 'john@example.com', email: 'john@example.com',
displayName: 'John Doe', displayName: 'John Doe',
__typename: 'users',
}, },
creationDate: now, creationDate: now,
}; __typename: 'companies',
} satisfies Company;
const graphQLCompany = mapToGqlCompany(company); const graphQLCompany = mapToGqlCompany(company);
expect(graphQLCompany).toStrictEqual({ expect(graphQLCompany).toStrictEqual({
id: company.id, id: company.id,

View File

@ -28,6 +28,7 @@ describe('Person mappers', () => {
const person = mapToPerson(graphQLPerson); const person = mapToPerson(graphQLPerson);
expect(person).toStrictEqual({ expect(person).toStrictEqual({
__typename: 'people',
id: graphQLPerson.id, id: graphQLPerson.id,
firstname: graphQLPerson.firstname, firstname: graphQLPerson.firstname,
lastname: graphQLPerson.lastname, lastname: graphQLPerson.lastname,
@ -36,6 +37,7 @@ describe('Person mappers', () => {
city: graphQLPerson.city, city: graphQLPerson.city,
phone: graphQLPerson.phone, phone: graphQLPerson.phone,
company: { company: {
__typename: 'companies',
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87', id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
accountOwner: undefined, accountOwner: undefined,
address: undefined, address: undefined,

View File

@ -28,6 +28,7 @@ describe('User mappers', () => {
const User = mapToUser(graphQLUser); const User = mapToUser(graphQLUser);
expect(User).toStrictEqual({ expect(User).toStrictEqual({
__typename: 'users',
id: graphQLUser.id, id: graphQLUser.id,
displayName: graphQLUser.display_name, displayName: graphQLUser.display_name,
email: graphQLUser.email, email: graphQLUser.email,
@ -47,6 +48,7 @@ describe('User mappers', () => {
const now = new Date(); const now = new Date();
now.setMilliseconds(0); now.setMilliseconds(0);
const user = { const user = {
__typename: 'users',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
displayName: 'John Doe', displayName: 'John Doe',
email: 'john.doe@gmail.com', email: 'john.doe@gmail.com',

View File

@ -3,6 +3,7 @@ import { GraphqlQueryUser, User, mapToUser } from './user.interface';
import { GraphqlQueryPipe } from './pipe.interface'; import { GraphqlQueryPipe } from './pipe.interface';
export type Company = { export type Company = {
__typename: 'companies';
id: string; id: string;
name?: string; name?: string;
domainName?: string; domainName?: string;
@ -43,6 +44,7 @@ export type GraphqlMutationCompany = {
}; };
export const mapToCompany = (company: GraphqlQueryCompany): Company => ({ export const mapToCompany = (company: GraphqlQueryCompany): Company => ({
__typename: 'companies',
id: company.id, id: company.id,
employees: company.employees, employees: company.employees,
name: company.name, name: company.name,

View File

@ -6,6 +6,7 @@ import {
import { Pipe } from './pipe.interface'; import { Pipe } from './pipe.interface';
export type Person = { export type Person = {
__typename: 'people';
id: string; id: string;
firstname?: string; firstname?: string;
lastname?: string; lastname?: string;
@ -44,10 +45,11 @@ export type GraphqlMutationPerson = {
city?: string; city?: string;
created_at?: string; created_at?: string;
company_id?: string; company_id?: string;
__typename: string; __typename: 'people';
}; };
export const mapToPerson = (person: GraphqlQueryPerson): Person => ({ export const mapToPerson = (person: GraphqlQueryPerson): Person => ({
__typename: 'people',
id: person.id, id: person.id,
firstname: person.firstname, firstname: person.firstname,
lastname: person.lastname, lastname: person.lastname,

View File

@ -5,6 +5,7 @@ import {
} from './workspace_member.interface'; } from './workspace_member.interface';
export interface User { export interface User {
__typename: 'users';
id: string; id: string;
email?: string; email?: string;
displayName?: string; displayName?: string;
@ -28,6 +29,7 @@ export type GraphqlMutationUser = {
}; };
export const mapToUser = (user: GraphqlQueryUser): User => ({ export const mapToUser = (user: GraphqlQueryUser): User => ({
__typename: 'users',
id: user.id, id: user.id,
email: user.email, email: user.email,
displayName: user.display_name, displayName: user.display_name,

View File

@ -17,6 +17,7 @@ export const NavbarOnCompanies = () => {
<MemoryRouter initialEntries={['/companies']}> <MemoryRouter initialEntries={['/companies']}>
<Navbar <Navbar
user={{ user={{
__typename: 'users',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
email: 'charles@twenty.com', email: 'charles@twenty.com',
displayName: 'Charles Bochet', displayName: 'Charles Bochet',

View File

@ -25,10 +25,7 @@ import {
Companies_Bool_Exp, Companies_Bool_Exp,
Companies_Order_By, Companies_Order_By,
} from '../../generated/graphql'; } from '../../generated/graphql';
import { import { SelectedFilterType } from '../../components/table/table-header/interface';
FilterConfigType,
SelectedFilterType,
} from '../../components/table/table-header/interface';
import { useSearch } from '../../services/search/search'; import { useSearch } from '../../services/search/search';
import ActionBar from '../../components/table/action-bar/ActionBar'; import ActionBar from '../../components/table/action-bar/ActionBar';
@ -66,7 +63,7 @@ function Companies() {
} }
}, [loading, setInternalData, data]); }, [loading, setInternalData, data]);
const addEmptyRow = useCallback(() => { const addEmptyRow = useCallback(async () => {
const newCompany: Company = { const newCompany: Company = {
id: uuidv4(), id: uuidv4(),
name: '', name: '',
@ -76,14 +73,15 @@ function Companies() {
pipes: [], pipes: [],
creationDate: new Date(), creationDate: new Date(),
accountOwner: null, accountOwner: null,
__typename: 'companies',
}; };
insertCompany(newCompany); await insertCompany(newCompany);
setInternalData([newCompany, ...internalData]); setInternalData([newCompany, ...internalData]);
refetch(); refetch();
}, [internalData, setInternalData, refetch]); }, [internalData, setInternalData, refetch]);
const deleteRows = useCallback(() => { const deleteRows = useCallback(async () => {
deleteCompanies(selectedRowIds); await deleteCompanies(selectedRowIds);
setInternalData([ setInternalData([
...internalData.filter((row) => !selectedRowIds.includes(row.id)), ...internalData.filter((row) => !selectedRowIds.includes(row.id)),
]); ]);
@ -111,7 +109,7 @@ function Companies() {
viewName="All Companies" viewName="All Companies"
viewIcon={<FaList />} viewIcon={<FaList />}
availableSorts={availableSorts} availableSorts={availableSorts}
availableFilters={availableFilters as Array<FilterConfigType>} availableFilters={availableFilters}
filterSearchResults={filterSearchResults} filterSearchResults={filterSearchResults}
onSortsUpdate={updateSorts} onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters} onFiltersUpdate={updateFilters}

View File

@ -5,6 +5,8 @@ import { lightTheme } from '../../../layout/styles/themes';
import { GET_COMPANIES } from '../../../services/companies'; import { GET_COMPANIES } from '../../../services/companies';
import { mockData } from '../__tests__/__data__/mock-data'; import { mockData } from '../__tests__/__data__/mock-data';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { SEARCH_COMPANY_QUERY } from '../../../services/search/search';
import { mockCompanySearchData } from '../../../services/search/__data__/mock-search-data';
const component = { const component = {
title: 'Companies', title: 'Companies',
@ -42,6 +44,27 @@ const mocks = [
}, },
}, },
}, },
{
request: {
query: SEARCH_COMPANY_QUERY,
variables: { where: { name: { _ilike: '%%' } }, limit: 5 },
},
result: mockCompanySearchData,
},
{
request: {
query: GET_COMPANIES,
variables: {
orderBy: [{ created_at: 'desc' }],
where: { domain_name: { _eq: 'linkedin-searched.com' } },
},
},
result: {
data: {
companies: mockData,
},
},
},
]; ];
export const CompaniesDefault = () => ( export const CompaniesDefault = () => (

View File

@ -136,3 +136,28 @@ it('Checks insert data is appending a new line', async () => {
expect(tableRows.length).toBe(7); expect(tableRows.length).toBe(7);
}); });
}); });
it('Checks filters are working', async () => {
const { getByText } = render(<CompaniesDefault />);
await waitFor(() => {
expect(getByText('Airbnb')).toBeDefined();
});
const filterDropdown = getByText('Filter');
fireEvent.click(filterDropdown);
await waitFor(() => {
expect(getByText('Url')).toBeDefined();
});
const urlFilter = getByText('Url');
fireEvent.click(urlFilter);
await waitFor(() => {
expect(getByText('linkedin-searched.com')).toBeDefined();
});
const filterByLinkedinOption = getByText('linkedin-searched.com');
fireEvent.click(filterByLinkedinOption);
});

View File

@ -7,7 +7,7 @@ export const mockData: Array<GraphqlQueryCompany> = [
name: 'Airbnb', name: 'Airbnb',
created_at: '2023-04-26T10:08:54.724515+00:00', created_at: '2023-04-26T10:08:54.724515+00:00',
address: '17 rue de clignancourt', address: '17 rue de clignancourt',
employees: 12, employees: '12',
account_owner: null, account_owner: null,
__typename: 'companies', __typename: 'companies',
}, },
@ -16,8 +16,8 @@ export const mockData: Array<GraphqlQueryCompany> = [
domain_name: 'aircall.io', domain_name: 'aircall.io',
name: 'Aircall', name: 'Aircall',
created_at: '2023-04-26T10:12:42.33625+00:00', created_at: '2023-04-26T10:12:42.33625+00:00',
address: null, address: '',
employees: 1, employees: '1',
account_owner: null, account_owner: null,
__typename: 'companies', __typename: 'companies',
}, },
@ -26,8 +26,8 @@ export const mockData: Array<GraphqlQueryCompany> = [
domain_name: 'algolia.com', domain_name: 'algolia.com',
name: 'Algolia', name: 'Algolia',
created_at: '2023-04-26T10:10:32.530184+00:00', created_at: '2023-04-26T10:10:32.530184+00:00',
address: null, address: '',
employees: 1, employees: '1',
account_owner: null, account_owner: null,
__typename: 'companies', __typename: 'companies',
}, },
@ -36,8 +36,8 @@ export const mockData: Array<GraphqlQueryCompany> = [
domain_name: 'apple.com', domain_name: 'apple.com',
name: 'Apple', name: 'Apple',
created_at: '2023-03-21T06:30:25.39474+00:00', created_at: '2023-03-21T06:30:25.39474+00:00',
address: null, address: '',
employees: 10, employees: '10',
account_owner: null, account_owner: null,
__typename: 'companies', __typename: 'companies',
}, },
@ -47,7 +47,7 @@ export const mockData: Array<GraphqlQueryCompany> = [
name: 'BeReal', name: 'BeReal',
created_at: '2023-04-26T10:13:29.712485+00:00', created_at: '2023-04-26T10:13:29.712485+00:00',
address: '10 rue de la Paix', address: '10 rue de la Paix',
employees: 1, employees: '1',
account_owner: null, account_owner: null,
__typename: 'companies', __typename: 'companies',
}, },
@ -56,8 +56,8 @@ export const mockData: Array<GraphqlQueryCompany> = [
domain_name: 'claap.com', domain_name: 'claap.com',
name: 'Claap', name: 'Claap',
created_at: '2023-04-26T10:09:25.656555+00:00', created_at: '2023-04-26T10:09:25.656555+00:00',
address: null, address: '',
employees: 1, employees: '1',
account_owner: null, account_owner: null,
__typename: 'companies', __typename: 'companies',
}, },

View File

@ -82,7 +82,7 @@ export const availableFilters = [
value: mapToCompany(company), value: mapToCompany(company),
}), }),
}, },
selectedValueRender: (company) => company.name, selectedValueRender: (company) => company.name || '',
operands: [ operands: [
{ {
label: 'Equal', label: 'Equal',
@ -99,7 +99,7 @@ export const availableFilters = [
}), }),
}, },
], ],
} as FilterConfigType<Company, Company>, } satisfies FilterConfigType<Company, Company>,
{ {
key: 'company_domain_name', key: 'company_domain_name',
label: 'Url', label: 'Url',
@ -114,7 +114,7 @@ export const availableFilters = [
value: mapToCompany(company), value: mapToCompany(company),
}), }),
}, },
selectedValueRender: (company) => company.domainName, selectedValueRender: (company) => company.domainName || '',
operands: [ operands: [
{ {
label: 'Equal', label: 'Equal',
@ -131,7 +131,7 @@ export const availableFilters = [
}), }),
}, },
], ],
} as FilterConfigType<Company, Company>, } satisfies FilterConfigType<Company, Company>,
]; ];
const columnHelper = createColumnHelper<Company>(); const columnHelper = createColumnHelper<Company>();
@ -253,6 +253,7 @@ export const useCompaniesColumns = () => {
company.accountOwner.id = relation.id; company.accountOwner.id = relation.id;
} else { } else {
company.accountOwner = { company.accountOwner = {
__typename: 'users',
id: relation.id, id: relation.id,
email: relation.email, email: relation.email,
displayName: relation.displayName, displayName: relation.displayName,

View File

@ -19,10 +19,7 @@ import {
} from '../../services/people'; } from '../../services/people';
import { useSearch } from '../../services/search/search'; import { useSearch } from '../../services/search/search';
import { People_Bool_Exp } from '../../generated/graphql'; import { People_Bool_Exp } from '../../generated/graphql';
import { import { SelectedFilterType } from '../../components/table/table-header/interface';
FilterConfigType,
SelectedFilterType,
} from '../../components/table/table-header/interface';
import { import {
reduceFiltersToWhere, reduceFiltersToWhere,
reduceSortsToOrderBy, reduceSortsToOrderBy,
@ -64,7 +61,8 @@ function People() {
}, [loading, setInternalData, data]); }, [loading, setInternalData, data]);
const addEmptyRow = useCallback(async () => { const addEmptyRow = useCallback(async () => {
const newCompany: Person = { const newPerson: Person = {
__typename: 'people',
id: uuidv4(), id: uuidv4(),
firstname: '', firstname: '',
lastname: '', lastname: '',
@ -75,13 +73,13 @@ function People() {
creationDate: new Date(), creationDate: new Date(),
city: '', city: '',
}; };
await insertPerson(newCompany); await insertPerson(newPerson);
setInternalData([newCompany, ...internalData]); setInternalData([newPerson, ...internalData]);
refetch(); refetch();
}, [internalData, setInternalData, refetch]); }, [internalData, setInternalData, refetch]);
const deleteRows = useCallback(() => { const deleteRows = useCallback(async () => {
deletePeople(selectedRowIds); await deletePeople(selectedRowIds);
setInternalData([ setInternalData([
...internalData.filter((row) => !selectedRowIds.includes(row.id)), ...internalData.filter((row) => !selectedRowIds.includes(row.id)),
]); ]);
@ -109,7 +107,7 @@ function People() {
viewName="All People" viewName="All People"
viewIcon={<FaList />} viewIcon={<FaList />}
availableSorts={availableSorts} availableSorts={availableSorts}
availableFilters={availableFilters as Array<FilterConfigType>} availableFilters={availableFilters}
filterSearchResults={filterSearchResults} filterSearchResults={filterSearchResults}
onSortsUpdate={updateSorts} onSortsUpdate={updateSorts}
onFiltersUpdate={updateFilters} onFiltersUpdate={updateFilters}

View File

@ -227,7 +227,7 @@ export const availableFilters = [
companyFilter, companyFilter,
emailFilter, emailFilter,
cityFilter, cityFilter,
]; ] satisfies FilterConfigType<Person, any>[];
const columnHelper = createColumnHelper<Person>(); const columnHelper = createColumnHelper<Person>();

View File

@ -0,0 +1,36 @@
export const mockCompanySearchData = {
data: {
searchResults: [
{
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
name: 'Linkedin',
domain_name: 'linkedin-searched.com',
__typename: 'companies',
},
{
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
name: 'Facebook',
domain_name: 'facebook-searched.com',
__typename: 'companies',
},
{
id: '04b2e9f5-0713-40a5-8216-82802401d33e',
name: 'Qonto',
domain_name: 'qonto-searched.com',
__typename: 'companies',
},
{
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
domain_name: 'microsoft-searched.com',
__typename: 'companies',
},
{
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
domain_name: 'google-searched.com',
__typename: 'companies',
},
],
},
};