Refactor/remove react table (#642)

* Refactored tables without tan stack
* Fixed checkbox behavior with multiple handlers on click
* Fixed hotkeys scope
* Fix debounce in editable cells
* Lowered coverage

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-07-13 19:08:13 +02:00
committed by GitHub
parent e7d48d5373
commit 734e18e01a
88 changed files with 1789 additions and 671 deletions

View File

@ -4,8 +4,9 @@ import { IconList } from '@tabler/icons-react';
import {
CompaniesSelectedSortType,
defaultOrderBy,
useCompaniesQuery,
} from '@/companies/services';
import { companyColumns } from '@/companies/table/components/companyColumns';
import { CompanyEntityTableData } from '@/companies/table/components/CompanyEntityTableData';
import { reduceSortsToOrderBy } from '@/lib/filters-and-sorts/helpers';
import { filtersScopedState } from '@/lib/filters-and-sorts/states/filtersScopedState';
import { turnFilterIntoWhereClause } from '@/lib/filters-and-sorts/utils/turnFilterIntoWhereClause';
@ -15,7 +16,6 @@ import { HooksEntityTable } from '@/ui/components/table/HooksEntityTable';
import { TableContext } from '@/ui/tables/states/TableContext';
import { CompanyOrderByWithRelationInput } from '~/generated/graphql';
import { useCompaniesColumns } from './companies-columns';
import { companiesFilters } from './companies-filters';
import { availableSorts } from './companies-sorts';
@ -30,27 +30,18 @@ export function CompanyTable() {
const filters = useRecoilScopedValue(filtersScopedState, TableContext);
const whereFilters = useMemo(() => {
if (!filters.length) return undefined;
return { AND: filters.map(turnFilterIntoWhereClause) };
}, [filters]) as any;
const companiesColumns = useCompaniesColumns();
const { data } = useCompaniesQuery(orderBy, whereFilters);
const companies = data?.companies ?? [];
return (
<>
<CompanyEntityTableData orderBy={orderBy} whereFilters={whereFilters} />
<HooksEntityTable
numberOfColumns={companiesColumns.length}
numberOfRows={companies.length}
availableTableFilters={companiesFilters}
numberOfColumns={companyColumns.length}
availableFilters={companiesFilters}
/>
<EntityTable
data={companies}
columns={companiesColumns}
columns={companyColumns}
viewName="All Companies"
viewIcon={<IconList size={16} />}
availableSorts={availableSorts}

View File

@ -1,28 +1,21 @@
import { IconList } from '@tabler/icons-react';
import { companyColumns } from '@/companies/table/components/companyColumns';
import { EntityTable } from '@/ui/components/table/EntityTable';
import { HooksEntityTable } from '@/ui/components/table/HooksEntityTable';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
import { useCompaniesColumns } from './companies-columns';
import { companiesFilters } from './companies-filters';
import { availableSorts } from './companies-sorts';
export function CompanyTableMockMode() {
const companiesColumns = useCompaniesColumns();
const companies = mockedCompaniesData;
return (
<>
<HooksEntityTable
numberOfColumns={companiesColumns.length}
numberOfRows={companies.length}
availableTableFilters={companiesFilters}
numberOfColumns={companyColumns.length}
availableFilters={companiesFilters}
/>
<EntityTable
data={companies}
columns={companiesColumns}
columns={companyColumns}
viewName="All Companies"
viewIcon={<IconList size={16} />}
availableSorts={availableSorts}

View File

@ -31,12 +31,6 @@ export const SortByName: Story = {
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();
expect(
(await canvas.findAllByRole('checkbox')).map((item) => {
return item.getAttribute('id');
})[1],
).toStrictEqual('checkbox-selected-89bb825c-171e-4bcc-9cf7-43448d6fb278');
const cancelButton = canvas.getByText('Cancel');
await userEvent.click(cancelButton);

View File

@ -1,150 +0,0 @@
import { useMemo } from 'react';
import { createColumnHelper } from '@tanstack/react-table';
import { CompanyAccountOwnerCell } from '@/companies/components/CompanyAccountOwnerCell';
import { CompanyEditableNameChipCell } from '@/companies/components/CompanyEditableNameCell';
import { EditableCellDate } from '@/ui/components/editable-cell/types/EditableCellDate';
import { EditableCellText } from '@/ui/components/editable-cell/types/EditableCellText';
import { ColumnHead } from '@/ui/components/table/ColumnHead';
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconLink,
IconMap,
IconUser,
IconUsers,
} from '@/ui/icons/index';
import { getCheckBoxColumn } from '@/ui/tables/utils/getCheckBoxColumn';
import {
GetCompaniesQuery,
useUpdateCompanyMutation,
} from '~/generated/graphql';
const columnHelper = createColumnHelper<GetCompaniesQuery['companies'][0]>();
export const useCompaniesColumns = () => {
const [updateCompany] = useUpdateCompanyMutation();
return useMemo(() => {
return [
getCheckBoxColumn(),
columnHelper.accessor('name', {
header: () => (
<ColumnHead
viewName="Name"
viewIcon={<IconBuildingSkyscraper size={16} />}
/>
),
cell: (props) => (
<CompanyEditableNameChipCell company={props.row.original} />
),
size: 180,
}),
columnHelper.accessor('domainName', {
header: () => (
<ColumnHead viewName="URL" viewIcon={<IconLink size={16} />} />
),
cell: (props) => (
<EditableCellText
value={props.row.original.domainName || ''}
placeholder="Domain name"
onChange={(value) => {
const company = { ...props.row.original };
company.domainName = value;
updateCompany({
variables: {
...company,
accountOwnerId: company.accountOwner?.id,
},
});
}}
/>
),
size: 100,
}),
columnHelper.accessor('employees', {
header: () => (
<ColumnHead viewName="Employees" viewIcon={<IconUsers size={16} />} />
),
cell: (props) => (
<EditableCellText
value={props.row.original.employees?.toString() || ''}
placeholder="Employees"
onChange={(value) => {
const company = { ...props.row.original };
updateCompany({
variables: {
...company,
employees: value === '' ? null : Number(value),
accountOwnerId: company.accountOwner?.id,
},
});
}}
/>
),
size: 150,
}),
columnHelper.accessor('address', {
header: () => (
<ColumnHead viewName="Address" viewIcon={<IconMap size={16} />} />
),
cell: (props) => (
<EditableCellText
value={props.row.original.address || ''}
placeholder="Address"
onChange={(value) => {
const company = { ...props.row.original };
company.address = value;
updateCompany({
variables: {
...company,
accountOwnerId: company.accountOwner?.id,
},
});
}}
/>
),
size: 170,
}),
columnHelper.accessor('createdAt', {
header: () => (
<ColumnHead
viewName="Creation"
viewIcon={<IconCalendarEvent size={16} />}
/>
),
cell: (props) => (
<EditableCellDate
value={
props.row.original.createdAt
? new Date(props.row.original.createdAt)
: new Date()
}
onChange={(value: Date) => {
const company = { ...props.row.original };
company.createdAt = value.toISOString();
updateCompany({
variables: {
...company,
accountOwnerId: company.accountOwner?.id,
},
});
}}
/>
),
size: 150,
}),
columnHelper.accessor('accountOwner', {
header: () => (
<ColumnHead
viewName="Account owner"
viewIcon={<IconUser size={16} />}
/>
),
cell: (props) => (
<CompanyAccountOwnerCell company={props.row.original} />
),
}),
];
}, [updateCompany]);
};

View File

@ -13,30 +13,25 @@ export const availableSorts = [
key: 'name',
label: 'Name',
icon: <IconBuildingSkyscraper size={16} />,
_type: 'default_sort',
},
{
key: 'employees',
label: 'Employees',
icon: <IconUsers size={16} />,
_type: 'default_sort',
},
{
key: 'domainName',
label: 'Url',
icon: <IconLink size={16} />,
_type: 'default_sort',
},
{
key: 'address',
label: 'Address',
icon: <IconMap size={16} />,
_type: 'default_sort',
},
{
key: 'createdAt',
label: 'Creation',
icon: <IconCalendarEvent size={16} />,
_type: 'default_sort',
},
] satisfies Array<SortType<Companies_Order_By>>;

View File

@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
import { GET_PEOPLE } from '@/people/services';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar';
import { IconUser } from '@/ui/icons/index';
import { IconBuildingSkyscraper } from '@/ui/icons/index';
import { FlexExpandingContainer } from '@/ui/layout/containers/FlexExpandingContainer';
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
import { TableContext } from '@/ui/tables/states/TableContext';
@ -38,8 +38,8 @@ export function People() {
return (
<RecoilScope SpecificContext={TableContext}>
<WithTopBarContainer
title="People"
icon={<IconUser size={theme.icon.size.md} />}
title="Companies"
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
onAddButtonClick={handleAddButtonClick}
>
<FlexExpandingContainer>

View File

@ -5,14 +5,15 @@ import { defaultOrderBy } from '@/companies/services';
import { reduceSortsToOrderBy } from '@/lib/filters-and-sorts/helpers';
import { filtersScopedState } from '@/lib/filters-and-sorts/states/filtersScopedState';
import { turnFilterIntoWhereClause } from '@/lib/filters-and-sorts/utils/turnFilterIntoWhereClause';
import { PeopleSelectedSortType, usePeopleQuery } from '@/people/services';
import { PeopleEntityTableData } from '@/people/components/PeopleEntityTableData';
import { PeopleSelectedSortType } from '@/people/services';
import { peopleColumns } from '@/people/table/components/peopleColumns';
import { useRecoilScopedValue } from '@/recoil-scope/hooks/useRecoilScopedValue';
import { EntityTable } from '@/ui/components/table/EntityTable';
import { HooksEntityTable } from '@/ui/components/table/HooksEntityTable';
import { TableContext } from '@/ui/tables/states/TableContext';
import { PersonOrderByWithRelationInput } from '~/generated/graphql';
import { usePeopleColumns } from './people-columns';
import { peopleFilters } from './people-filters';
import { availableSorts } from './people-sorts';
@ -30,21 +31,14 @@ export function PeopleTable() {
return { AND: filters.map(turnFilterIntoWhereClause) };
}, [filters]) as any;
const peopleColumns = usePeopleColumns();
const { data } = usePeopleQuery(orderBy, whereFilters);
const people = data?.people ?? [];
return (
<>
<PeopleEntityTableData orderBy={orderBy} whereFilters={whereFilters} />
<HooksEntityTable
numberOfColumns={peopleColumns.length}
numberOfRows={people.length}
availableTableFilters={peopleFilters}
availableFilters={peopleFilters}
/>
<EntityTable
data={people}
columns={peopleColumns}
viewName="All People"
viewIcon={<IconList size={16} />}

View File

@ -32,18 +32,12 @@ export const InteractWithManyRows: Story = {
let firstRowEmailCell = await canvas.findByText(mockedPeopleData[0].email);
let secondRowEmailCell = await canvas.findByText(mockedPeopleData[1].email);
expect(
canvas.queryByTestId('editable-cell-edit-mode-container'),
).toBeNull();
await userEvent.click(firstRowEmailCell);
await sleep(100);
firstRowEmailCell = await canvas.findByText(mockedPeopleData[0].email);
await userEvent.click(firstRowEmailCell);
await sleep(100);
firstRowEmailCell = await canvas.findByText(mockedPeopleData[0].email);
await userEvent.click(firstRowEmailCell);
@ -51,7 +45,9 @@ export const InteractWithManyRows: Story = {
canvas.queryByTestId('editable-cell-edit-mode-container'),
).toBeInTheDocument();
secondRowEmailCell = await canvas.findByText(mockedPeopleData[1].email);
const secondRowEmailCell = await canvas.findByText(
mockedPeopleData[1].email,
);
await userEvent.click(secondRowEmailCell);
await sleep(25);

View File

@ -31,12 +31,6 @@ export const Email: Story = {
expect(await canvas.getByTestId('remove-icon-email')).toBeInTheDocument();
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
expect(
(await canvas.findAllByRole('checkbox')).map((item) => {
return item.getAttribute('id');
})[1],
).toStrictEqual('checkbox-selected-7dfbc3f7-6e5e-4128-957e-8d86808cdf6b');
},
parameters: {
msw: graphqlMocks,

View File

@ -1,159 +0,0 @@
import { useMemo } from 'react';
import { createColumnHelper } from '@tanstack/react-table';
import { EditablePeopleFullName } from '@/people/components/EditablePeopleFullName';
import { PeopleCompanyCell } from '@/people/components/PeopleCompanyCell';
import { EditableCellDate } from '@/ui/components/editable-cell/types/EditableCellDate';
import { EditableCellPhone } from '@/ui/components/editable-cell/types/EditableCellPhone';
import { EditableCellText } from '@/ui/components/editable-cell/types/EditableCellText';
import { ColumnHead } from '@/ui/components/table/ColumnHead';
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconMail,
IconMap,
IconPhone,
IconUser,
} from '@/ui/icons/index';
import { getCheckBoxColumn } from '@/ui/tables/utils/getCheckBoxColumn';
import { GetPeopleQuery, useUpdatePeopleMutation } from '~/generated/graphql';
const columnHelper = createColumnHelper<GetPeopleQuery['people'][0]>();
export const usePeopleColumns = () => {
const [updatePerson] = useUpdatePeopleMutation();
return useMemo(() => {
return [
getCheckBoxColumn(),
columnHelper.accessor('firstName', {
header: () => (
<ColumnHead viewName="People" viewIcon={<IconUser size={16} />} />
),
cell: (props) => (
<>
<EditablePeopleFullName
person={props.row.original}
onChange={async (firstName: string, lastName: string) => {
const person = { ...props.row.original };
await updatePerson({
variables: {
...person,
firstName,
lastName,
companyId: person.company?.id,
},
});
}}
/>
</>
),
size: 210,
}),
columnHelper.accessor('email', {
header: () => (
<ColumnHead viewName="Email" viewIcon={<IconMail size={16} />} />
),
cell: (props) => (
<EditableCellText
placeholder="Email"
value={props.row.original.email || ''}
onChange={async (value: string) => {
const person = props.row.original;
await updatePerson({
variables: {
...person,
email: value,
companyId: person.company?.id,
},
});
}}
/>
),
size: 200,
}),
columnHelper.accessor('company', {
header: () => (
<ColumnHead
viewName="Company"
viewIcon={<IconBuildingSkyscraper size={16} />}
/>
),
cell: (props) => <PeopleCompanyCell people={props.row.original} />,
size: 150,
}),
columnHelper.accessor('phone', {
header: () => (
<ColumnHead viewName="Phone" viewIcon={<IconPhone size={16} />} />
),
cell: (props) => (
<EditableCellPhone
placeholder="Phone"
value={props.row.original.phone || ''}
changeHandler={async (value: string) => {
const person = { ...props.row.original };
await updatePerson({
variables: {
...person,
phone: value,
companyId: person.company?.id,
},
});
}}
/>
),
size: 130,
}),
columnHelper.accessor('createdAt', {
header: () => (
<ColumnHead
viewName="Creation"
viewIcon={<IconCalendarEvent size={16} />}
/>
),
cell: (props) => (
<EditableCellDate
value={
props.row.original.createdAt
? new Date(props.row.original.createdAt)
: new Date()
}
onChange={async (value: Date) => {
const person = { ...props.row.original };
await updatePerson({
variables: {
...person,
createdAt: value.toISOString(),
companyId: person.company?.id,
},
});
}}
/>
),
size: 100,
}),
columnHelper.accessor('city', {
header: () => (
<ColumnHead viewName="City" viewIcon={<IconMap size={16} />} />
),
cell: (props) => (
<EditableCellText
editModeHorizontalAlign="right"
placeholder="City"
value={props.row.original.city || ''}
onChange={async (value: string) => {
const person = { ...props.row.original };
await updatePerson({
variables: {
...person,
city: value,
companyId: person.company?.id,
},
});
}}
/>
),
}),
];
}, [updatePerson]);
};

View File

@ -17,7 +17,7 @@ export const availableSorts = [
key: 'fullname',
label: 'People',
icon: <IconUser size={16} />,
_type: 'custom_sort',
orderByTemplates: [
(order: Order_By) => ({
firstName: order,
@ -31,31 +31,27 @@ export const availableSorts = [
key: 'company_name',
label: 'Company',
icon: <IconBuildingSkyscraper size={16} />,
_type: 'custom_sort',
orderByTemplates: [(order: Order_By) => ({ company: { name: order } })],
},
{
key: 'email',
label: 'Email',
icon: <IconMail size={16} />,
_type: 'default_sort',
},
{
key: 'phone',
label: 'Phone',
icon: <IconPhone size={16} />,
_type: 'default_sort',
},
{
key: 'createdAt',
label: 'Created at',
icon: <IconCalendarEvent size={16} />,
_type: 'default_sort',
},
{
key: 'city',
label: 'City',
icon: <IconMap size={16} />,
_type: 'default_sort',
},
] satisfies Array<SortType<People_Order_By>>;