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:
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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]);
|
||||
};
|
||||
@ -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>>;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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]);
|
||||
};
|
||||
@ -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>>;
|
||||
|
||||
Reference in New Issue
Block a user