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

@ -1,4 +1,3 @@
import { useState } from 'react';
import styled from '@emotion/styled';
import { CellCommentChip } from '@/comments/components/table/CellCommentChip';
@ -9,7 +8,12 @@ import { CommentableType, Person } from '~/generated/graphql';
import { PersonChip } from './PersonChip';
type OwnProps = {
person: Pick<Person, 'id' | 'firstName' | 'lastName' | '_commentThreadCount'>;
person:
| Partial<
Pick<Person, 'id' | 'firstName' | 'lastName' | '_commentThreadCount'>
>
| null
| undefined;
onChange: (firstName: string, lastName: string) => void;
};
@ -25,17 +29,12 @@ const RightContainer = styled.div`
`;
export function EditablePeopleFullName({ person, onChange }: OwnProps) {
const [firstNameValue, setFirstNameValue] = useState(person.firstName ?? '');
const [lastNameValue, setLastNameValue] = useState(person.lastName ?? '');
const openCommentRightDrawer = useOpenTimelineRightDrawer();
function handleDoubleTextChange(
firstValue: string,
secondValue: string,
): void {
setFirstNameValue(firstValue);
setLastNameValue(secondValue);
onChange(firstValue, secondValue);
}
@ -43,30 +42,34 @@ export function EditablePeopleFullName({ person, onChange }: OwnProps) {
event.preventDefault();
event.stopPropagation();
if (!person) {
return;
}
openCommentRightDrawer([
{
type: CommentableType.Person,
id: person.id,
id: person.id ?? '',
},
]);
}
return (
<EditableCellDoubleText
firstValue={firstNameValue}
secondValue={lastNameValue}
firstValue={person?.firstName ?? ''}
secondValue={person?.lastName ?? ''}
firstValuePlaceholder="First name"
secondValuePlaceholder="Last name"
onChange={handleDoubleTextChange}
nonEditModeContent={
<NoEditModeContainer>
<PersonChip
name={person.firstName + ' ' + person.lastName}
id={person.id}
name={person?.firstName + ' ' + person?.lastName}
id={person?.id ?? ''}
/>
<RightContainer>
<CellCommentChip
count={person._commentThreadCount ?? 0}
count={person?._commentThreadCount ?? 0}
onClick={handleCommentClick}
/>
</RightContainer>

View File

@ -9,6 +9,10 @@ import { Company, Person } from '~/generated/graphql';
import { PeopleCompanyCreateCell } from './PeopleCompanyCreateCell';
import { PeopleCompanyPicker } from './PeopleCompanyPicker';
export type PeopleWithCompany = Pick<Person, 'id'> & {
company?: Pick<Company, 'id' | 'name' | 'domainName'> | null;
};
export type OwnProps = {
people: Pick<Person, 'id'> & {
company?: Pick<Company, 'id' | 'name' | 'domainName'> | null;

View File

@ -0,0 +1,50 @@
import { useRecoilState } from 'recoil';
import { isFetchingEntityTableDataState } from '@/ui/tables/states/isFetchingEntityTableDataState';
import { tableRowIdsState } from '@/ui/tables/states/tableRowIdsState';
import {
PersonOrderByWithRelationInput,
useGetPeopleQuery,
} from '~/generated/graphql';
import { useSetPeopleEntityTable } from '../hooks/useSetPeopleEntityTable';
import { defaultOrderBy } from '../services';
export function PeopleEntityTableData({
orderBy = defaultOrderBy,
whereFilters,
}: {
orderBy?: PersonOrderByWithRelationInput[];
whereFilters?: any;
}) {
const [, setTableRowIds] = useRecoilState(tableRowIdsState);
const [, setIsFetchingEntityTableData] = useRecoilState(
isFetchingEntityTableDataState,
);
const setPeopleEntityTable = useSetPeopleEntityTable();
useGetPeopleQuery({
variables: { orderBy, where: whereFilters },
onCompleted: (data) => {
const people = data.people ?? [];
const peopleIds = people.map((person) => person.id);
setTableRowIds((currentRowIds) => {
if (JSON.stringify(currentRowIds) !== JSON.stringify(peopleIds)) {
return peopleIds;
}
return currentRowIds;
});
setPeopleEntityTable(people);
setIsFetchingEntityTableData(false);
},
});
return <></>;
}

View File

@ -50,6 +50,7 @@ const StyledName = styled.span`
export function PersonChip({ id, name, picture }: PersonChipPropsType) {
const ContainerComponent = id ? StyledContainerLink : StyledContainerNoLink;
return (
<ContainerComponent data-testid="person-chip" to={`/person/${id}`}>
<Avatar

View File

@ -0,0 +1,78 @@
import { useRecoilCallback } from 'recoil';
import { GetPeopleQuery } from '~/generated/graphql';
import { peopleCityFamilyState } from '../states/peopleCityFamilyState';
import { peopleCompanyFamilyState } from '../states/peopleCompanyFamilyState';
import { peopleCreatedAtFamilyState } from '../states/peopleCreatedAtFamilyState';
import { peopleEmailFamilyState } from '../states/peopleEmailFamilyState';
import { peopleNameCellFamilyState } from '../states/peopleNamesFamilyState';
import { peoplePhoneFamilyState } from '../states/peoplePhoneFamilyState';
export function useSetPeopleEntityTable() {
return useRecoilCallback(
({ set, snapshot }) =>
(newPeopleArray: GetPeopleQuery['people']) => {
for (const person of newPeopleArray) {
const currentEmail = snapshot
.getLoadable(peopleEmailFamilyState(person.id))
.valueOrThrow();
if (currentEmail !== person.email) {
set(peopleEmailFamilyState(person.id), person.email);
}
const currentCity = snapshot
.getLoadable(peopleCityFamilyState(person.id))
.valueOrThrow();
if (currentCity !== person.city) {
set(peopleCityFamilyState(person.id), person.city);
}
const currentCompany = snapshot
.getLoadable(peopleCompanyFamilyState(person.id))
.valueOrThrow();
if (
JSON.stringify(currentCompany) !== JSON.stringify(person.company)
) {
set(peopleCompanyFamilyState(person.id), person.company);
}
const currentPhone = snapshot
.getLoadable(peoplePhoneFamilyState(person.id))
.valueOrThrow();
if (currentPhone !== person.phone) {
set(peoplePhoneFamilyState(person.id), person.phone);
}
const currentCreatedAt = snapshot
.getLoadable(peopleCreatedAtFamilyState(person.id))
.valueOrThrow();
if (currentCreatedAt !== person.createdAt) {
set(peopleCreatedAtFamilyState(person.id), person.createdAt);
}
const currentNameCell = snapshot
.getLoadable(peopleNameCellFamilyState(person.id))
.valueOrThrow();
if (
currentNameCell.firstName !== person.firstName ||
currentNameCell.lastName !== person.lastName ||
currentNameCell.commentCount !== person._commentThreadCount
) {
set(peopleNameCellFamilyState(person.id), {
firstName: person.firstName,
lastName: person.lastName,
commentCount: person._commentThreadCount,
});
}
}
},
[],
);
}

View File

@ -48,3 +48,72 @@ export const defaultOrderBy: People_Order_By[] = [
createdAt: SortOrder.Desc,
},
];
export const GET_PERSON_PHONE = gql`
query GetPersonPhoneById($id: String!) {
person: findUniquePerson(id: $id) {
id
phone
}
}
`;
export const GET_PERSON_EMAIL = gql`
query GetPersonEmailById($id: String!) {
person: findUniquePerson(id: $id) {
id
email
}
}
`;
export const GET_PERSON_NAMES_AND_COMMENT_COUNT = gql`
query GetPersonNamesAndCommentCountById($id: String!) {
person: findUniquePerson(id: $id) {
id
firstName
lastName
_commentThreadCount
}
}
`;
export const GET_PERSON_COMPANY = gql`
query GetPersonCompanyById($id: String!) {
person: findUniquePerson(id: $id) {
id
company {
id
name
domainName
}
}
}
`;
export const GET_PERSON_COMMENT_COUNT = gql`
query GetPersonCommentCountById($id: String!) {
person: findUniquePerson(id: $id) {
id
_commentThreadCount
}
}
`;
export const GET_PERSON_CREATED_AT = gql`
query GetPersonCreatedAtById($id: String!) {
person: findUniquePerson(id: $id) {
id
createdAt
}
}
`;
export const GET_PERSON_CITY = gql`
query GetPersonCityById($id: String!) {
person: findUniquePerson(id: $id) {
id
city
}
}
`;

View File

@ -9,7 +9,12 @@ export const GET_PERSON = gql`
firstName
lastName
displayName
email
createdAt
_commentThreadCount
company {
id
}
}
}
`;

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const peopleCityFamilyState = atomFamily<string | null, string>({
key: 'peopleCityFamilyState',
default: null,
});

View File

@ -0,0 +1,11 @@
import { atomFamily } from 'recoil';
import { GetPeopleQuery } from '~/generated/graphql';
export const peopleCompanyFamilyState = atomFamily<
GetPeopleQuery['people'][0]['company'] | null,
string
>({
key: 'peopleCompanyFamilyState',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const peopleCreatedAtFamilyState = atomFamily<string | null, string>({
key: 'peopleCreatedAtFamilyState',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const peopleEmailFamilyState = atomFamily<string | null, string>({
key: 'peopleEmailFamilyState',
default: null,
});

View File

@ -0,0 +1,11 @@
import { atomFamily } from 'recoil';
import { GetPeopleQuery } from '~/generated/graphql';
export const peopleEntityTableFamilyState = atomFamily<
GetPeopleQuery['people'][0] | null,
string
>({
key: 'peopleEntityTableFamilyState',
default: null,
});

View File

@ -0,0 +1,17 @@
import { atomFamily } from 'recoil';
export const peopleNameCellFamilyState = atomFamily<
{
firstName: string | null;
lastName: string | null;
commentCount: number | null;
},
string
>({
key: 'peopleNameCellFamilyState',
default: {
firstName: null,
lastName: null,
commentCount: null,
},
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const peoplePhoneFamilyState = atomFamily<string | null, string>({
key: 'peoplePhoneFamilyState',
default: null,
});

View File

@ -0,0 +1,30 @@
import { useRecoilValue } from 'recoil';
import { peopleCityFamilyState } from '@/people/states/peopleCityFamilyState';
import { EditableCellPhone } from '@/ui/components/editable-cell/types/EditableCellPhone';
import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId';
import { useUpdatePeopleMutation } from '~/generated/graphql';
export function EditablePeopleCityCell() {
const currentRowEntityId = useCurrentRowEntityId();
const [updatePerson] = useUpdatePeopleMutation();
const city = useRecoilValue(peopleCityFamilyState(currentRowEntityId ?? ''));
return (
<EditableCellPhone
value={city ?? ''}
onChange={async (newCity: string) => {
if (!currentRowEntityId) return;
await updatePerson({
variables: {
id: currentRowEntityId,
city: newCity,
},
});
}}
/>
);
}

View File

@ -0,0 +1,26 @@
import { useRecoilValue } from 'recoil';
import { PeopleCompanyCell } from '@/people/components/PeopleCompanyCell';
import { peopleCompanyFamilyState } from '@/people/states/peopleCompanyFamilyState';
import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId';
export function EditablePeopleCompanyCell() {
const currentRowEntityId = useCurrentRowEntityId();
const company = useRecoilValue(
peopleCompanyFamilyState(currentRowEntityId ?? ''),
);
return (
<PeopleCompanyCell
people={{
id: currentRowEntityId ?? '',
company: {
domainName: company?.domainName ?? '',
name: company?.name ?? '',
id: company?.id ?? '',
},
}}
/>
);
}

View File

@ -0,0 +1,33 @@
import { DateTime } from 'luxon';
import { useRecoilValue } from 'recoil';
import { peopleCreatedAtFamilyState } from '@/people/states/peopleCreatedAtFamilyState';
import { EditableCellDate } from '@/ui/components/editable-cell/types/EditableCellDate';
import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId';
import { useUpdatePeopleMutation } from '~/generated/graphql';
export function EditablePeopleCreatedAtCell() {
const currentRowEntityId = useCurrentRowEntityId();
const createdAt = useRecoilValue(
peopleCreatedAtFamilyState(currentRowEntityId ?? ''),
);
const [updatePerson] = useUpdatePeopleMutation();
return (
<EditableCellDate
onChange={async (newDate: Date) => {
if (!currentRowEntityId) return;
await updatePerson({
variables: {
id: currentRowEntityId,
createdAt: newDate.toISOString(),
},
});
}}
value={createdAt ? DateTime.fromISO(createdAt).toJSDate() : new Date()}
/>
);
}

View File

@ -0,0 +1,32 @@
import { useRecoilValue } from 'recoil';
import { peopleEmailFamilyState } from '@/people/states/peopleEmailFamilyState';
import { EditableCellText } from '@/ui/components/editable-cell/types/EditableCellText';
import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId';
import { useUpdatePeopleMutation } from '~/generated/graphql';
export function EditablePeopleEmailCell() {
const currentRowEntityId = useCurrentRowEntityId();
const [updatePerson] = useUpdatePeopleMutation();
const email = useRecoilValue(
peopleEmailFamilyState(currentRowEntityId ?? ''),
);
return (
<EditableCellText
value={email ?? ''}
onChange={async (newEmail: string) => {
if (!currentRowEntityId) return;
await updatePerson({
variables: {
id: currentRowEntityId,
email: newEmail,
},
});
}}
/>
);
}

View File

@ -0,0 +1,38 @@
import { useRecoilValue } from 'recoil';
import { EditablePeopleFullName } from '@/people/components/EditablePeopleFullName';
import { peopleNameCellFamilyState } from '@/people/states/peopleNamesFamilyState';
import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId';
import { useUpdatePeopleMutation } from '~/generated/graphql';
export function EditablePeopleFullNameCell() {
const currentRowEntityId = useCurrentRowEntityId();
const [updatePerson] = useUpdatePeopleMutation();
const { commentCount, firstName, lastName } = useRecoilValue(
peopleNameCellFamilyState(currentRowEntityId ?? ''),
);
return (
<EditablePeopleFullName
person={{
id: currentRowEntityId ?? undefined,
_commentThreadCount: commentCount ?? undefined,
firstName: firstName ?? undefined,
lastName: lastName ?? undefined,
}}
onChange={async (firstName: string, lastName: string) => {
if (!currentRowEntityId) return;
await updatePerson({
variables: {
id: currentRowEntityId,
firstName,
lastName,
},
});
}}
/>
);
}

View File

@ -0,0 +1,31 @@
import { useRecoilValue } from 'recoil';
import { peoplePhoneFamilyState } from '@/people/states/peoplePhoneFamilyState';
import { EditableCellPhone } from '@/ui/components/editable-cell/types/EditableCellPhone';
import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId';
import { useUpdatePeopleMutation } from '~/generated/graphql';
export function EditablePeoplePhoneCell() {
const currentRowEntityId = useCurrentRowEntityId();
const [updatePerson] = useUpdatePeopleMutation();
const phone = useRecoilValue(
peoplePhoneFamilyState(currentRowEntityId ?? ''),
);
return (
<EditableCellPhone
value={phone ?? ''}
onChange={async (newPhone: string) => {
if (!currentRowEntityId) return;
await updatePerson({
variables: {
id: currentRowEntityId,
phone: newPhone,
},
});
}}
/>
);
}

View File

@ -0,0 +1,68 @@
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconMail,
IconMap,
IconPhone,
IconUser,
} from '@/ui/icons/index';
import { EditablePeopleCityCell } from './EditablePeopleCityCell';
import { EditablePeopleCompanyCell } from './EditablePeopleCompanyCell';
import { EditablePeopleCreatedAtCell } from './EditablePeopleCreatedAtCell';
import { EditablePeopleEmailCell } from './EditablePeopleEmailCell';
import { EditablePeopleFullNameCell } from './EditablePeopleFullNameCell';
import { EditablePeoplePhoneCell } from './EditablePeoplePhoneCell';
export type TableColumn = {
id: string;
title: string;
icon: JSX.Element;
size: number;
cellComponent: JSX.Element;
};
export const peopleColumns: TableColumn[] = [
{
id: 'fullName',
title: 'People',
icon: <IconUser size={16} />,
size: 210,
cellComponent: <EditablePeopleFullNameCell />,
},
{
id: 'email',
title: 'Email',
icon: <IconMail size={16} />,
size: 150,
cellComponent: <EditablePeopleEmailCell />,
},
{
id: 'company',
title: 'Company',
icon: <IconBuildingSkyscraper size={16} />,
size: 150,
cellComponent: <EditablePeopleCompanyCell />,
},
{
id: 'phone',
title: 'Phone',
icon: <IconPhone size={16} />,
size: 150,
cellComponent: <EditablePeoplePhoneCell />,
},
{
id: 'createdAt',
title: 'Creation',
icon: <IconCalendarEvent size={16} />,
size: 150,
cellComponent: <EditablePeopleCreatedAtCell />,
},
{
id: 'city',
title: 'City',
icon: <IconMap size={16} />,
size: 150,
cellComponent: <EditablePeopleCityCell />,
},
];