From 734e18e01aea6c87445725d8da63661a9ff3067b Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 13 Jul 2023 19:08:13 +0200 Subject: [PATCH] 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 --- front/package.json | 2 +- front/src/generated/graphql.tsx | 314 +++++++++++++++++- .../hooks/useOpenTimelineRightDrawer.ts | 1 + .../components/CompanyAccountOwnerCell.tsx | 10 +- .../components/CompanyEditableNameCell.tsx | 7 +- .../states/companyAccountOwnerFamilyState.ts | 11 + .../states/companyAddressFamilyState.ts | 6 + .../states/companyCommentCountFamilyState.ts | 8 + .../states/companyCreatedAtFamilyState.ts | 6 + .../states/companyDomainNameFamilyState.ts | 6 + .../states/companyEmployeesFamilyState.ts | 6 + .../states/companyNameFamilyState.ts | 6 + .../components/CompanyEntityTableData.tsx | 50 +++ .../EditableCompanyAccountOwnerCell.tsx | 25 ++ .../components/EditableCompanyAddressCell.tsx | 32 ++ .../EditableCompanyCreatedAtCell.tsx | 33 ++ .../EditableCompanyDomainNameCell.tsx | 32 ++ .../EditableCompanyEmployeesCell.tsx | 33 ++ .../components/EditableCompanyNameCell.tsx | 32 ++ .../table/components/companyColumns.tsx | 61 ++++ .../table/hooks/useSetCompanyEntityTable.ts | 88 +++++ .../modules/lib/filters-and-sorts/helpers.ts | 4 +- .../interfaces/sorts/interface.ts | 20 +- .../components/EditablePeopleFullName.tsx | 29 +- .../people/components/PeopleCompanyCell.tsx | 4 + .../components/PeopleEntityTableData.tsx | 50 +++ .../modules/people/components/PersonChip.tsx | 1 + .../people/hooks/useSetPeopleEntityTable.ts | 78 +++++ front/src/modules/people/services/select.ts | 69 ++++ front/src/modules/people/services/show.ts | 5 + .../people/states/peopleCityFamilyState.ts | 6 + .../people/states/peopleCompanyFamilyState.ts | 11 + .../states/peopleCreatedAtFamilyState.ts | 6 + .../people/states/peopleEmailFamilyState.ts | 6 + .../states/peopleEntityTableFamilyState.ts | 11 + .../people/states/peopleNamesFamilyState.ts | 17 + .../people/states/peoplePhoneFamilyState.ts | 6 + .../components/EditablePeopleCityCell.tsx | 30 ++ .../components/EditablePeopleCompanyCell.tsx | 26 ++ .../EditablePeopleCreatedAtCell.tsx | 33 ++ .../components/EditablePeopleEmailCell.tsx | 32 ++ .../components/EditablePeopleFullNameCell.tsx | 38 +++ .../components/EditablePeoplePhoneCell.tsx | 31 ++ .../people/table/components/peopleColumns.tsx | 68 ++++ .../components/editable-cell/CellSkeleton.tsx | 9 + .../types/EditableCellDoubleText.tsx | 22 +- .../editable-cell/types/EditableCellPhone.tsx | 16 +- .../editable-cell/types/EditableCellText.tsx | 24 +- .../editable-cell/types/EditableChip.tsx | 13 +- .../modules/ui/components/form/Checkbox.tsx | 22 +- .../menu/DropdownMenuCheckableItem.tsx | 3 +- .../ui/components/table/CheckboxCell.tsx | 41 +-- .../ui/components/table/EntityTable.tsx | 88 +---- .../ui/components/table/EntityTableBody.tsx | 33 ++ .../ui/components/table/EntityTableCell.tsx | 24 +- .../ui/components/table/EntityTableHeader.tsx | 39 +++ .../ui/components/table/EntityTableRow.tsx | 50 ++- .../ui/components/table/HooksEntityTable.tsx | 9 +- .../ui/components/table/SelectAllCheckbox.tsx | 46 ++- .../src/modules/ui/tables/constants/index.ts | 1 - .../ui/tables/hooks/useCurrentEntityId.ts | 18 + .../ui/tables/hooks/useCurrentRowSelected.ts | 43 +++ .../tables/hooks/useInitializeEntityTable.ts | 16 +- .../hooks/useInitializeEntityTableFilters.ts | 10 +- .../ui/tables/hooks/useMoveSoftFocus.ts | 17 +- .../ui/tables/hooks/useSelectAllRows.ts | 48 +++ .../states/allRowsSelectedStatusSelector.ts | 24 ++ .../states/currentRowEntityIdScopedState.ts | 6 + .../states/isFetchingEntityTableDataState.ts | 6 + .../tables/states/isRowSelectedFamilyState.ts | 6 + .../tables/states/numberOfSelectedRowState.ts | 6 + .../ui/tables/states/tableRowIdsState.ts | 6 + .../ui/tables/types/AllRowSelectedStatus.ts | 1 + .../ui/tables/utils/getCheckBoxColumn.tsx | 27 -- front/src/pages/companies/CompanyTable.tsx | 21 +- .../pages/companies/CompanyTableMockMode.tsx | 15 +- .../__stories__/Companies.sortBy.stories.tsx | 6 - .../src/pages/companies/companies-columns.tsx | 150 --------- front/src/pages/companies/companies-sorts.tsx | 5 - front/src/pages/people/People.tsx | 6 +- front/src/pages/people/PeopleTable.tsx | 16 +- .../__stories__/People.inputs.stories.tsx | 10 +- .../__stories__/People.sortBy.stories.tsx | 6 - front/src/pages/people/people-columns.tsx | 159 --------- front/src/pages/people/people-sorts.tsx | 8 +- front/src/testing/renderWrappers.tsx | 4 +- server/src/core/company/company.resolver.ts | 15 +- server/src/core/person/person.resolver.ts | 15 +- 88 files changed, 1789 insertions(+), 671 deletions(-) create mode 100644 front/src/modules/companies/states/companyAccountOwnerFamilyState.ts create mode 100644 front/src/modules/companies/states/companyAddressFamilyState.ts create mode 100644 front/src/modules/companies/states/companyCommentCountFamilyState.ts create mode 100644 front/src/modules/companies/states/companyCreatedAtFamilyState.ts create mode 100644 front/src/modules/companies/states/companyDomainNameFamilyState.ts create mode 100644 front/src/modules/companies/states/companyEmployeesFamilyState.ts create mode 100644 front/src/modules/companies/states/companyNameFamilyState.ts create mode 100644 front/src/modules/companies/table/components/CompanyEntityTableData.tsx create mode 100644 front/src/modules/companies/table/components/EditableCompanyAccountOwnerCell.tsx create mode 100644 front/src/modules/companies/table/components/EditableCompanyAddressCell.tsx create mode 100644 front/src/modules/companies/table/components/EditableCompanyCreatedAtCell.tsx create mode 100644 front/src/modules/companies/table/components/EditableCompanyDomainNameCell.tsx create mode 100644 front/src/modules/companies/table/components/EditableCompanyEmployeesCell.tsx create mode 100644 front/src/modules/companies/table/components/EditableCompanyNameCell.tsx create mode 100644 front/src/modules/companies/table/components/companyColumns.tsx create mode 100644 front/src/modules/companies/table/hooks/useSetCompanyEntityTable.ts create mode 100644 front/src/modules/people/components/PeopleEntityTableData.tsx create mode 100644 front/src/modules/people/hooks/useSetPeopleEntityTable.ts create mode 100644 front/src/modules/people/states/peopleCityFamilyState.ts create mode 100644 front/src/modules/people/states/peopleCompanyFamilyState.ts create mode 100644 front/src/modules/people/states/peopleCreatedAtFamilyState.ts create mode 100644 front/src/modules/people/states/peopleEmailFamilyState.ts create mode 100644 front/src/modules/people/states/peopleEntityTableFamilyState.ts create mode 100644 front/src/modules/people/states/peopleNamesFamilyState.ts create mode 100644 front/src/modules/people/states/peoplePhoneFamilyState.ts create mode 100644 front/src/modules/people/table/components/EditablePeopleCityCell.tsx create mode 100644 front/src/modules/people/table/components/EditablePeopleCompanyCell.tsx create mode 100644 front/src/modules/people/table/components/EditablePeopleCreatedAtCell.tsx create mode 100644 front/src/modules/people/table/components/EditablePeopleEmailCell.tsx create mode 100644 front/src/modules/people/table/components/EditablePeopleFullNameCell.tsx create mode 100644 front/src/modules/people/table/components/EditablePeoplePhoneCell.tsx create mode 100644 front/src/modules/people/table/components/peopleColumns.tsx create mode 100644 front/src/modules/ui/components/editable-cell/CellSkeleton.tsx create mode 100644 front/src/modules/ui/components/table/EntityTableBody.tsx create mode 100644 front/src/modules/ui/components/table/EntityTableHeader.tsx delete mode 100644 front/src/modules/ui/tables/constants/index.ts create mode 100644 front/src/modules/ui/tables/hooks/useCurrentEntityId.ts create mode 100644 front/src/modules/ui/tables/hooks/useCurrentRowSelected.ts create mode 100644 front/src/modules/ui/tables/hooks/useSelectAllRows.ts create mode 100644 front/src/modules/ui/tables/states/allRowsSelectedStatusSelector.ts create mode 100644 front/src/modules/ui/tables/states/currentRowEntityIdScopedState.ts create mode 100644 front/src/modules/ui/tables/states/isFetchingEntityTableDataState.ts create mode 100644 front/src/modules/ui/tables/states/isRowSelectedFamilyState.ts create mode 100644 front/src/modules/ui/tables/states/numberOfSelectedRowState.ts create mode 100644 front/src/modules/ui/tables/states/tableRowIdsState.ts create mode 100644 front/src/modules/ui/tables/types/AllRowSelectedStatus.ts delete mode 100644 front/src/modules/ui/tables/utils/getCheckBoxColumn.tsx delete mode 100644 front/src/pages/companies/companies-columns.tsx delete mode 100644 front/src/pages/people/people-columns.tsx diff --git a/front/package.json b/front/package.json index 92e33d811..594e22913 100644 --- a/front/package.json +++ b/front/package.json @@ -166,7 +166,7 @@ }, "nyc": { "lines": 65, - "statements": 65, + "statements": 60, "exclude": [ "src/generated/**/*" ] diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index b9080f10b..e2f9e02f1 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -3271,12 +3271,61 @@ export type GetPeopleQueryVariables = Exact<{ export type GetPeopleQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstName: string, lastName: string, createdAt: string, _commentThreadCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null }> }; +export type GetPersonPhoneByIdQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type GetPersonPhoneByIdQuery = { __typename?: 'Query', person: { __typename?: 'Person', id: string, phone: string } }; + +export type GetPersonEmailByIdQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type GetPersonEmailByIdQuery = { __typename?: 'Query', person: { __typename?: 'Person', id: string, email: string } }; + +export type GetPersonNamesAndCommentCountByIdQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type GetPersonNamesAndCommentCountByIdQuery = { __typename?: 'Query', person: { __typename?: 'Person', id: string, firstName: string, lastName: string, _commentThreadCount: number } }; + +export type GetPersonCompanyByIdQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type GetPersonCompanyByIdQuery = { __typename?: 'Query', person: { __typename?: 'Person', id: string, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null } }; + +export type GetPersonCommentCountByIdQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type GetPersonCommentCountByIdQuery = { __typename?: 'Query', person: { __typename?: 'Person', id: string, _commentThreadCount: number } }; + +export type GetPersonCreatedAtByIdQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type GetPersonCreatedAtByIdQuery = { __typename?: 'Query', person: { __typename?: 'Person', id: string, createdAt: string } }; + +export type GetPersonCityByIdQueryVariables = Exact<{ + id: Scalars['String']; +}>; + + +export type GetPersonCityByIdQuery = { __typename?: 'Query', person: { __typename?: 'Person', id: string, city: string } }; + export type GetPersonQueryVariables = Exact<{ id: Scalars['String']; }>; -export type GetPersonQuery = { __typename?: 'Query', findUniquePerson: { __typename?: 'Person', id: string, firstName: string, lastName: string, displayName: string, createdAt: string } }; +export type GetPersonQuery = { __typename?: 'Query', findUniquePerson: { __typename?: 'Person', id: string, firstName: string, lastName: string, displayName: string, email: string, createdAt: string, _commentThreadCount: number, company?: { __typename?: 'Company', id: string } | null } }; export type UpdatePeopleMutationVariables = Exact<{ id?: InputMaybe; @@ -4456,6 +4505,264 @@ export function useGetPeopleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions< export type GetPeopleQueryHookResult = ReturnType; export type GetPeopleLazyQueryHookResult = ReturnType; export type GetPeopleQueryResult = Apollo.QueryResult; +export const GetPersonPhoneByIdDocument = gql` + query GetPersonPhoneById($id: String!) { + person: findUniquePerson(id: $id) { + id + phone + } +} + `; + +/** + * __useGetPersonPhoneByIdQuery__ + * + * To run a query within a React component, call `useGetPersonPhoneByIdQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPersonPhoneByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetPersonPhoneByIdQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetPersonPhoneByIdQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetPersonPhoneByIdDocument, options); + } +export function useGetPersonPhoneByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetPersonPhoneByIdDocument, options); + } +export type GetPersonPhoneByIdQueryHookResult = ReturnType; +export type GetPersonPhoneByIdLazyQueryHookResult = ReturnType; +export type GetPersonPhoneByIdQueryResult = Apollo.QueryResult; +export const GetPersonEmailByIdDocument = gql` + query GetPersonEmailById($id: String!) { + person: findUniquePerson(id: $id) { + id + email + } +} + `; + +/** + * __useGetPersonEmailByIdQuery__ + * + * To run a query within a React component, call `useGetPersonEmailByIdQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPersonEmailByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetPersonEmailByIdQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetPersonEmailByIdQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetPersonEmailByIdDocument, options); + } +export function useGetPersonEmailByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetPersonEmailByIdDocument, options); + } +export type GetPersonEmailByIdQueryHookResult = ReturnType; +export type GetPersonEmailByIdLazyQueryHookResult = ReturnType; +export type GetPersonEmailByIdQueryResult = Apollo.QueryResult; +export const GetPersonNamesAndCommentCountByIdDocument = gql` + query GetPersonNamesAndCommentCountById($id: String!) { + person: findUniquePerson(id: $id) { + id + firstName + lastName + _commentThreadCount + } +} + `; + +/** + * __useGetPersonNamesAndCommentCountByIdQuery__ + * + * To run a query within a React component, call `useGetPersonNamesAndCommentCountByIdQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPersonNamesAndCommentCountByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetPersonNamesAndCommentCountByIdQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetPersonNamesAndCommentCountByIdQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetPersonNamesAndCommentCountByIdDocument, options); + } +export function useGetPersonNamesAndCommentCountByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetPersonNamesAndCommentCountByIdDocument, options); + } +export type GetPersonNamesAndCommentCountByIdQueryHookResult = ReturnType; +export type GetPersonNamesAndCommentCountByIdLazyQueryHookResult = ReturnType; +export type GetPersonNamesAndCommentCountByIdQueryResult = Apollo.QueryResult; +export const GetPersonCompanyByIdDocument = gql` + query GetPersonCompanyById($id: String!) { + person: findUniquePerson(id: $id) { + id + company { + id + name + domainName + } + } +} + `; + +/** + * __useGetPersonCompanyByIdQuery__ + * + * To run a query within a React component, call `useGetPersonCompanyByIdQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPersonCompanyByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetPersonCompanyByIdQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetPersonCompanyByIdQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetPersonCompanyByIdDocument, options); + } +export function useGetPersonCompanyByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetPersonCompanyByIdDocument, options); + } +export type GetPersonCompanyByIdQueryHookResult = ReturnType; +export type GetPersonCompanyByIdLazyQueryHookResult = ReturnType; +export type GetPersonCompanyByIdQueryResult = Apollo.QueryResult; +export const GetPersonCommentCountByIdDocument = gql` + query GetPersonCommentCountById($id: String!) { + person: findUniquePerson(id: $id) { + id + _commentThreadCount + } +} + `; + +/** + * __useGetPersonCommentCountByIdQuery__ + * + * To run a query within a React component, call `useGetPersonCommentCountByIdQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPersonCommentCountByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetPersonCommentCountByIdQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetPersonCommentCountByIdQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetPersonCommentCountByIdDocument, options); + } +export function useGetPersonCommentCountByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetPersonCommentCountByIdDocument, options); + } +export type GetPersonCommentCountByIdQueryHookResult = ReturnType; +export type GetPersonCommentCountByIdLazyQueryHookResult = ReturnType; +export type GetPersonCommentCountByIdQueryResult = Apollo.QueryResult; +export const GetPersonCreatedAtByIdDocument = gql` + query GetPersonCreatedAtById($id: String!) { + person: findUniquePerson(id: $id) { + id + createdAt + } +} + `; + +/** + * __useGetPersonCreatedAtByIdQuery__ + * + * To run a query within a React component, call `useGetPersonCreatedAtByIdQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPersonCreatedAtByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetPersonCreatedAtByIdQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetPersonCreatedAtByIdQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetPersonCreatedAtByIdDocument, options); + } +export function useGetPersonCreatedAtByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetPersonCreatedAtByIdDocument, options); + } +export type GetPersonCreatedAtByIdQueryHookResult = ReturnType; +export type GetPersonCreatedAtByIdLazyQueryHookResult = ReturnType; +export type GetPersonCreatedAtByIdQueryResult = Apollo.QueryResult; +export const GetPersonCityByIdDocument = gql` + query GetPersonCityById($id: String!) { + person: findUniquePerson(id: $id) { + id + city + } +} + `; + +/** + * __useGetPersonCityByIdQuery__ + * + * To run a query within a React component, call `useGetPersonCityByIdQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPersonCityByIdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetPersonCityByIdQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetPersonCityByIdQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetPersonCityByIdDocument, options); + } +export function useGetPersonCityByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetPersonCityByIdDocument, options); + } +export type GetPersonCityByIdQueryHookResult = ReturnType; +export type GetPersonCityByIdLazyQueryHookResult = ReturnType; +export type GetPersonCityByIdQueryResult = Apollo.QueryResult; export const GetPersonDocument = gql` query GetPerson($id: String!) { findUniquePerson(id: $id) { @@ -4463,7 +4770,12 @@ export const GetPersonDocument = gql` firstName lastName displayName + email createdAt + _commentThreadCount + company { + id + } } } `; diff --git a/front/src/modules/comments/hooks/useOpenTimelineRightDrawer.ts b/front/src/modules/comments/hooks/useOpenTimelineRightDrawer.ts index 2ebfbdf57..e38c09e34 100644 --- a/front/src/modules/comments/hooks/useOpenTimelineRightDrawer.ts +++ b/front/src/modules/comments/hooks/useOpenTimelineRightDrawer.ts @@ -8,6 +8,7 @@ import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRi import { commentableEntityArrayState } from '../states/commentableEntityArrayState'; import { CommentableEntity } from '../types/CommentableEntity'; +// TODO: refactor with recoil callback to avoid rerender export function useOpenTimelineRightDrawer() { const openRightDrawer = useOpenRightDrawer(); const [, setCommentableEntityArray] = useRecoilState( diff --git a/front/src/modules/companies/components/CompanyAccountOwnerCell.tsx b/front/src/modules/companies/components/CompanyAccountOwnerCell.tsx index 51a07641a..224e177bf 100644 --- a/front/src/modules/companies/components/CompanyAccountOwnerCell.tsx +++ b/front/src/modules/companies/components/CompanyAccountOwnerCell.tsx @@ -1,18 +1,22 @@ +import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope'; import { PersonChip } from '@/people/components/PersonChip'; import { EditableCell } from '@/ui/components/editable-cell/EditableCell'; import { Company, User } from '~/generated/graphql'; import { CompanyAccountOwnerPicker } from './CompanyAccountOwnerPicker'; +export type CompanyAccountOnwer = Pick & { + accountOwner?: Pick | null; +}; + export type OwnProps = { - company: Pick & { - accountOwner?: Pick | null; - }; + company: CompanyAccountOnwer; }; export function CompanyAccountOwnerCell({ company }: OwnProps) { return ( } nonEditModeContent={ company.accountOwner?.displayName ? ( diff --git a/front/src/modules/companies/components/CompanyEditableNameCell.tsx b/front/src/modules/companies/components/CompanyEditableNameCell.tsx index 94047aa6e..3d759e79e 100644 --- a/front/src/modules/companies/components/CompanyEditableNameCell.tsx +++ b/front/src/modules/companies/components/CompanyEditableNameCell.tsx @@ -13,7 +13,7 @@ import { CompanyChip } from './CompanyChip'; type OwnProps = { company: Pick< GetCompaniesQuery['companies'][0], - 'id' | 'name' | 'domainName' | '_commentThreadCount' | 'accountOwner' + 'id' | 'name' | 'domainName' | '_commentThreadCount' >; }; @@ -35,16 +35,15 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) { return ( { updateCompany({ variables: { - ...company, + id: company.id, name: value, - accountOwnerId: company.accountOwner?.id, }, }); }} diff --git a/front/src/modules/companies/states/companyAccountOwnerFamilyState.ts b/front/src/modules/companies/states/companyAccountOwnerFamilyState.ts new file mode 100644 index 000000000..1b38131ac --- /dev/null +++ b/front/src/modules/companies/states/companyAccountOwnerFamilyState.ts @@ -0,0 +1,11 @@ +import { atomFamily } from 'recoil'; + +import { CompanyAccountOnwer } from '../components/CompanyAccountOwnerCell'; + +export const companyAccountOwnerFamilyState = atomFamily< + CompanyAccountOnwer['accountOwner'] | null, + string +>({ + key: 'companyAccountOwnerFamilyState', + default: null, +}); diff --git a/front/src/modules/companies/states/companyAddressFamilyState.ts b/front/src/modules/companies/states/companyAddressFamilyState.ts new file mode 100644 index 000000000..90bf78f76 --- /dev/null +++ b/front/src/modules/companies/states/companyAddressFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const companyAddressFamilyState = atomFamily({ + key: 'companyAddressFamilyState', + default: null, +}); diff --git a/front/src/modules/companies/states/companyCommentCountFamilyState.ts b/front/src/modules/companies/states/companyCommentCountFamilyState.ts new file mode 100644 index 000000000..e0fcd6b6e --- /dev/null +++ b/front/src/modules/companies/states/companyCommentCountFamilyState.ts @@ -0,0 +1,8 @@ +import { atomFamily } from 'recoil'; + +export const companyCommentCountFamilyState = atomFamily( + { + key: 'companyCommentCountFamilyState', + default: null, + }, +); diff --git a/front/src/modules/companies/states/companyCreatedAtFamilyState.ts b/front/src/modules/companies/states/companyCreatedAtFamilyState.ts new file mode 100644 index 000000000..a89b7db29 --- /dev/null +++ b/front/src/modules/companies/states/companyCreatedAtFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const companyCreatedAtFamilyState = atomFamily({ + key: 'companyCreatedAtFamilyState', + default: null, +}); diff --git a/front/src/modules/companies/states/companyDomainNameFamilyState.ts b/front/src/modules/companies/states/companyDomainNameFamilyState.ts new file mode 100644 index 000000000..10208940d --- /dev/null +++ b/front/src/modules/companies/states/companyDomainNameFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const companyDomainNameFamilyState = atomFamily({ + key: 'companyDomainNameFamilyState', + default: null, +}); diff --git a/front/src/modules/companies/states/companyEmployeesFamilyState.ts b/front/src/modules/companies/states/companyEmployeesFamilyState.ts new file mode 100644 index 000000000..7236e2b03 --- /dev/null +++ b/front/src/modules/companies/states/companyEmployeesFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const companyEmployeesFamilyState = atomFamily({ + key: 'companyEmployeesFamilyState', + default: null, +}); diff --git a/front/src/modules/companies/states/companyNameFamilyState.ts b/front/src/modules/companies/states/companyNameFamilyState.ts new file mode 100644 index 000000000..b5179a516 --- /dev/null +++ b/front/src/modules/companies/states/companyNameFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const companyNameFamilyState = atomFamily({ + key: 'companyNameFamilyState', + default: null, +}); diff --git a/front/src/modules/companies/table/components/CompanyEntityTableData.tsx b/front/src/modules/companies/table/components/CompanyEntityTableData.tsx new file mode 100644 index 000000000..0cf6aaa3b --- /dev/null +++ b/front/src/modules/companies/table/components/CompanyEntityTableData.tsx @@ -0,0 +1,50 @@ +import { useRecoilState } from 'recoil'; + +import { defaultOrderBy } from '@/companies/services'; +import { isFetchingEntityTableDataState } from '@/ui/tables/states/isFetchingEntityTableDataState'; +import { tableRowIdsState } from '@/ui/tables/states/tableRowIdsState'; +import { + PersonOrderByWithRelationInput, + useGetCompaniesQuery, +} from '~/generated/graphql'; + +import { useSetCompanyEntityTable } from '../hooks/useSetCompanyEntityTable'; + +export function CompanyEntityTableData({ + orderBy = defaultOrderBy, + whereFilters, +}: { + orderBy?: PersonOrderByWithRelationInput[]; + whereFilters?: any; +}) { + const [, setTableRowIds] = useRecoilState(tableRowIdsState); + + const [, setIsFetchingEntityTableData] = useRecoilState( + isFetchingEntityTableDataState, + ); + + const setCompanyEntityTable = useSetCompanyEntityTable(); + + useGetCompaniesQuery({ + variables: { orderBy, where: whereFilters }, + onCompleted: (data) => { + const companies = data.companies ?? []; + + const companyIds = companies.map((company) => company.id); + + setTableRowIds((currentRowIds) => { + if (JSON.stringify(currentRowIds) !== JSON.stringify(companyIds)) { + return companyIds; + } + + return currentRowIds; + }); + + setCompanyEntityTable(companies); + + setIsFetchingEntityTableData(false); + }, + }); + + return <>; +} diff --git a/front/src/modules/companies/table/components/EditableCompanyAccountOwnerCell.tsx b/front/src/modules/companies/table/components/EditableCompanyAccountOwnerCell.tsx new file mode 100644 index 000000000..b4adc8c7d --- /dev/null +++ b/front/src/modules/companies/table/components/EditableCompanyAccountOwnerCell.tsx @@ -0,0 +1,25 @@ +import { useRecoilValue } from 'recoil'; + +import { CompanyAccountOwnerCell } from '@/companies/components/CompanyAccountOwnerCell'; +import { companyAccountOwnerFamilyState } from '@/companies/states/companyAccountOwnerFamilyState'; +import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId'; + +export function EditableCompanyAccountOwnerCell() { + const currentRowEntityId = useCurrentRowEntityId(); + + const accountOwner = useRecoilValue( + companyAccountOwnerFamilyState(currentRowEntityId ?? ''), + ); + + return ( + + ); +} diff --git a/front/src/modules/companies/table/components/EditableCompanyAddressCell.tsx b/front/src/modules/companies/table/components/EditableCompanyAddressCell.tsx new file mode 100644 index 000000000..d3292a366 --- /dev/null +++ b/front/src/modules/companies/table/components/EditableCompanyAddressCell.tsx @@ -0,0 +1,32 @@ +import { useRecoilValue } from 'recoil'; + +import { companyAddressFamilyState } from '@/companies/states/companyAddressFamilyState'; +import { EditableCellText } from '@/ui/components/editable-cell/types/EditableCellText'; +import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId'; +import { useUpdateCompanyMutation } from '~/generated/graphql'; + +export function EditableCompanyAddressCell() { + const currentRowEntityId = useCurrentRowEntityId(); + + const [updateCompany] = useUpdateCompanyMutation(); + + const address = useRecoilValue( + companyAddressFamilyState(currentRowEntityId ?? ''), + ); + + return ( + { + if (!currentRowEntityId) return; + + await updateCompany({ + variables: { + id: currentRowEntityId, + address: newAddress, + }, + }); + }} + /> + ); +} diff --git a/front/src/modules/companies/table/components/EditableCompanyCreatedAtCell.tsx b/front/src/modules/companies/table/components/EditableCompanyCreatedAtCell.tsx new file mode 100644 index 000000000..4ad165542 --- /dev/null +++ b/front/src/modules/companies/table/components/EditableCompanyCreatedAtCell.tsx @@ -0,0 +1,33 @@ +import { DateTime } from 'luxon'; +import { useRecoilValue } from 'recoil'; + +import { companyCreatedAtFamilyState } from '@/companies/states/companyCreatedAtFamilyState'; +import { EditableCellDate } from '@/ui/components/editable-cell/types/EditableCellDate'; +import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId'; +import { useUpdateCompanyMutation } from '~/generated/graphql'; + +export function EditableCompanyCreatedAtCell() { + const currentRowEntityId = useCurrentRowEntityId(); + + const createdAt = useRecoilValue( + companyCreatedAtFamilyState(currentRowEntityId ?? ''), + ); + + const [updateCompany] = useUpdateCompanyMutation(); + + return ( + { + if (!currentRowEntityId) return; + + await updateCompany({ + variables: { + id: currentRowEntityId, + createdAt: newDate.toISOString(), + }, + }); + }} + value={createdAt ? DateTime.fromISO(createdAt).toJSDate() : new Date()} + /> + ); +} diff --git a/front/src/modules/companies/table/components/EditableCompanyDomainNameCell.tsx b/front/src/modules/companies/table/components/EditableCompanyDomainNameCell.tsx new file mode 100644 index 000000000..81f6dffd4 --- /dev/null +++ b/front/src/modules/companies/table/components/EditableCompanyDomainNameCell.tsx @@ -0,0 +1,32 @@ +import { useRecoilValue } from 'recoil'; + +import { companyDomainNameFamilyState } from '@/companies/states/companyDomainNameFamilyState'; +import { EditableCellText } from '@/ui/components/editable-cell/types/EditableCellText'; +import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId'; +import { useUpdateCompanyMutation } from '~/generated/graphql'; + +export function EditableCompanyDomainNameCell() { + const currentRowEntityId = useCurrentRowEntityId(); + + const [updateCompany] = useUpdateCompanyMutation(); + + const name = useRecoilValue( + companyDomainNameFamilyState(currentRowEntityId ?? ''), + ); + + return ( + { + if (!currentRowEntityId) return; + + await updateCompany({ + variables: { + id: currentRowEntityId, + domainName: domainName, + }, + }); + }} + /> + ); +} diff --git a/front/src/modules/companies/table/components/EditableCompanyEmployeesCell.tsx b/front/src/modules/companies/table/components/EditableCompanyEmployeesCell.tsx new file mode 100644 index 000000000..8643bb6be --- /dev/null +++ b/front/src/modules/companies/table/components/EditableCompanyEmployeesCell.tsx @@ -0,0 +1,33 @@ +import { useRecoilValue } from 'recoil'; + +import { companyEmployeesFamilyState } from '@/companies/states/companyEmployeesFamilyState'; +import { EditableCellText } from '@/ui/components/editable-cell/types/EditableCellText'; +import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId'; +import { useUpdateCompanyMutation } from '~/generated/graphql'; + +export function EditableCompanyEmployeesCell() { + const currentRowEntityId = useCurrentRowEntityId(); + + const [updateCompany] = useUpdateCompanyMutation(); + + const employees = useRecoilValue( + companyEmployeesFamilyState(currentRowEntityId ?? ''), + ); + + return ( + // TODO: Create an EditableCellNumber component + { + if (!currentRowEntityId) return; + + await updateCompany({ + variables: { + id: currentRowEntityId, + employees: parseInt(newEmployees), + }, + }); + }} + /> + ); +} diff --git a/front/src/modules/companies/table/components/EditableCompanyNameCell.tsx b/front/src/modules/companies/table/components/EditableCompanyNameCell.tsx new file mode 100644 index 000000000..3a267049e --- /dev/null +++ b/front/src/modules/companies/table/components/EditableCompanyNameCell.tsx @@ -0,0 +1,32 @@ +import { useRecoilValue } from 'recoil'; + +import { CompanyEditableNameChipCell } from '@/companies/components/CompanyEditableNameCell'; +import { companyCommentCountFamilyState } from '@/companies/states/companyCommentCountFamilyState'; +import { companyDomainNameFamilyState } from '@/companies/states/companyDomainNameFamilyState'; +import { companyNameFamilyState } from '@/companies/states/companyNameFamilyState'; +import { useCurrentRowEntityId } from '@/ui/tables/hooks/useCurrentEntityId'; + +export function EditableCompanyNameCell() { + const currentRowEntityId = useCurrentRowEntityId(); + + const name = useRecoilValue(companyNameFamilyState(currentRowEntityId ?? '')); + + const domainName = useRecoilValue( + companyDomainNameFamilyState(currentRowEntityId ?? ''), + ); + + const commentCount = useRecoilValue( + companyCommentCountFamilyState(currentRowEntityId ?? ''), + ); + + return ( + + ); +} diff --git a/front/src/modules/companies/table/components/companyColumns.tsx b/front/src/modules/companies/table/components/companyColumns.tsx new file mode 100644 index 000000000..b92ea6a17 --- /dev/null +++ b/front/src/modules/companies/table/components/companyColumns.tsx @@ -0,0 +1,61 @@ +import { TableColumn } from '@/people/table/components/peopleColumns'; +import { + IconBuildingSkyscraper, + IconCalendarEvent, + IconLink, + IconMap, + IconUser, + IconUsers, +} from '@/ui/icons/index'; + +import { EditableCompanyAccountOwnerCell } from './EditableCompanyAccountOwnerCell'; +import { EditableCompanyAddressCell } from './EditableCompanyAddressCell'; +import { EditableCompanyCreatedAtCell } from './EditableCompanyCreatedAtCell'; +import { EditableCompanyDomainNameCell } from './EditableCompanyDomainNameCell'; +import { EditableCompanyEmployeesCell } from './EditableCompanyEmployeesCell'; +import { EditableCompanyNameCell } from './EditableCompanyNameCell'; + +export const companyColumns: TableColumn[] = [ + { + id: 'name', + title: 'Name', + icon: , + size: 180, + cellComponent: , + }, + { + id: 'domainName', + title: 'URL', + icon: , + size: 100, + cellComponent: , + }, + { + id: 'employees', + title: 'Employees', + icon: , + size: 150, + cellComponent: , + }, + { + id: 'address', + title: 'Address', + icon: , + size: 170, + cellComponent: , + }, + { + id: 'createdAt', + title: 'Creation', + icon: , + size: 150, + cellComponent: , + }, + { + id: 'accountOwner', + title: 'Account owner', + icon: , + size: 150, + cellComponent: , + }, +]; diff --git a/front/src/modules/companies/table/hooks/useSetCompanyEntityTable.ts b/front/src/modules/companies/table/hooks/useSetCompanyEntityTable.ts new file mode 100644 index 000000000..d7030c373 --- /dev/null +++ b/front/src/modules/companies/table/hooks/useSetCompanyEntityTable.ts @@ -0,0 +1,88 @@ +import { useRecoilCallback } from 'recoil'; + +import { companyAccountOwnerFamilyState } from '@/companies/states/companyAccountOwnerFamilyState'; +import { companyAddressFamilyState } from '@/companies/states/companyAddressFamilyState'; +import { companyCommentCountFamilyState } from '@/companies/states/companyCommentCountFamilyState'; +import { companyCreatedAtFamilyState } from '@/companies/states/companyCreatedAtFamilyState'; +import { companyDomainNameFamilyState } from '@/companies/states/companyDomainNameFamilyState'; +import { companyEmployeesFamilyState } from '@/companies/states/companyEmployeesFamilyState'; +import { companyNameFamilyState } from '@/companies/states/companyNameFamilyState'; +import { GetCompaniesQuery } from '~/generated/graphql'; + +export function useSetCompanyEntityTable() { + return useRecoilCallback( + ({ set, snapshot }) => + (newCompanyArray: GetCompaniesQuery['companies']) => { + for (const company of newCompanyArray) { + const currentName = snapshot + .getLoadable(companyNameFamilyState(company.id)) + .valueOrThrow(); + + if (currentName !== company.name) { + set(companyNameFamilyState(company.id), company.name); + } + + const currentDomainName = snapshot + .getLoadable(companyDomainNameFamilyState(company.id)) + .valueOrThrow(); + + if (currentDomainName !== company.domainName) { + set(companyDomainNameFamilyState(company.id), company.domainName); + } + + const currentEmployees = snapshot + .getLoadable(companyEmployeesFamilyState(company.id)) + .valueOrThrow(); + + if (currentEmployees !== company.employees) { + set( + companyEmployeesFamilyState(company.id), + company.employees?.toString() ?? '', + ); + } + + const currentAddress = snapshot + .getLoadable(companyAddressFamilyState(company.id)) + .valueOrThrow(); + + if (currentAddress !== company.address) { + set(companyAddressFamilyState(company.id), company.address); + } + + const currentCommentCount = snapshot + .getLoadable(companyCommentCountFamilyState(company.id)) + .valueOrThrow(); + + if (currentCommentCount !== company._commentThreadCount) { + set( + companyCommentCountFamilyState(company.id), + company._commentThreadCount, + ); + } + + const currentAccountOwner = snapshot + .getLoadable(companyAccountOwnerFamilyState(company.id)) + .valueOrThrow(); + + if ( + JSON.stringify(currentAccountOwner) !== + JSON.stringify(company.accountOwner) + ) { + set( + companyAccountOwnerFamilyState(company.id), + company.accountOwner, + ); + } + + const currentCreatedAt = snapshot + .getLoadable(companyCreatedAtFamilyState(company.id)) + .valueOrThrow(); + + if (currentCreatedAt !== company.createdAt) { + set(companyCreatedAtFamilyState(company.id), company.createdAt); + } + } + }, + [], + ); +} diff --git a/front/src/modules/lib/filters-and-sorts/helpers.ts b/front/src/modules/lib/filters-and-sorts/helpers.ts index 26b6d773a..f2922f9b6 100644 --- a/front/src/modules/lib/filters-and-sorts/helpers.ts +++ b/front/src/modules/lib/filters-and-sorts/helpers.ts @@ -16,8 +16,8 @@ export const reduceSortsToOrderBy = ( sorts: Array>, ): OrderByTemplate[] => { const mappedSorts = sorts.map((sort) => { - if (sort._type === 'custom_sort') { - return sort.orderByTemplates.map((orderByTemplate) => + if (sort.orderByTemplates) { + return sort.orderByTemplates?.map((orderByTemplate) => orderByTemplate(mapOrderToOrder_By(sort.order)), ); } diff --git a/front/src/modules/lib/filters-and-sorts/interfaces/sorts/interface.ts b/front/src/modules/lib/filters-and-sorts/interfaces/sorts/interface.ts index e222f7ab3..1610e1b44 100644 --- a/front/src/modules/lib/filters-and-sorts/interfaces/sorts/interface.ts +++ b/front/src/modules/lib/filters-and-sorts/interfaces/sorts/interface.ts @@ -2,20 +2,12 @@ import { ReactNode } from 'react'; import { SortOrder as Order_By } from '~/generated/graphql'; -export type SortType = - | { - _type: 'default_sort'; - label: string; - key: keyof OrderByTemplate & string; - icon?: ReactNode; - } - | { - _type: 'custom_sort'; - label: string; - key: string; - icon?: ReactNode; - orderByTemplates: Array<(order: Order_By) => OrderByTemplate>; - }; +export type SortType = { + label: string; + key: string; + icon?: ReactNode; + orderByTemplates?: Array<(order: Order_By) => OrderByTemplate>; +}; export type SelectedSortType = SortType & { order: 'asc' | 'desc'; diff --git a/front/src/modules/people/components/EditablePeopleFullName.tsx b/front/src/modules/people/components/EditablePeopleFullName.tsx index 5b83de8e1..1e8393b91 100644 --- a/front/src/modules/people/components/EditablePeopleFullName.tsx +++ b/front/src/modules/people/components/EditablePeopleFullName.tsx @@ -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: + | Partial< + Pick + > + | 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 ( diff --git a/front/src/modules/people/components/PeopleCompanyCell.tsx b/front/src/modules/people/components/PeopleCompanyCell.tsx index 1b90c62ed..ac9eaa1d9 100644 --- a/front/src/modules/people/components/PeopleCompanyCell.tsx +++ b/front/src/modules/people/components/PeopleCompanyCell.tsx @@ -9,6 +9,10 @@ import { Company, Person } from '~/generated/graphql'; import { PeopleCompanyCreateCell } from './PeopleCompanyCreateCell'; import { PeopleCompanyPicker } from './PeopleCompanyPicker'; +export type PeopleWithCompany = Pick & { + company?: Pick | null; +}; + export type OwnProps = { people: Pick & { company?: Pick | null; diff --git a/front/src/modules/people/components/PeopleEntityTableData.tsx b/front/src/modules/people/components/PeopleEntityTableData.tsx new file mode 100644 index 000000000..c45ccc222 --- /dev/null +++ b/front/src/modules/people/components/PeopleEntityTableData.tsx @@ -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 <>; +} diff --git a/front/src/modules/people/components/PersonChip.tsx b/front/src/modules/people/components/PersonChip.tsx index e7d0d7d3f..f98b67f1b 100644 --- a/front/src/modules/people/components/PersonChip.tsx +++ b/front/src/modules/people/components/PersonChip.tsx @@ -50,6 +50,7 @@ const StyledName = styled.span` export function PersonChip({ id, name, picture }: PersonChipPropsType) { const ContainerComponent = id ? StyledContainerLink : StyledContainerNoLink; + return ( + (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, + }); + } + } + }, + [], + ); +} diff --git a/front/src/modules/people/services/select.ts b/front/src/modules/people/services/select.ts index dcdac5d67..76c84c9b4 100644 --- a/front/src/modules/people/services/select.ts +++ b/front/src/modules/people/services/select.ts @@ -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 + } + } +`; diff --git a/front/src/modules/people/services/show.ts b/front/src/modules/people/services/show.ts index 96a4e4a21..85594107b 100644 --- a/front/src/modules/people/services/show.ts +++ b/front/src/modules/people/services/show.ts @@ -9,7 +9,12 @@ export const GET_PERSON = gql` firstName lastName displayName + email createdAt + _commentThreadCount + company { + id + } } } `; diff --git a/front/src/modules/people/states/peopleCityFamilyState.ts b/front/src/modules/people/states/peopleCityFamilyState.ts new file mode 100644 index 000000000..8808ca711 --- /dev/null +++ b/front/src/modules/people/states/peopleCityFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const peopleCityFamilyState = atomFamily({ + key: 'peopleCityFamilyState', + default: null, +}); diff --git a/front/src/modules/people/states/peopleCompanyFamilyState.ts b/front/src/modules/people/states/peopleCompanyFamilyState.ts new file mode 100644 index 000000000..b8882a870 --- /dev/null +++ b/front/src/modules/people/states/peopleCompanyFamilyState.ts @@ -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, +}); diff --git a/front/src/modules/people/states/peopleCreatedAtFamilyState.ts b/front/src/modules/people/states/peopleCreatedAtFamilyState.ts new file mode 100644 index 000000000..1ef2d8abc --- /dev/null +++ b/front/src/modules/people/states/peopleCreatedAtFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const peopleCreatedAtFamilyState = atomFamily({ + key: 'peopleCreatedAtFamilyState', + default: null, +}); diff --git a/front/src/modules/people/states/peopleEmailFamilyState.ts b/front/src/modules/people/states/peopleEmailFamilyState.ts new file mode 100644 index 000000000..dd392b6ed --- /dev/null +++ b/front/src/modules/people/states/peopleEmailFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const peopleEmailFamilyState = atomFamily({ + key: 'peopleEmailFamilyState', + default: null, +}); diff --git a/front/src/modules/people/states/peopleEntityTableFamilyState.ts b/front/src/modules/people/states/peopleEntityTableFamilyState.ts new file mode 100644 index 000000000..d94947c9a --- /dev/null +++ b/front/src/modules/people/states/peopleEntityTableFamilyState.ts @@ -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, +}); diff --git a/front/src/modules/people/states/peopleNamesFamilyState.ts b/front/src/modules/people/states/peopleNamesFamilyState.ts new file mode 100644 index 000000000..03254fd5f --- /dev/null +++ b/front/src/modules/people/states/peopleNamesFamilyState.ts @@ -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, + }, +}); diff --git a/front/src/modules/people/states/peoplePhoneFamilyState.ts b/front/src/modules/people/states/peoplePhoneFamilyState.ts new file mode 100644 index 000000000..763e9688c --- /dev/null +++ b/front/src/modules/people/states/peoplePhoneFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const peoplePhoneFamilyState = atomFamily({ + key: 'peoplePhoneFamilyState', + default: null, +}); diff --git a/front/src/modules/people/table/components/EditablePeopleCityCell.tsx b/front/src/modules/people/table/components/EditablePeopleCityCell.tsx new file mode 100644 index 000000000..d34a6975f --- /dev/null +++ b/front/src/modules/people/table/components/EditablePeopleCityCell.tsx @@ -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 ( + { + if (!currentRowEntityId) return; + + await updatePerson({ + variables: { + id: currentRowEntityId, + city: newCity, + }, + }); + }} + /> + ); +} diff --git a/front/src/modules/people/table/components/EditablePeopleCompanyCell.tsx b/front/src/modules/people/table/components/EditablePeopleCompanyCell.tsx new file mode 100644 index 000000000..db6c7df70 --- /dev/null +++ b/front/src/modules/people/table/components/EditablePeopleCompanyCell.tsx @@ -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 ( + + ); +} diff --git a/front/src/modules/people/table/components/EditablePeopleCreatedAtCell.tsx b/front/src/modules/people/table/components/EditablePeopleCreatedAtCell.tsx new file mode 100644 index 000000000..51edec72a --- /dev/null +++ b/front/src/modules/people/table/components/EditablePeopleCreatedAtCell.tsx @@ -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 ( + { + if (!currentRowEntityId) return; + + await updatePerson({ + variables: { + id: currentRowEntityId, + createdAt: newDate.toISOString(), + }, + }); + }} + value={createdAt ? DateTime.fromISO(createdAt).toJSDate() : new Date()} + /> + ); +} diff --git a/front/src/modules/people/table/components/EditablePeopleEmailCell.tsx b/front/src/modules/people/table/components/EditablePeopleEmailCell.tsx new file mode 100644 index 000000000..2df9ef5ef --- /dev/null +++ b/front/src/modules/people/table/components/EditablePeopleEmailCell.tsx @@ -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 ( + { + if (!currentRowEntityId) return; + + await updatePerson({ + variables: { + id: currentRowEntityId, + email: newEmail, + }, + }); + }} + /> + ); +} diff --git a/front/src/modules/people/table/components/EditablePeopleFullNameCell.tsx b/front/src/modules/people/table/components/EditablePeopleFullNameCell.tsx new file mode 100644 index 000000000..9d3a9f8ec --- /dev/null +++ b/front/src/modules/people/table/components/EditablePeopleFullNameCell.tsx @@ -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 ( + { + if (!currentRowEntityId) return; + + await updatePerson({ + variables: { + id: currentRowEntityId, + firstName, + lastName, + }, + }); + }} + /> + ); +} diff --git a/front/src/modules/people/table/components/EditablePeoplePhoneCell.tsx b/front/src/modules/people/table/components/EditablePeoplePhoneCell.tsx new file mode 100644 index 000000000..b620065ea --- /dev/null +++ b/front/src/modules/people/table/components/EditablePeoplePhoneCell.tsx @@ -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 ( + { + if (!currentRowEntityId) return; + + await updatePerson({ + variables: { + id: currentRowEntityId, + phone: newPhone, + }, + }); + }} + /> + ); +} diff --git a/front/src/modules/people/table/components/peopleColumns.tsx b/front/src/modules/people/table/components/peopleColumns.tsx new file mode 100644 index 000000000..89e2e0dce --- /dev/null +++ b/front/src/modules/people/table/components/peopleColumns.tsx @@ -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: , + size: 210, + cellComponent: , + }, + { + id: 'email', + title: 'Email', + icon: , + size: 150, + cellComponent: , + }, + { + id: 'company', + title: 'Company', + icon: , + size: 150, + cellComponent: , + }, + { + id: 'phone', + title: 'Phone', + icon: , + size: 150, + cellComponent: , + }, + { + id: 'createdAt', + title: 'Creation', + icon: , + size: 150, + cellComponent: , + }, + { + id: 'city', + title: 'City', + icon: , + size: 150, + cellComponent: , + }, +]; diff --git a/front/src/modules/ui/components/editable-cell/CellSkeleton.tsx b/front/src/modules/ui/components/editable-cell/CellSkeleton.tsx new file mode 100644 index 000000000..f6f880c63 --- /dev/null +++ b/front/src/modules/ui/components/editable-cell/CellSkeleton.tsx @@ -0,0 +1,9 @@ +import Skeleton from 'react-loading-skeleton'; + +export function CellSkeleton() { + return ( +
+ +
+ ); +} diff --git a/front/src/modules/ui/components/editable-cell/types/EditableCellDoubleText.tsx b/front/src/modules/ui/components/editable-cell/types/EditableCellDoubleText.tsx index 2a22db373..4378f36a8 100644 --- a/front/src/modules/ui/components/editable-cell/types/EditableCellDoubleText.tsx +++ b/front/src/modules/ui/components/editable-cell/types/EditableCellDoubleText.tsx @@ -1,7 +1,8 @@ -import { ReactElement } from 'react'; +import { ReactElement, useState } from 'react'; import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope'; +import { CellSkeleton } from '../CellSkeleton'; import { EditableCell } from '../EditableCell'; import { EditableCellDoubleTextEditMode } from './EditableCellDoubleTextEditMode'; @@ -13,6 +14,7 @@ type OwnProps = { secondValuePlaceholder: string; nonEditModeContent: ReactElement; onChange: (firstValue: string, secondValue: string) => void; + loading?: boolean; }; export function EditableCellDoubleText({ @@ -22,20 +24,30 @@ export function EditableCellDoubleText({ secondValuePlaceholder, onChange, nonEditModeContent, + loading, }: OwnProps) { + const [firstInternalValue, setFirstInternalValue] = useState(firstValue); + const [secondInternalValue, setSecondInternalValue] = useState(secondValue); + + function handleOnChange(firstValue: string, secondValue: string): void { + setFirstInternalValue(firstValue); + setSecondInternalValue(secondValue); + onChange(firstValue, secondValue); + } + return ( } - nonEditModeContent={nonEditModeContent} + nonEditModeContent={loading ? : nonEditModeContent} > ); } diff --git a/front/src/modules/ui/components/editable-cell/types/EditableCellPhone.tsx b/front/src/modules/ui/components/editable-cell/types/EditableCellPhone.tsx index 7decf9c65..a2971d9e8 100644 --- a/front/src/modules/ui/components/editable-cell/types/EditableCellPhone.tsx +++ b/front/src/modules/ui/components/editable-cell/types/EditableCellPhone.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useRef, useState } from 'react'; +import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { InplaceInputPhoneDisplayMode } from '@/ui/inplace-inputs/components/InplaceInputPhoneDisplayMode'; import { InplaceInputTextEditMode } from '@/ui/inplace-inputs/components/InplaceInputTextEditMode'; @@ -8,17 +8,17 @@ import { EditableCell } from '../EditableCell'; type OwnProps = { placeholder?: string; value: string; - changeHandler: (updated: string) => void; + onChange: (updated: string) => void; }; -export function EditableCellPhone({ - value, - placeholder, - changeHandler, -}: OwnProps) { +export function EditableCellPhone({ value, placeholder, onChange }: OwnProps) { const inputRef = useRef(null); const [inputValue, setInputValue] = useState(value); + useEffect(() => { + setInputValue(value); + }, [value]); + return ( ) => { setInputValue(event.target.value); - changeHandler(event.target.value); + onChange(event.target.value); }} /> } diff --git a/front/src/modules/ui/components/editable-cell/types/EditableCellText.tsx b/front/src/modules/ui/components/editable-cell/types/EditableCellText.tsx index a0edcfc4c..74e2c2933 100644 --- a/front/src/modules/ui/components/editable-cell/types/EditableCellText.tsx +++ b/front/src/modules/ui/components/editable-cell/types/EditableCellText.tsx @@ -1,9 +1,9 @@ -import { ChangeEvent, useMemo, useState } from 'react'; +import { ChangeEvent, useEffect, useState } from 'react'; import { InplaceInputTextDisplayMode } from '@/ui/inplace-inputs/components/InplaceInputTextDisplayMode'; import { InplaceInputTextEditMode } from '@/ui/inplace-inputs/components/InplaceInputTextEditMode'; -import { debounce } from '@/utils/debounce'; +import { CellSkeleton } from '../CellSkeleton'; import { EditableCell } from '../EditableCell'; type OwnProps = { @@ -11,6 +11,7 @@ type OwnProps = { value: string; onChange: (newValue: string) => void; editModeHorizontalAlign?: 'left' | 'right'; + loading?: boolean; }; export function EditableCellText({ @@ -18,12 +19,13 @@ export function EditableCellText({ placeholder, onChange, editModeHorizontalAlign, + loading, }: OwnProps) { const [internalValue, setInternalValue] = useState(value); - const debouncedOnChange = useMemo(() => { - return debounce(onChange, 200); - }, [onChange]); + useEffect(() => { + setInternalValue(value); + }, [value]); return ( ) => { setInternalValue(event.target.value); - debouncedOnChange(event.target.value); + onChange(event.target.value); }} /> } nonEditModeContent={ - - {internalValue} - + loading ? ( + + ) : ( + + {internalValue} + + ) } > ); diff --git a/front/src/modules/ui/components/editable-cell/types/EditableChip.tsx b/front/src/modules/ui/components/editable-cell/types/EditableChip.tsx index 3ff79ae68..c63a9fd0a 100644 --- a/front/src/modules/ui/components/editable-cell/types/EditableChip.tsx +++ b/front/src/modules/ui/components/editable-cell/types/EditableChip.tsx @@ -1,4 +1,11 @@ -import { ChangeEvent, ComponentType, ReactNode, useRef, useState } from 'react'; +import { + ChangeEvent, + ComponentType, + ReactNode, + useEffect, + useRef, + useState, +} from 'react'; import styled from '@emotion/styled'; import { textInputStyle } from '@/ui/themes/effects'; @@ -55,6 +62,10 @@ export function EditableCellChip({ const inputRef = useRef(null); const [inputValue, setInputValue] = useState(value); + useEffect(() => { + setInputValue(value); + }, [value]); + const handleRightEndContentClick = ( event: React.MouseEvent, ) => { diff --git a/front/src/modules/ui/components/form/Checkbox.tsx b/front/src/modules/ui/components/form/Checkbox.tsx index 1997fbc6f..95bf2fd44 100644 --- a/front/src/modules/ui/components/form/Checkbox.tsx +++ b/front/src/modules/ui/components/form/Checkbox.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import styled from '@emotion/styled'; type OwnProps = { - name?: string; - id?: string; - checked?: boolean; + checked: boolean; indeterminate?: boolean; onChange?: (newCheckedValue: boolean) => void; }; @@ -41,13 +39,7 @@ const StyledContainer = styled.div` } `; -export function Checkbox({ - name, - id, - checked, - onChange, - indeterminate, -}: OwnProps) { +export function Checkbox({ checked, onChange, indeterminate }: OwnProps) { const ref = React.useRef(null); React.useEffect(() => { @@ -57,10 +49,8 @@ export function Checkbox({ } }, [ref, indeterminate, checked]); - function handleInputChange(event: React.ChangeEvent) { - if (onChange) { - onChange(event.target.checked); - } + function handleChange(event: React.ChangeEvent) { + onChange?.(event.target.checked); } return ( @@ -69,10 +59,8 @@ export function Checkbox({ ref={ref} type="checkbox" data-testid="input-checkbox" - id={id} - name={name} checked={checked} - onChange={handleInputChange} + onChange={handleChange} /> ); diff --git a/front/src/modules/ui/components/menu/DropdownMenuCheckableItem.tsx b/front/src/modules/ui/components/menu/DropdownMenuCheckableItem.tsx index 75b68588e..8997bd8b0 100644 --- a/front/src/modules/ui/components/menu/DropdownMenuCheckableItem.tsx +++ b/front/src/modules/ui/components/menu/DropdownMenuCheckableItem.tsx @@ -35,7 +35,6 @@ const StyledChildrenContainer = styled.div` export function DropdownMenuCheckableItem({ checked, onChange, - id, children, }: React.PropsWithChildren) { function handleClick() { @@ -45,7 +44,7 @@ export function DropdownMenuCheckableItem({ return ( - + {children} diff --git a/front/src/modules/ui/components/table/CheckboxCell.tsx b/front/src/modules/ui/components/table/CheckboxCell.tsx index fd335e2e3..b9beafea0 100644 --- a/front/src/modules/ui/components/table/CheckboxCell.tsx +++ b/front/src/modules/ui/components/table/CheckboxCell.tsx @@ -2,18 +2,11 @@ import * as React from 'react'; import styled from '@emotion/styled'; import { useSetRecoilState } from 'recoil'; +import { useCurrentRowSelected } from '@/ui/tables/hooks/useCurrentRowSelected'; import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPositionState'; import { Checkbox } from '../form/Checkbox'; -type OwnProps = { - name: string; - id: string; - checked?: boolean; - indeterminate?: boolean; - onChange?: (newCheckedValue: boolean) => void; -}; - const StyledContainer = styled.div` align-items: center; @@ -24,31 +17,19 @@ const StyledContainer = styled.div` justify-content: center; `; -export function CheckboxCell({ - name, - id, - checked, - onChange, - indeterminate, -}: OwnProps) { - const [internalChecked, setInternalChecked] = React.useState(checked); +export function CheckboxCell() { const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); + const { currentRowSelected, setCurrentRowSelected } = useCurrentRowSelected(); + function handleContainerClick() { - handleCheckboxChange(!internalChecked); + handleCheckboxChange(!currentRowSelected); } - React.useEffect(() => { - setInternalChecked(checked); - }, [checked]); - function handleCheckboxChange(newCheckedValue: boolean) { - setInternalChecked(newCheckedValue); - setContextMenuPosition({ x: null, y: null }); + setCurrentRowSelected(newCheckedValue); - if (onChange) { - onChange(newCheckedValue); - } + setContextMenuPosition({ x: null, y: null }); } return ( @@ -56,13 +37,7 @@ export function CheckboxCell({ onClick={handleContainerClick} data-testid="input-checkbox-cell-container" > - + ); } diff --git a/front/src/modules/ui/components/table/EntityTable.tsx b/front/src/modules/ui/components/table/EntityTable.tsx index f29deb492..534220dc5 100644 --- a/front/src/modules/ui/components/table/EntityTable.tsx +++ b/front/src/modules/ui/components/table/EntityTable.tsx @@ -1,36 +1,17 @@ import * as React from 'react'; import styled from '@emotion/styled'; -import { - ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table'; -import { useRecoilState } from 'recoil'; import { SelectedSortType, SortType, } from '@/lib/filters-and-sorts/interfaces/sorts/interface'; -import { RecoilScope } from '@/recoil-scope/components/RecoilScope'; +import { TableColumn } from '@/people/table/components/peopleColumns'; import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef'; import { useLeaveTableFocus } from '@/ui/tables/hooks/useLeaveTableFocus'; -import { RowContext } from '@/ui/tables/states/RowContext'; - -import { currentRowSelectionState } from '../../tables/states/rowSelectionState'; import { TableHeader } from './table-header/TableHeader'; -import { EntityTableRow } from './EntityTableRow'; - -type OwnProps = { - data: Array; - columns: Array>; - viewName: string; - viewIcon?: React.ReactNode; - availableSorts?: Array>; - onSortsUpdate?: (sorts: Array>) => void; - onRowSelectionChange?: (rowSelection: string[]) => void; -}; +import { EntityTableBody } from './EntityTableBody'; +import { EntityTableHeader } from './EntityTableHeader'; const StyledTable = styled.table` border-collapse: collapse; @@ -91,32 +72,24 @@ const StyledTableWithHeader = styled.div` width: 100%; `; -export function EntityTable({ - data, +type OwnProps = { + columns: Array; + viewName: string; + viewIcon?: React.ReactNode; + availableSorts?: Array>; + onSortsUpdate?: (sorts: Array>) => void; + onRowSelectionChange?: (rowSelection: string[]) => void; +}; + +export function EntityTable({ columns, viewName, viewIcon, availableSorts, onSortsUpdate, -}: OwnProps) { +}: OwnProps) { const tableBodyRef = React.useRef(null); - const [currentRowSelection, setCurrentRowSelection] = useRecoilState( - currentRowSelectionState, - ); - - const table = useReactTable({ - data, - columns, - state: { - rowSelection: currentRowSelection, - }, - getCoreRowModel: getCoreRowModel(), - enableRowSelection: true, - onRowSelectionChange: setCurrentRowSelection, - getRowId: (row) => row.id, - }); - const leaveTableFocus = useLeaveTableFocus(); useListenClickOutsideArrayOfRef([tableBodyRef], () => { @@ -133,37 +106,8 @@ export function EntityTable({ />
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} - - - ))} - - - {table.getRowModel().rows.map((row, index) => ( - - - - ))} - + +
diff --git a/front/src/modules/ui/components/table/EntityTableBody.tsx b/front/src/modules/ui/components/table/EntityTableBody.tsx new file mode 100644 index 000000000..b23ad8cd7 --- /dev/null +++ b/front/src/modules/ui/components/table/EntityTableBody.tsx @@ -0,0 +1,33 @@ +import { useRecoilValue } from 'recoil'; + +import { TableColumn } from '@/people/table/components/peopleColumns'; +import { RecoilScope } from '@/recoil-scope/components/RecoilScope'; +import { isFetchingEntityTableDataState } from '@/ui/tables/states/isFetchingEntityTableDataState'; +import { RowContext } from '@/ui/tables/states/RowContext'; +import { tableRowIdsState } from '@/ui/tables/states/tableRowIdsState'; + +import { EntityTableRow } from './EntityTableRow'; + +export function EntityTableBody({ columns }: { columns: Array }) { + const rowIds = useRecoilValue(tableRowIdsState); + + const isFetchingEntityTableData = useRecoilValue( + isFetchingEntityTableDataState, + ); + + return ( + + {!isFetchingEntityTableData ? ( + rowIds.map((rowId, index) => ( + + + + )) + ) : ( + + loading... + + )} + + ); +} diff --git a/front/src/modules/ui/components/table/EntityTableCell.tsx b/front/src/modules/ui/components/table/EntityTableCell.tsx index cb755dfd2..08067932d 100644 --- a/front/src/modules/ui/components/table/EntityTableCell.tsx +++ b/front/src/modules/ui/components/table/EntityTableCell.tsx @@ -1,6 +1,4 @@ import { useEffect } from 'react'; -import { flexRender } from '@tanstack/react-table'; -import { Cell, Row } from '@tanstack/table-core'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState'; @@ -9,14 +7,16 @@ import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPosition import { currentColumnNumberScopedState } from '@/ui/tables/states/currentColumnNumberScopedState'; import { currentRowSelectionState } from '@/ui/tables/states/rowSelectionState'; -export function EntityTableCell({ - row, - cell, +export function EntityTableCell({ + rowId, cellIndex, + children, + size, }: { - row: Row; - cell: Cell; + size: number; + rowId: string; cellIndex: number; + children: React.ReactNode; }) { const [, setCurrentRowSelection] = useRecoilState(currentRowSelectionState); @@ -43,14 +43,14 @@ export function EntityTableCell({ return ( handleContextMenu(event, row.original.id)} + onContextMenu={(event) => handleContextMenu(event, rowId)} style={{ - width: cell.column.getSize(), - minWidth: cell.column.getSize(), - maxWidth: cell.column.getSize(), + width: size, + minWidth: size, + maxWidth: size, }} > - {flexRender(cell.column.columnDef.cell, cell.getContext())} + {children} ); } diff --git a/front/src/modules/ui/components/table/EntityTableHeader.tsx b/front/src/modules/ui/components/table/EntityTableHeader.tsx new file mode 100644 index 000000000..292962fea --- /dev/null +++ b/front/src/modules/ui/components/table/EntityTableHeader.tsx @@ -0,0 +1,39 @@ +import { TableColumn } from '@/people/table/components/peopleColumns'; + +import { ColumnHead } from './ColumnHead'; +import { SelectAllCheckbox } from './SelectAllCheckbox'; + +export function EntityTableHeader({ + columns, +}: { + columns: Array; +}) { + return ( + + + + + + {columns.map((column) => ( + + + + ))} + + + + ); +} diff --git a/front/src/modules/ui/components/table/EntityTableRow.tsx b/front/src/modules/ui/components/table/EntityTableRow.tsx index bbf5c4298..a2648605c 100644 --- a/front/src/modules/ui/components/table/EntityTableRow.tsx +++ b/front/src/modules/ui/components/table/EntityTableRow.tsx @@ -1,15 +1,17 @@ import { useEffect } from 'react'; import styled from '@emotion/styled'; -import { Row } from '@tanstack/table-core'; import { useRecoilState } from 'recoil'; +import { TableColumn } from '@/people/table/components/peopleColumns'; import { RecoilScope } from '@/recoil-scope/components/RecoilScope'; import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState'; import { CellContext } from '@/ui/tables/states/CellContext'; +import { currentRowEntityIdScopedState } from '@/ui/tables/states/currentRowEntityIdScopedState'; import { currentRowNumberScopedState } from '@/ui/tables/states/currentRowNumberScopedState'; import { RowContext } from '@/ui/tables/states/RowContext'; import { currentRowSelectionState } from '@/ui/tables/states/rowSelectionState'; +import { CheckboxCell } from './CheckboxCell'; import { EntityTableCell } from './EntityTableCell'; const StyledRow = styled.tr<{ selected: boolean }>` @@ -17,42 +19,56 @@ const StyledRow = styled.tr<{ selected: boolean }>` props.selected ? props.theme.background.secondary : 'none'}; `; -export function EntityTableRow({ - row, +export function EntityTableRow({ + columns, + rowId, index, }: { - row: Row; + columns: TableColumn[]; + rowId: string; index: number; }) { const [currentRowSelection] = useRecoilState(currentRowSelectionState); + const [currentRowEntityId, setCurrentRowEntityId] = useRecoilScopedState( + currentRowEntityIdScopedState, + RowContext, + ); const [, setCurrentRowNumber] = useRecoilScopedState( currentRowNumberScopedState, RowContext, ); + useEffect(() => { + if (currentRowEntityId !== rowId) { + setCurrentRowEntityId(rowId); + } + }, [rowId, setCurrentRowEntityId, currentRowEntityId]); + useEffect(() => { setCurrentRowNumber(index); }, [index, setCurrentRowNumber]); return ( - {row.getVisibleCells().map((cell, cellIndex) => { + + + + {columns.map((column, columnIndex) => { return ( - + - - row={row} - cell={cell} - cellIndex={cellIndex} - /> + + {column.cellComponent} + ); diff --git a/front/src/modules/ui/components/table/HooksEntityTable.tsx b/front/src/modules/ui/components/table/HooksEntityTable.tsx index 198122331..a4fe59294 100644 --- a/front/src/modules/ui/components/table/HooksEntityTable.tsx +++ b/front/src/modules/ui/components/table/HooksEntityTable.tsx @@ -5,22 +5,19 @@ import { useMapKeyboardToSoftFocus } from '@/ui/tables/hooks/useMapKeyboardToSof export function HooksEntityTable({ numberOfColumns, - numberOfRows, - availableTableFilters, + availableFilters, }: { numberOfColumns: number; - numberOfRows: number; - availableTableFilters: FilterDefinition[]; + availableFilters: FilterDefinition[]; }) { useMapKeyboardToSoftFocus(); useInitializeEntityTable({ numberOfColumns, - numberOfRows, }); useInitializeEntityTableFilters({ - availableTableFilters, + availableFilters, }); return <>; diff --git a/front/src/modules/ui/components/table/SelectAllCheckbox.tsx b/front/src/modules/ui/components/table/SelectAllCheckbox.tsx index 005d91104..88e0de08e 100644 --- a/front/src/modules/ui/components/table/SelectAllCheckbox.tsx +++ b/front/src/modules/ui/components/table/SelectAllCheckbox.tsx @@ -1,18 +1,36 @@ -import { CheckboxCell } from './CheckboxCell'; +import React from 'react'; +import styled from '@emotion/styled'; + +import { useSelectAllRows } from '@/ui/tables/hooks/useSelectAllRows'; + +import { Checkbox } from '../form/Checkbox'; + +const StyledContainer = styled.div` + align-items: center; + + cursor: pointer; + display: flex; + height: 32px; + + justify-content: center; +`; + +export const SelectAllCheckbox = () => { + const { selectAllRows, allRowsSelectedStatus } = useSelectAllRows(); + + function handleContainerClick() { + selectAllRows(); + } + + const checked = allRowsSelectedStatus === 'all'; + const indeterminate = allRowsSelectedStatus === 'some'; -export const SelectAllCheckbox = ({ - indeterminate, - onChange, -}: { - indeterminate?: boolean; - onChange?: (newCheckedValue: boolean) => void; -} & React.HTMLProps) => { return ( - + + + ); }; diff --git a/front/src/modules/ui/tables/constants/index.ts b/front/src/modules/ui/tables/constants/index.ts deleted file mode 100644 index f7a07d372..000000000 --- a/front/src/modules/ui/tables/constants/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN = 1; diff --git a/front/src/modules/ui/tables/hooks/useCurrentEntityId.ts b/front/src/modules/ui/tables/hooks/useCurrentEntityId.ts new file mode 100644 index 000000000..ebd08fc11 --- /dev/null +++ b/front/src/modules/ui/tables/hooks/useCurrentEntityId.ts @@ -0,0 +1,18 @@ +import { useRecoilScopedValue } from '@/recoil-scope/hooks/useRecoilScopedValue'; + +import { currentRowEntityIdScopedState } from '../states/currentRowEntityIdScopedState'; +import { RowContext } from '../states/RowContext'; + +export type TableDimensions = { + numberOfColumns: number; + numberOfRows: number; +}; + +export function useCurrentRowEntityId() { + const currentRowEntityIdScoped = useRecoilScopedValue( + currentRowEntityIdScopedState, + RowContext, + ); + + return currentRowEntityIdScoped; +} diff --git a/front/src/modules/ui/tables/hooks/useCurrentRowSelected.ts b/front/src/modules/ui/tables/hooks/useCurrentRowSelected.ts new file mode 100644 index 000000000..cc46df59f --- /dev/null +++ b/front/src/modules/ui/tables/hooks/useCurrentRowSelected.ts @@ -0,0 +1,43 @@ +import { useRecoilCallback, useRecoilState } from 'recoil'; + +import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState'; +import { numberOfSelectedRowState } from '../states/numberOfSelectedRowState'; + +import { useCurrentRowEntityId } from './useCurrentEntityId'; + +export function useCurrentRowSelected() { + const currentRowId = useCurrentRowEntityId(); + + const [isRowSelected] = useRecoilState( + isRowSelectedFamilyState(currentRowId ?? ''), + ); + + const setCurrentRowSelected = useRecoilCallback( + ({ set, snapshot }) => + (newSelectedState: boolean) => { + if (!currentRowId) return; + + const isRowSelected = snapshot + .getLoadable(isRowSelectedFamilyState(currentRowId)) + .valueOrThrow(); + + const numberOfSelectedRow = snapshot + .getLoadable(numberOfSelectedRowState) + .valueOrThrow(); + + if (newSelectedState && !isRowSelected) { + set(numberOfSelectedRowState, numberOfSelectedRow + 1); + set(isRowSelectedFamilyState(currentRowId), true); + } else if (!newSelectedState && isRowSelected) { + set(numberOfSelectedRowState, numberOfSelectedRow - 1); + set(isRowSelectedFamilyState(currentRowId), false); + } + }, + [currentRowId], + ); + + return { + currentRowSelected: isRowSelected, + setCurrentRowSelected, + }; +} diff --git a/front/src/modules/ui/tables/hooks/useInitializeEntityTable.ts b/front/src/modules/ui/tables/hooks/useInitializeEntityTable.ts index c9445e220..1e7de1c14 100644 --- a/front/src/modules/ui/tables/hooks/useInitializeEntityTable.ts +++ b/front/src/modules/ui/tables/hooks/useInitializeEntityTable.ts @@ -1,21 +1,25 @@ import { useEffect } from 'react'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { entityTableDimensionsState } from '../states/entityTableDimensionsState'; +import { tableRowIdsState } from '../states/tableRowIdsState'; import { useResetTableRowSelection } from './useResetTableRowSelection'; export type TableDimensions = { - numberOfRows: number; numberOfColumns: number; + numberOfRows: number; }; export function useInitializeEntityTable({ - numberOfRows, numberOfColumns, -}: TableDimensions) { +}: { + numberOfColumns: number; +}) { const resetTableRowSelection = useResetTableRowSelection(); + const tableRowIds = useRecoilValue(tableRowIdsState); + useEffect(() => { resetTableRowSelection(); }, [resetTableRowSelection]); @@ -25,7 +29,7 @@ export function useInitializeEntityTable({ useEffect(() => { setTableDimensions({ numberOfColumns, - numberOfRows, + numberOfRows: tableRowIds?.length, }); - }, [numberOfRows, numberOfColumns, setTableDimensions]); + }, [tableRowIds, numberOfColumns, setTableDimensions]); } diff --git a/front/src/modules/ui/tables/hooks/useInitializeEntityTableFilters.ts b/front/src/modules/ui/tables/hooks/useInitializeEntityTableFilters.ts index 52935e128..de9f4aa7e 100644 --- a/front/src/modules/ui/tables/hooks/useInitializeEntityTableFilters.ts +++ b/front/src/modules/ui/tables/hooks/useInitializeEntityTableFilters.ts @@ -7,16 +7,16 @@ import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState' import { TableContext } from '../states/TableContext'; export function useInitializeEntityTableFilters({ - availableTableFilters, + availableFilters, }: { - availableTableFilters: FilterDefinition[]; + availableFilters: FilterDefinition[]; }) { - const [, setAvailableTableFilters] = useRecoilScopedState( + const [, setAvailableFilters] = useRecoilScopedState( availableFiltersScopedState, TableContext, ); useEffect(() => { - setAvailableTableFilters(availableTableFilters); - }, [setAvailableTableFilters, availableTableFilters]); + setAvailableFilters(availableFilters); + }, [setAvailableFilters, availableFilters]); } diff --git a/front/src/modules/ui/tables/hooks/useMoveSoftFocus.ts b/front/src/modules/ui/tables/hooks/useMoveSoftFocus.ts index 54c33c814..800bda596 100644 --- a/front/src/modules/ui/tables/hooks/useMoveSoftFocus.ts +++ b/front/src/modules/ui/tables/hooks/useMoveSoftFocus.ts @@ -1,6 +1,5 @@ import { useRecoilCallback } from 'recoil'; -import { TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN } from '../constants'; import { numberOfTableColumnsSelectorState } from '../states/numberOfTableColumnsSelectorState'; import { numberOfTableRowsSelectorState } from '../states/numberOfTableRowsSelectorState'; import { softFocusPositionState } from '../states/softFocusPositionState'; @@ -98,7 +97,7 @@ export function useMoveSoftFocus() { } else if (isLastColumnButNotLastRow) { setSoftFocusPosition({ row: currentRowNumber + 1, - column: TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN, + column: 0, }); } }, @@ -120,18 +119,12 @@ export function useMoveSoftFocus() { const currentRowNumber = softFocusPosition.row; const isFirstRowAndFirstColumn = - currentColumnNumber === - TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN && - currentRowNumber === 0; + currentColumnNumber === 0 && currentRowNumber === 0; const isFirstColumnButNotFirstRow = - currentColumnNumber === - TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN && - currentRowNumber > 0; + currentColumnNumber === 0 && currentRowNumber > 0; - const isNotFirstColumn = - currentColumnNumber > - TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN; + const isNotFirstColumn = currentColumnNumber > 0; if (isFirstRowAndFirstColumn) { return; @@ -149,7 +142,7 @@ export function useMoveSoftFocus() { }); } }, - [setSoftFocusPosition, TABLE_MIN_COLUMN_NUMBER_BECAUSE_OF_CHECKBOX_COLUMN], + [setSoftFocusPosition], ); return { diff --git a/front/src/modules/ui/tables/hooks/useSelectAllRows.ts b/front/src/modules/ui/tables/hooks/useSelectAllRows.ts new file mode 100644 index 000000000..e644ed5ce --- /dev/null +++ b/front/src/modules/ui/tables/hooks/useSelectAllRows.ts @@ -0,0 +1,48 @@ +import { useRecoilCallback, useRecoilValue } from 'recoil'; + +import { allRowsSelectedStatusSelector } from '../states/allRowsSelectedStatusSelector'; +import { isRowSelectedFamilyState } from '../states/isRowSelectedFamilyState'; +import { numberOfSelectedRowState } from '../states/numberOfSelectedRowState'; +import { numberOfTableRowsSelectorState } from '../states/numberOfTableRowsSelectorState'; +import { tableRowIdsState } from '../states/tableRowIdsState'; + +export function useSelectAllRows() { + const allRowsSelectedStatus = useRecoilValue(allRowsSelectedStatusSelector); + + const selectAllRows = useRecoilCallback( + ({ set, snapshot }) => + () => { + const allRowsSelectedStatus = snapshot + .getLoadable(allRowsSelectedStatusSelector) + .valueOrThrow(); + + const numberOfRows = snapshot + .getLoadable(numberOfTableRowsSelectorState) + .valueOrThrow(); + + const tableRowIds = snapshot + .getLoadable(tableRowIdsState) + .valueOrThrow(); + + if (allRowsSelectedStatus === 'none') { + set(numberOfSelectedRowState, numberOfRows); + + for (const rowId of tableRowIds) { + set(isRowSelectedFamilyState(rowId), true); + } + } else { + set(numberOfSelectedRowState, 0); + + for (const rowId of tableRowIds) { + set(isRowSelectedFamilyState(rowId), false); + } + } + }, + [], + ); + + return { + allRowsSelectedStatus, + selectAllRows, + }; +} diff --git a/front/src/modules/ui/tables/states/allRowsSelectedStatusSelector.ts b/front/src/modules/ui/tables/states/allRowsSelectedStatusSelector.ts new file mode 100644 index 000000000..09ebde969 --- /dev/null +++ b/front/src/modules/ui/tables/states/allRowsSelectedStatusSelector.ts @@ -0,0 +1,24 @@ +import { selector } from 'recoil'; + +import { AllRowsSelectedStatus } from '../types/AllRowSelectedStatus'; + +import { numberOfSelectedRowState } from './numberOfSelectedRowState'; +import { numberOfTableRowsSelectorState } from './numberOfTableRowsSelectorState'; + +export const allRowsSelectedStatusSelector = selector({ + key: 'allRowsSelectedStatusSelector', + get: ({ get }) => { + const numberOfRows = get(numberOfTableRowsSelectorState); + + const numberOfSelectedRows = get(numberOfSelectedRowState); + + const allRowsSelectedStatus = + numberOfSelectedRows === 0 + ? 'none' + : numberOfRows === numberOfSelectedRows + ? 'all' + : 'some'; + + return allRowsSelectedStatus; + }, +}); diff --git a/front/src/modules/ui/tables/states/currentRowEntityIdScopedState.ts b/front/src/modules/ui/tables/states/currentRowEntityIdScopedState.ts new file mode 100644 index 000000000..194aac930 --- /dev/null +++ b/front/src/modules/ui/tables/states/currentRowEntityIdScopedState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const currentRowEntityIdScopedState = atomFamily({ + key: 'currentRowEntityIdScopedState', + default: null, +}); diff --git a/front/src/modules/ui/tables/states/isFetchingEntityTableDataState.ts b/front/src/modules/ui/tables/states/isFetchingEntityTableDataState.ts new file mode 100644 index 000000000..40d89f730 --- /dev/null +++ b/front/src/modules/ui/tables/states/isFetchingEntityTableDataState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isFetchingEntityTableDataState = atom({ + key: 'isFetchingEntityTableDataState', + default: true, +}); diff --git a/front/src/modules/ui/tables/states/isRowSelectedFamilyState.ts b/front/src/modules/ui/tables/states/isRowSelectedFamilyState.ts new file mode 100644 index 000000000..70ef414b8 --- /dev/null +++ b/front/src/modules/ui/tables/states/isRowSelectedFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const isRowSelectedFamilyState = atomFamily({ + key: 'isRowSelectedFamilyState', + default: false, +}); diff --git a/front/src/modules/ui/tables/states/numberOfSelectedRowState.ts b/front/src/modules/ui/tables/states/numberOfSelectedRowState.ts new file mode 100644 index 000000000..2169e9fbb --- /dev/null +++ b/front/src/modules/ui/tables/states/numberOfSelectedRowState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const numberOfSelectedRowState = atom({ + key: 'numberOfSelectedRowState', + default: 0, +}); diff --git a/front/src/modules/ui/tables/states/tableRowIdsState.ts b/front/src/modules/ui/tables/states/tableRowIdsState.ts new file mode 100644 index 000000000..e7832f409 --- /dev/null +++ b/front/src/modules/ui/tables/states/tableRowIdsState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const tableRowIdsState = atom({ + key: 'tableRowIdsState', + default: [], +}); diff --git a/front/src/modules/ui/tables/types/AllRowSelectedStatus.ts b/front/src/modules/ui/tables/types/AllRowSelectedStatus.ts new file mode 100644 index 000000000..d37df60de --- /dev/null +++ b/front/src/modules/ui/tables/types/AllRowSelectedStatus.ts @@ -0,0 +1 @@ +export type AllRowsSelectedStatus = 'none' | 'some' | 'all'; diff --git a/front/src/modules/ui/tables/utils/getCheckBoxColumn.tsx b/front/src/modules/ui/tables/utils/getCheckBoxColumn.tsx deleted file mode 100644 index 224b5e48f..000000000 --- a/front/src/modules/ui/tables/utils/getCheckBoxColumn.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { CellContext } from '@tanstack/react-table'; - -import { CheckboxCell } from '@/ui/components/table/CheckboxCell'; -import { SelectAllCheckbox } from '@/ui/components/table/SelectAllCheckbox'; - -export function getCheckBoxColumn() { - return { - id: 'select', - header: ({ table }: any) => ( - table.toggleAllRowsSelected(newValue)} - /> - ), - cell: (props: CellContext) => ( - props.row.toggleSelected(newValue)} - /> - ), - size: 32, - maxSize: 32, - }; -} diff --git a/front/src/pages/companies/CompanyTable.tsx b/front/src/pages/companies/CompanyTable.tsx index 803381ad7..94ffa7f09 100644 --- a/front/src/pages/companies/CompanyTable.tsx +++ b/front/src/pages/companies/CompanyTable.tsx @@ -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 ( <> + } availableSorts={availableSorts} diff --git a/front/src/pages/companies/CompanyTableMockMode.tsx b/front/src/pages/companies/CompanyTableMockMode.tsx index 07ef66e56..5a596d575 100644 --- a/front/src/pages/companies/CompanyTableMockMode.tsx +++ b/front/src/pages/companies/CompanyTableMockMode.tsx @@ -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 ( <> } availableSorts={availableSorts} diff --git a/front/src/pages/companies/__stories__/Companies.sortBy.stories.tsx b/front/src/pages/companies/__stories__/Companies.sortBy.stories.tsx index 1fc2bba2c..34980a62e 100644 --- a/front/src/pages/companies/__stories__/Companies.sortBy.stories.tsx +++ b/front/src/pages/companies/__stories__/Companies.sortBy.stories.tsx @@ -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); diff --git a/front/src/pages/companies/companies-columns.tsx b/front/src/pages/companies/companies-columns.tsx deleted file mode 100644 index 18974f698..000000000 --- a/front/src/pages/companies/companies-columns.tsx +++ /dev/null @@ -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(); - -export const useCompaniesColumns = () => { - const [updateCompany] = useUpdateCompanyMutation(); - return useMemo(() => { - return [ - getCheckBoxColumn(), - columnHelper.accessor('name', { - header: () => ( - } - /> - ), - cell: (props) => ( - - ), - size: 180, - }), - columnHelper.accessor('domainName', { - header: () => ( - } /> - ), - cell: (props) => ( - { - const company = { ...props.row.original }; - company.domainName = value; - updateCompany({ - variables: { - ...company, - accountOwnerId: company.accountOwner?.id, - }, - }); - }} - /> - ), - size: 100, - }), - columnHelper.accessor('employees', { - header: () => ( - } /> - ), - cell: (props) => ( - { - const company = { ...props.row.original }; - - updateCompany({ - variables: { - ...company, - employees: value === '' ? null : Number(value), - accountOwnerId: company.accountOwner?.id, - }, - }); - }} - /> - ), - size: 150, - }), - columnHelper.accessor('address', { - header: () => ( - } /> - ), - cell: (props) => ( - { - const company = { ...props.row.original }; - company.address = value; - updateCompany({ - variables: { - ...company, - accountOwnerId: company.accountOwner?.id, - }, - }); - }} - /> - ), - size: 170, - }), - columnHelper.accessor('createdAt', { - header: () => ( - } - /> - ), - cell: (props) => ( - { - const company = { ...props.row.original }; - company.createdAt = value.toISOString(); - updateCompany({ - variables: { - ...company, - accountOwnerId: company.accountOwner?.id, - }, - }); - }} - /> - ), - size: 150, - }), - columnHelper.accessor('accountOwner', { - header: () => ( - } - /> - ), - cell: (props) => ( - - ), - }), - ]; - }, [updateCompany]); -}; diff --git a/front/src/pages/companies/companies-sorts.tsx b/front/src/pages/companies/companies-sorts.tsx index 27ad5c25f..3467eff45 100644 --- a/front/src/pages/companies/companies-sorts.tsx +++ b/front/src/pages/companies/companies-sorts.tsx @@ -13,30 +13,25 @@ export const availableSorts = [ key: 'name', label: 'Name', icon: , - _type: 'default_sort', }, { key: 'employees', label: 'Employees', icon: , - _type: 'default_sort', }, { key: 'domainName', label: 'Url', icon: , - _type: 'default_sort', }, { key: 'address', label: 'Address', icon: , - _type: 'default_sort', }, { key: 'createdAt', label: 'Creation', icon: , - _type: 'default_sort', }, ] satisfies Array>; diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index 1451e33ce..421e35e9b 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -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 ( } + title="Companies" + icon={} onAddButtonClick={handleAddButtonClick} > diff --git a/front/src/pages/people/PeopleTable.tsx b/front/src/pages/people/PeopleTable.tsx index 43c0f3d5c..afb64693b 100644 --- a/front/src/pages/people/PeopleTable.tsx +++ b/front/src/pages/people/PeopleTable.tsx @@ -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 ( <> + } diff --git a/front/src/pages/people/__stories__/People.inputs.stories.tsx b/front/src/pages/people/__stories__/People.inputs.stories.tsx index ae5a04604..f3d423fc9 100644 --- a/front/src/pages/people/__stories__/People.inputs.stories.tsx +++ b/front/src/pages/people/__stories__/People.inputs.stories.tsx @@ -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); diff --git a/front/src/pages/people/__stories__/People.sortBy.stories.tsx b/front/src/pages/people/__stories__/People.sortBy.stories.tsx index 6769d10e9..476c54953 100644 --- a/front/src/pages/people/__stories__/People.sortBy.stories.tsx +++ b/front/src/pages/people/__stories__/People.sortBy.stories.tsx @@ -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, diff --git a/front/src/pages/people/people-columns.tsx b/front/src/pages/people/people-columns.tsx deleted file mode 100644 index 769ebec40..000000000 --- a/front/src/pages/people/people-columns.tsx +++ /dev/null @@ -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(); - -export const usePeopleColumns = () => { - const [updatePerson] = useUpdatePeopleMutation(); - - return useMemo(() => { - return [ - getCheckBoxColumn(), - columnHelper.accessor('firstName', { - header: () => ( - } /> - ), - cell: (props) => ( - <> - { - const person = { ...props.row.original }; - await updatePerson({ - variables: { - ...person, - firstName, - lastName, - companyId: person.company?.id, - }, - }); - }} - /> - - ), - size: 210, - }), - columnHelper.accessor('email', { - header: () => ( - } /> - ), - cell: (props) => ( - { - const person = props.row.original; - await updatePerson({ - variables: { - ...person, - email: value, - companyId: person.company?.id, - }, - }); - }} - /> - ), - size: 200, - }), - columnHelper.accessor('company', { - header: () => ( - } - /> - ), - cell: (props) => , - size: 150, - }), - columnHelper.accessor('phone', { - header: () => ( - } /> - ), - cell: (props) => ( - { - const person = { ...props.row.original }; - await updatePerson({ - variables: { - ...person, - phone: value, - companyId: person.company?.id, - }, - }); - }} - /> - ), - size: 130, - }), - columnHelper.accessor('createdAt', { - header: () => ( - } - /> - ), - cell: (props) => ( - { - const person = { ...props.row.original }; - await updatePerson({ - variables: { - ...person, - createdAt: value.toISOString(), - companyId: person.company?.id, - }, - }); - }} - /> - ), - size: 100, - }), - columnHelper.accessor('city', { - header: () => ( - } /> - ), - cell: (props) => ( - { - const person = { ...props.row.original }; - await updatePerson({ - variables: { - ...person, - city: value, - companyId: person.company?.id, - }, - }); - }} - /> - ), - }), - ]; - }, [updatePerson]); -}; diff --git a/front/src/pages/people/people-sorts.tsx b/front/src/pages/people/people-sorts.tsx index 159030356..f000dfec7 100644 --- a/front/src/pages/people/people-sorts.tsx +++ b/front/src/pages/people/people-sorts.tsx @@ -17,7 +17,7 @@ export const availableSorts = [ key: 'fullname', label: 'People', icon: , - _type: 'custom_sort', + orderByTemplates: [ (order: Order_By) => ({ firstName: order, @@ -31,31 +31,27 @@ export const availableSorts = [ key: 'company_name', label: 'Company', icon: , - _type: 'custom_sort', + orderByTemplates: [(order: Order_By) => ({ company: { name: order } })], }, { key: 'email', label: 'Email', icon: , - _type: 'default_sort', }, { key: 'phone', label: 'Phone', icon: , - _type: 'default_sort', }, { key: 'createdAt', label: 'Created at', icon: , - _type: 'default_sort', }, { key: 'city', label: 'City', icon: , - _type: 'default_sort', }, ] satisfies Array>; diff --git a/front/src/testing/renderWrappers.tsx b/front/src/testing/renderWrappers.tsx index fb7380894..eb06be001 100644 --- a/front/src/testing/renderWrappers.tsx +++ b/front/src/testing/renderWrappers.tsx @@ -13,7 +13,6 @@ import { companiesFilters } from '~/pages/companies/companies-filters'; import { ClientConfigProvider } from '~/providers/client-config/ClientConfigProvider'; import { UserProvider } from '~/providers/user/UserProvider'; -import { mockedCompaniesData } from './mock-data/companies'; import { ComponentStorybookLayout } from './ComponentStorybookLayout'; import { FullHeightStorybookLayout } from './FullHeightStorybookLayout'; import { mockedClient } from './mockedClient'; @@ -64,9 +63,8 @@ export function getRenderWrapperForEntityTableComponent( {children} diff --git a/server/src/core/company/company.resolver.ts b/server/src/core/company/company.resolver.ts index 0de30df7d..5f759305e 100644 --- a/server/src/core/company/company.resolver.ts +++ b/server/src/core/company/company.resolver.ts @@ -86,8 +86,19 @@ export class CompanyResolver { @PrismaSelector({ modelName: 'Company' }) prismaSelect: PrismaSelect<'Company'>, ): Promise | null> { - if (!args.data.accountOwner?.connect?.id) { - args.data.accountOwner = { disconnect: true }; + // TODO: Do a proper check with recursion testing on args in a more generic place + for (const key in args.data) { + if (args.data[key]) { + for (const subKey in args.data[key]) { + if (JSON.stringify(args.data[key][subKey]) === '{}') { + delete args.data[key][subKey]; + } + } + } + + if (JSON.stringify(args.data[key]) === '{}') { + delete args.data[key]; + } } return this.companyService.update({ diff --git a/server/src/core/person/person.resolver.ts b/server/src/core/person/person.resolver.ts index bfbb96c67..2a3722052 100644 --- a/server/src/core/person/person.resolver.ts +++ b/server/src/core/person/person.resolver.ts @@ -103,8 +103,19 @@ export class PersonResolver { @PrismaSelector({ modelName: 'Person' }) prismaSelect: PrismaSelect<'Person'>, ): Promise | null> { - if (!args.data.company?.connect?.id) { - args.data.company = { disconnect: true }; + // TODO: Do a proper check with recursion testing on args in a more generic place + for (const key in args.data) { + if (args.data[key]) { + for (const subKey in args.data[key]) { + if (JSON.stringify(args.data[key][subKey]) === '{}') { + delete args.data[key][subKey]; + } + } + } + + if (JSON.stringify(args.data[key]) === '{}') { + delete args.data[key]; + } } return this.personService.update({