From f4b8a3decb748680a1939ae77dd3849301f82883 Mon Sep 17 00:00:00 2001
From: Lucas Bordeau <bordeau.lucas@gmail.com>
Date: Fri, 28 Jul 2023 01:28:42 +0200
Subject: [PATCH] Added generic relation cell (#969)

* Added generic relation cell

* Deactivated debug

* Added default warning

* Put back display component

* Removed unused types
---
 .../components/CompanyPickerCell.tsx          | 60 ++++++++++++++++
 .../people/components/PeopleCompanyPicker.tsx | 11 ---
 .../people/components/PeoplePicker.tsx        | 55 +++++++++++++++
 .../constants/peopleFieldMetadataArray.tsx    | 15 +++-
 .../people/hooks/useUpdateEntityField.ts      | 65 ++++++++++++++++++
 .../people/hooks/useUpdatePeopleField.ts      | 18 -----
 .../people/table/components/PeopleTableV2.tsx |  4 +-
 .../hotkey/hooks/useScopedHotkeyCallback.ts   | 24 +++++++
 .../ui/table/components/EntityTableCellV2.tsx |  2 +-
 .../ui/table/components/EntityTableV2.tsx     | 11 ++-
 .../table/components/GenericEditableCell.tsx  | 10 ++-
 .../GenericEditableRelationCell.tsx           | 35 ++++++++++
 ...GenericEditableRelationCellDisplayMode.tsx | 44 ++++++++++++
 .../GenericEditableRelationCellEditMode.tsx   | 68 +++++++++++++++++++
 .../components/GenericEditableTextCell.tsx    |  0
 .../GenericEditableTextCellEditMode.tsx       |  5 +-
 .../ui/table/hooks/useCellUpdateFieldHook.ts  |  7 --
 .../states/EntityUpdateFieldHookContext.ts    |  6 --
 .../states/EntityUpdateMutationHookContext.ts |  3 +
 .../ui/table/types/EntityFieldMetadata.ts     | 11 ++-
 .../modules/users/components/UserPicker.tsx   | 56 +++++++++++++++
 21 files changed, 447 insertions(+), 63 deletions(-)
 create mode 100644 front/src/modules/companies/components/CompanyPickerCell.tsx
 create mode 100644 front/src/modules/people/components/PeoplePicker.tsx
 create mode 100644 front/src/modules/people/hooks/useUpdateEntityField.ts
 delete mode 100644 front/src/modules/people/hooks/useUpdatePeopleField.ts
 rename front/src/modules/{people => ui}/table/components/GenericEditableCell.tsx (65%)
 create mode 100644 front/src/modules/ui/table/components/GenericEditableRelationCell.tsx
 create mode 100644 front/src/modules/ui/table/components/GenericEditableRelationCellDisplayMode.tsx
 create mode 100644 front/src/modules/ui/table/components/GenericEditableRelationCellEditMode.tsx
 rename front/src/modules/{people => ui}/table/components/GenericEditableTextCell.tsx (100%)
 rename front/src/modules/{people => ui}/table/components/GenericEditableTextCellEditMode.tsx (86%)
 delete mode 100644 front/src/modules/ui/table/hooks/useCellUpdateFieldHook.ts
 delete mode 100644 front/src/modules/ui/table/states/EntityUpdateFieldHookContext.ts
 create mode 100644 front/src/modules/ui/table/states/EntityUpdateMutationHookContext.ts
 create mode 100644 front/src/modules/users/components/UserPicker.tsx

diff --git a/front/src/modules/companies/components/CompanyPickerCell.tsx b/front/src/modules/companies/components/CompanyPickerCell.tsx
new file mode 100644
index 0000000000..03c02df853
--- /dev/null
+++ b/front/src/modules/companies/components/CompanyPickerCell.tsx
@@ -0,0 +1,60 @@
+import { useFilteredSearchCompanyQuery } from '@/companies/queries';
+import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
+import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
+import { SingleEntitySelect } from '@/ui/relation-picker/components/SingleEntitySelect';
+import { relationPickerSearchFilterScopedState } from '@/ui/relation-picker/states/relationPickerSearchFilterScopedState';
+import { isCreateModeScopedState } from '@/ui/table/editable-cell/states/isCreateModeScopedState';
+import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
+
+import { EntityForSelect } from '../../ui/relation-picker/types/EntityForSelect';
+
+export type OwnProps = {
+  companyId: string | null;
+  onSubmit: (newCompany: EntityForSelect | null) => void;
+  onCancel?: () => void;
+  createModeEnabled?: boolean;
+};
+
+export function CompanyPickerCell({
+  companyId,
+  onSubmit,
+  onCancel,
+  createModeEnabled,
+}: OwnProps) {
+  const [, setIsCreating] = useRecoilScopedState(isCreateModeScopedState);
+
+  const [searchFilter] = useRecoilScopedState(
+    relationPickerSearchFilterScopedState,
+  );
+
+  const setHotkeyScope = useSetHotkeyScope();
+
+  const companies = useFilteredSearchCompanyQuery({
+    searchFilter,
+    selectedIds: [companyId ?? ''],
+  });
+
+  async function handleEntitySelected(
+    entity: EntityForSelect | null | undefined,
+  ) {
+    onSubmit(entity ?? null);
+  }
+
+  function handleCreate() {
+    setIsCreating(true);
+    setHotkeyScope(TableHotkeyScope.CellDoubleTextInput);
+  }
+
+  return (
+    <SingleEntitySelect
+      onCreate={createModeEnabled ? handleCreate : undefined}
+      onCancel={onCancel}
+      onEntitySelected={handleEntitySelected}
+      entities={{
+        entitiesToSelect: companies.entitiesToSelect,
+        selectedEntity: companies.selectedEntities[0],
+        loading: companies.loading,
+      }}
+    />
+  );
+}
diff --git a/front/src/modules/people/components/PeopleCompanyPicker.tsx b/front/src/modules/people/components/PeopleCompanyPicker.tsx
index 0c96fa8437..bdc3e54465 100644
--- a/front/src/modules/people/components/PeopleCompanyPicker.tsx
+++ b/front/src/modules/people/components/PeopleCompanyPicker.tsx
@@ -1,12 +1,8 @@
-import { Key } from 'ts-key-enum';
-
 import { useFilteredSearchCompanyQuery } from '@/companies/queries';
-import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
 import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
 import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
 import { SingleEntitySelect } from '@/ui/relation-picker/components/SingleEntitySelect';
 import { relationPickerSearchFilterScopedState } from '@/ui/relation-picker/states/relationPickerSearchFilterScopedState';
-import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
 import { useEditableCell } from '@/ui/table/editable-cell/hooks/useEditableCell';
 import { isCreateModeScopedState } from '@/ui/table/editable-cell/states/isCreateModeScopedState';
 import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
@@ -63,13 +59,6 @@ export function PeopleCompanyPicker({ people }: OwnProps) {
     addToScopeStack(TableHotkeyScope.CellDoubleTextInput);
   }
 
-  useScopedHotkeys(
-    Key.Escape,
-    () => closeEditableCell(),
-    RelationPickerHotkeyScope.RelationPicker,
-    [closeEditableCell],
-  );
-
   return (
     <SingleEntitySelect
       onCreate={handleCreate}
diff --git a/front/src/modules/people/components/PeoplePicker.tsx b/front/src/modules/people/components/PeoplePicker.tsx
new file mode 100644
index 0000000000..bb7924c720
--- /dev/null
+++ b/front/src/modules/people/components/PeoplePicker.tsx
@@ -0,0 +1,55 @@
+import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
+import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
+import { SingleEntitySelect } from '@/ui/relation-picker/components/SingleEntitySelect';
+import { relationPickerSearchFilterScopedState } from '@/ui/relation-picker/states/relationPickerSearchFilterScopedState';
+import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
+import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
+import { useSearchPeopleQuery } from '~/generated/graphql';
+
+export type OwnProps = {
+  personId: string;
+  onSubmit: (newPersonId: string | null) => void;
+  onCancel?: () => void;
+};
+
+type PersonForSelect = EntityForSelect & {
+  entityType: Entity.Person;
+};
+
+export function PeoplePicker({ personId, onSubmit, onCancel }: OwnProps) {
+  const [searchFilter] = useRecoilScopedState(
+    relationPickerSearchFilterScopedState,
+  );
+
+  const people = useFilteredSearchEntityQuery({
+    queryHook: useSearchPeopleQuery,
+    selectedIds: [personId],
+    searchFilter: searchFilter,
+    mappingFunction: (person) => ({
+      entityType: Entity.Person,
+      id: person.id,
+      name: person.firstName + ' ' + person.lastName,
+      avatarType: 'rounded',
+    }),
+    orderByField: 'firstName',
+    searchOnFields: ['firstName', 'lastName'],
+  });
+
+  async function handleEntitySelected(
+    selectedPerson: PersonForSelect | null | undefined,
+  ) {
+    onSubmit(selectedPerson?.id ?? null);
+  }
+
+  return (
+    <SingleEntitySelect
+      onEntitySelected={handleEntitySelected}
+      onCancel={onCancel}
+      entities={{
+        loading: people.loading,
+        entitiesToSelect: people.entitiesToSelect,
+        selectedEntity: people.selectedEntities[0],
+      }}
+    />
+  );
+}
diff --git a/front/src/modules/people/constants/peopleFieldMetadataArray.tsx b/front/src/modules/people/constants/peopleFieldMetadataArray.tsx
index 9549a6f9ab..3f21bf769f 100644
--- a/front/src/modules/people/constants/peopleFieldMetadataArray.tsx
+++ b/front/src/modules/people/constants/peopleFieldMetadataArray.tsx
@@ -1,5 +1,10 @@
-import { IconBriefcase, IconMap } from '@tabler/icons-react';
+import {
+  IconBriefcase,
+  IconBuildingSkyscraper,
+  IconMap,
+} from '@tabler/icons-react';
 
+import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
 import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
 
 export const peopleFieldMetadataArray: EntityFieldMetadata[] = [
@@ -17,4 +22,12 @@ export const peopleFieldMetadataArray: EntityFieldMetadata[] = [
     columnSize: 150,
     type: 'text',
   },
+  {
+    fieldName: 'company',
+    label: 'Company',
+    icon: <IconBuildingSkyscraper size={16} />,
+    columnSize: 150,
+    type: 'relation',
+    relationType: Entity.Company,
+  },
 ];
diff --git a/front/src/modules/people/hooks/useUpdateEntityField.ts b/front/src/modules/people/hooks/useUpdateEntityField.ts
new file mode 100644
index 0000000000..1affa08dc3
--- /dev/null
+++ b/front/src/modules/people/hooks/useUpdateEntityField.ts
@@ -0,0 +1,65 @@
+import { useContext } from 'react';
+import { useRecoilValue } from 'recoil';
+
+import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
+import { entityFieldMetadataArrayState } from '@/ui/table/states/entityFieldMetadataArrayState';
+import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
+
+export function useUpdateEntityField() {
+  const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);
+
+  const [updateEntity] = useUpdateEntityMutation();
+
+  const entityFieldMetadataArray = useRecoilValue(
+    entityFieldMetadataArrayState,
+  );
+
+  return function updatePeopleField(
+    currentEntityId: string,
+    fieldName: string,
+    newFieldValue: unknown,
+  ) {
+    const fieldMetadata = entityFieldMetadataArray.find(
+      (metadata) => metadata.fieldName === fieldName,
+    );
+
+    if (!fieldMetadata) {
+      throw new Error(`Field metadata not found for field ${fieldName}`);
+    }
+
+    if (fieldMetadata.type === 'relation') {
+      const newSelectedEntity = newFieldValue as EntityForSelect | null;
+
+      if (!newSelectedEntity) {
+        updateEntity({
+          variables: {
+            where: { id: currentEntityId },
+            data: {
+              [fieldName]: {
+                disconnect: true,
+              },
+            },
+          },
+        });
+      } else {
+        updateEntity({
+          variables: {
+            where: { id: currentEntityId },
+            data: {
+              [fieldName]: {
+                connect: { id: newSelectedEntity.id },
+              },
+            },
+          },
+        });
+      }
+    } else {
+      updateEntity({
+        variables: {
+          where: { id: currentEntityId },
+          data: { [fieldName]: newFieldValue },
+        },
+      });
+    }
+  };
+}
diff --git a/front/src/modules/people/hooks/useUpdatePeopleField.ts b/front/src/modules/people/hooks/useUpdatePeopleField.ts
deleted file mode 100644
index eaaa3b372b..0000000000
--- a/front/src/modules/people/hooks/useUpdatePeopleField.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useUpdateOnePersonMutation } from '~/generated/graphql';
-
-export function useUpdatePeopleField() {
-  const [updatePeople] = useUpdateOnePersonMutation();
-
-  return function updatePeopleField(
-    peopleId: string,
-    fieldName: string,
-    fieldValue: unknown,
-  ) {
-    updatePeople({
-      variables: {
-        where: { id: peopleId },
-        data: { [fieldName]: fieldValue },
-      },
-    });
-  };
-}
diff --git a/front/src/modules/people/table/components/PeopleTableV2.tsx b/front/src/modules/people/table/components/PeopleTableV2.tsx
index fad160abde..50a972e594 100644
--- a/front/src/modules/people/table/components/PeopleTableV2.tsx
+++ b/front/src/modules/people/table/components/PeopleTableV2.tsx
@@ -3,7 +3,6 @@ import { useCallback, useMemo, useState } from 'react';
 import { defaultOrderBy } from '@/companies/queries';
 import { GenericEntityTableData } from '@/people/components/GenericEntityTableData';
 import { peopleFieldMetadataArray } from '@/people/constants/peopleFieldMetadataArray';
-import { useUpdatePeopleField } from '@/people/hooks/useUpdatePeopleField';
 import { PeopleSelectedSortType } from '@/people/queries';
 import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
 import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
@@ -15,6 +14,7 @@ import { TableContext } from '@/ui/table/states/TableContext';
 import {
   PersonOrderByWithRelationInput,
   useGetPeopleQuery,
+  useUpdateOnePersonMutation,
 } from '~/generated/graphql';
 import { availableSorts } from '~/pages/people/people-sorts';
 
@@ -46,7 +46,7 @@ export function PeopleTable() {
         viewIcon={<IconList size={16} />}
         availableSorts={availableSorts}
         onSortsUpdate={updateSorts}
-        useUpdateField={useUpdatePeopleField}
+        useUpdateEntityMutation={useUpdateOnePersonMutation}
       />
     </>
   );
diff --git a/front/src/modules/ui/hotkey/hooks/useScopedHotkeyCallback.ts b/front/src/modules/ui/hotkey/hooks/useScopedHotkeyCallback.ts
index 656903121d..d73f70d5b3 100644
--- a/front/src/modules/ui/hotkey/hooks/useScopedHotkeyCallback.ts
+++ b/front/src/modules/ui/hotkey/hooks/useScopedHotkeyCallback.ts
@@ -3,6 +3,8 @@ import { useRecoilCallback } from 'recoil';
 
 import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
 
+const DEBUG_HOTKEY_SCOPE = false;
+
 export function useScopedHotkeyCallback() {
   return useRecoilCallback(
     ({ snapshot }) =>
@@ -24,9 +26,31 @@ export function useScopedHotkeyCallback() {
           .valueOrThrow();
 
         if (!currentHotkeyScopes.includes(scope)) {
+          if (DEBUG_HOTKEY_SCOPE) {
+            console.debug(
+              `%cI can't call hotkey (${
+                hotkeysEvent.keys
+              }) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
+                ', ',
+              )}]`,
+              'color: gray; ',
+            );
+          }
+
           return;
         }
 
+        if (DEBUG_HOTKEY_SCOPE) {
+          console.debug(
+            `%cI can call hotkey (${
+              hotkeysEvent.keys
+            }) because I'm in scope [${scope}] and the active scopes are : [${currentHotkeyScopes.join(
+              ', ',
+            )}]`,
+            'color: green;',
+          );
+        }
+
         if (preventDefault) {
           keyboardEvent.stopPropagation();
           keyboardEvent.preventDefault();
diff --git a/front/src/modules/ui/table/components/EntityTableCellV2.tsx b/front/src/modules/ui/table/components/EntityTableCellV2.tsx
index 52e95a5593..3f7979749e 100644
--- a/front/src/modules/ui/table/components/EntityTableCellV2.tsx
+++ b/front/src/modules/ui/table/components/EntityTableCellV2.tsx
@@ -1,7 +1,7 @@
 import { useContext } from 'react';
 import { useSetRecoilState } from 'recoil';
 
-import { GenericEditableCell } from '@/people/table/components/GenericEditableCell';
+import { GenericEditableCell } from '@/ui/table/components/GenericEditableCell';
 
 import { RecoilScope } from '../../recoil-scope/components/RecoilScope';
 import { useCurrentRowSelected } from '../hooks/useCurrentRowSelected';
diff --git a/front/src/modules/ui/table/components/EntityTableV2.tsx b/front/src/modules/ui/table/components/EntityTableV2.tsx
index f34c70f741..21c3413502 100644
--- a/front/src/modules/ui/table/components/EntityTableV2.tsx
+++ b/front/src/modules/ui/table/components/EntityTableV2.tsx
@@ -6,9 +6,8 @@ import { useListenClickOutside } from '@/ui/hooks/useListenClickOutside';
 
 import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
 import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
-import { EntityUpdateFieldHookContext } from '../states/EntityUpdateFieldHookContext';
+import { EntityUpdateMutationHookContext } from '../states/EntityUpdateMutationHookContext';
 import { TableHeader } from '../table-header/components/TableHeader';
-import { EntityUpdateFieldHook } from '../types/CellUpdateFieldHook';
 
 import { EntityTableBody } from './EntityTableBodyV2';
 import { EntityTableHeader } from './EntityTableHeaderV2';
@@ -90,7 +89,7 @@ type OwnProps<SortField> = {
   availableSorts?: Array<SortType<SortField>>;
   onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
   onRowSelectionChange?: (rowSelection: string[]) => void;
-  useUpdateField: EntityUpdateFieldHook;
+  useUpdateEntityMutation: any;
 };
 
 export function EntityTable<SortField>({
@@ -98,7 +97,7 @@ export function EntityTable<SortField>({
   viewIcon,
   availableSorts,
   onSortsUpdate,
-  useUpdateField,
+  useUpdateEntityMutation,
 }: OwnProps<SortField>) {
   const tableBodyRef = React.useRef<HTMLDivElement>(null);
 
@@ -114,7 +113,7 @@ export function EntityTable<SortField>({
   });
 
   return (
-    <EntityUpdateFieldHookContext.Provider value={useUpdateField}>
+    <EntityUpdateMutationHookContext.Provider value={useUpdateEntityMutation}>
       <StyledTableWithHeader>
         <StyledTableContainer ref={tableBodyRef}>
           <TableHeader
@@ -131,6 +130,6 @@ export function EntityTable<SortField>({
           </StyledTableWrapper>
         </StyledTableContainer>
       </StyledTableWithHeader>
-    </EntityUpdateFieldHookContext.Provider>
+    </EntityUpdateMutationHookContext.Provider>
   );
 }
diff --git a/front/src/modules/people/table/components/GenericEditableCell.tsx b/front/src/modules/ui/table/components/GenericEditableCell.tsx
similarity index 65%
rename from front/src/modules/people/table/components/GenericEditableCell.tsx
rename to front/src/modules/ui/table/components/GenericEditableCell.tsx
index 2b8a6fff0d..839244d36a 100644
--- a/front/src/modules/people/table/components/GenericEditableCell.tsx
+++ b/front/src/modules/ui/table/components/GenericEditableCell.tsx
@@ -1,5 +1,6 @@
 import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
 
+import { GenericEditableRelationCell } from './GenericEditableRelationCell';
 import { GenericEditableTextCell } from './GenericEditableTextCell';
 
 type OwnProps = {
@@ -16,8 +17,15 @@ export function GenericEditableCell({ entityFieldMetadata }: OwnProps) {
           editModeHorizontalAlign="left"
         />
       );
-
+    case 'relation': {
+      return (
+        <GenericEditableRelationCell fieldMetadata={entityFieldMetadata} />
+      );
+    }
     default:
+      console.warn(
+        `Unknown field type: ${entityFieldMetadata.type} in GenericEditableCell`,
+      );
       return <></>;
   }
 }
diff --git a/front/src/modules/ui/table/components/GenericEditableRelationCell.tsx b/front/src/modules/ui/table/components/GenericEditableRelationCell.tsx
new file mode 100644
index 0000000000..9e5d0876ec
--- /dev/null
+++ b/front/src/modules/ui/table/components/GenericEditableRelationCell.tsx
@@ -0,0 +1,35 @@
+import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
+import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
+import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
+
+import { GenericEditableRelationCellDisplayMode } from './GenericEditableRelationCellDisplayMode';
+import { GenericEditableRelationCellEditMode } from './GenericEditableRelationCellEditMode';
+
+type OwnProps = {
+  fieldMetadata: EntityFieldMetadata;
+  editModeHorizontalAlign?: 'left' | 'right';
+  placeholder?: string;
+};
+
+export function GenericEditableRelationCell({
+  fieldMetadata,
+  editModeHorizontalAlign,
+  placeholder,
+}: OwnProps) {
+  return (
+    <EditableCell
+      editModeHorizontalAlign={editModeHorizontalAlign}
+      editHotkeyScope={{ scope: RelationPickerHotkeyScope.RelationPicker }}
+      editModeContent={
+        <GenericEditableRelationCellEditMode fieldMetadata={fieldMetadata} />
+      }
+      nonEditModeContent={
+        <GenericEditableRelationCellDisplayMode
+          fieldMetadata={fieldMetadata}
+          editModeHorizontalAlign={editModeHorizontalAlign}
+          placeholder={placeholder}
+        />
+      }
+    ></EditableCell>
+  );
+}
diff --git a/front/src/modules/ui/table/components/GenericEditableRelationCellDisplayMode.tsx b/front/src/modules/ui/table/components/GenericEditableRelationCellDisplayMode.tsx
new file mode 100644
index 0000000000..8488516cdb
--- /dev/null
+++ b/front/src/modules/ui/table/components/GenericEditableRelationCellDisplayMode.tsx
@@ -0,0 +1,44 @@
+import { useRecoilValue } from 'recoil';
+
+import { CompanyChip } from '@/companies/components/CompanyChip';
+import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
+import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
+import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
+import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
+import { getLogoUrlFromDomainName } from '~/utils';
+
+type OwnProps = {
+  fieldMetadata: EntityFieldMetadata;
+  editModeHorizontalAlign?: 'left' | 'right';
+  placeholder?: string;
+};
+
+export function GenericEditableRelationCellDisplayMode({
+  fieldMetadata,
+}: OwnProps) {
+  const currentRowEntityId = useCurrentRowEntityId();
+
+  const fieldValue = useRecoilValue<any | null>(
+    tableEntityFieldFamilySelector({
+      entityId: currentRowEntityId ?? '',
+      fieldName: fieldMetadata.fieldName,
+    }),
+  );
+
+  switch (fieldMetadata.relationType) {
+    case Entity.Company: {
+      return (
+        <CompanyChip
+          id={fieldValue?.id ?? ''}
+          name={fieldValue?.name ?? ''}
+          pictureUrl={getLogoUrlFromDomainName(fieldValue?.domainName)}
+        />
+      );
+    }
+    default:
+      console.warn(
+        `Unknown relation type: "${fieldMetadata.relationType}" in GenericEditableRelationCellEditMode`,
+      );
+      return <> </>;
+  }
+}
diff --git a/front/src/modules/ui/table/components/GenericEditableRelationCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableRelationCellEditMode.tsx
new file mode 100644
index 0000000000..5fb2311e0e
--- /dev/null
+++ b/front/src/modules/ui/table/components/GenericEditableRelationCellEditMode.tsx
@@ -0,0 +1,68 @@
+import { useRecoilState } from 'recoil';
+
+import { CompanyPickerCell } from '@/companies/components/CompanyPickerCell';
+import { useUpdateEntityField } from '@/people/hooks/useUpdateEntityField';
+import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
+import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
+import { useEditableCell } from '@/ui/table/editable-cell/hooks/useEditableCell';
+import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
+import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
+import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
+
+type OwnProps = {
+  fieldMetadata: EntityFieldMetadata;
+};
+
+export function GenericEditableRelationCellEditMode({
+  fieldMetadata,
+}: OwnProps) {
+  const currentRowEntityId = useCurrentRowEntityId();
+
+  const { closeEditableCell } = useEditableCell();
+
+  const [fieldValueEntity] = useRecoilState<any | null>(
+    tableEntityFieldFamilySelector({
+      entityId: currentRowEntityId ?? '',
+      fieldName: fieldMetadata.fieldName,
+    }),
+  );
+
+  const updateEntityField = useUpdateEntityField();
+
+  function handleEntitySubmit(newFieldEntity: EntityForSelect | null) {
+    if (
+      newFieldEntity?.id !== fieldValueEntity?.id &&
+      currentRowEntityId &&
+      updateEntityField
+    ) {
+      updateEntityField(
+        currentRowEntityId,
+        fieldMetadata.fieldName,
+        newFieldEntity,
+      );
+    }
+
+    closeEditableCell();
+  }
+
+  function handleCancel() {
+    closeEditableCell();
+  }
+
+  switch (fieldMetadata.relationType) {
+    case Entity.Company: {
+      return (
+        <CompanyPickerCell
+          companyId={fieldValueEntity?.id ?? null}
+          onSubmit={handleEntitySubmit}
+          onCancel={handleCancel}
+        />
+      );
+    }
+    default:
+      console.warn(
+        `Unknown relation type: "${fieldMetadata.relationType}" in GenericEditableRelationCellEditMode`,
+      );
+      return <></>;
+  }
+}
diff --git a/front/src/modules/people/table/components/GenericEditableTextCell.tsx b/front/src/modules/ui/table/components/GenericEditableTextCell.tsx
similarity index 100%
rename from front/src/modules/people/table/components/GenericEditableTextCell.tsx
rename to front/src/modules/ui/table/components/GenericEditableTextCell.tsx
diff --git a/front/src/modules/people/table/components/GenericEditableTextCellEditMode.tsx b/front/src/modules/ui/table/components/GenericEditableTextCellEditMode.tsx
similarity index 86%
rename from front/src/modules/people/table/components/GenericEditableTextCellEditMode.tsx
rename to front/src/modules/ui/table/components/GenericEditableTextCellEditMode.tsx
index 6f0010da9b..18058e9af1 100644
--- a/front/src/modules/people/table/components/GenericEditableTextCellEditMode.tsx
+++ b/front/src/modules/ui/table/components/GenericEditableTextCellEditMode.tsx
@@ -1,7 +1,7 @@
 import { useRecoilState } from 'recoil';
 
+import { useUpdateEntityField } from '@/people/hooks/useUpdateEntityField';
 import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
-import { useEntityUpdateFieldHook } from '@/ui/table/hooks/useCellUpdateFieldHook';
 import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
 import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
 
@@ -23,8 +23,7 @@ export function GenericEditableTextCellEditMode({
     }),
   );
 
-  const useUpdateField = useEntityUpdateFieldHook();
-  const updateField = useUpdateField?.();
+  const updateField = useUpdateEntityField();
 
   function handleSubmit(newText: string) {
     if (newText === fieldValue) return;
diff --git a/front/src/modules/ui/table/hooks/useCellUpdateFieldHook.ts b/front/src/modules/ui/table/hooks/useCellUpdateFieldHook.ts
deleted file mode 100644
index 9b0ba8d54c..0000000000
--- a/front/src/modules/ui/table/hooks/useCellUpdateFieldHook.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { useContext } from 'react';
-
-import { EntityUpdateFieldHookContext } from '../states/EntityUpdateFieldHookContext';
-
-export function useEntityUpdateFieldHook() {
-  return useContext(EntityUpdateFieldHookContext);
-}
diff --git a/front/src/modules/ui/table/states/EntityUpdateFieldHookContext.ts b/front/src/modules/ui/table/states/EntityUpdateFieldHookContext.ts
deleted file mode 100644
index 2ed5eaf308..0000000000
--- a/front/src/modules/ui/table/states/EntityUpdateFieldHookContext.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createContext } from 'react';
-
-import { EntityUpdateFieldHook } from '../types/CellUpdateFieldHook';
-
-export const EntityUpdateFieldHookContext =
-  createContext<EntityUpdateFieldHook | null>(null);
diff --git a/front/src/modules/ui/table/states/EntityUpdateMutationHookContext.ts b/front/src/modules/ui/table/states/EntityUpdateMutationHookContext.ts
new file mode 100644
index 0000000000..621130e58d
--- /dev/null
+++ b/front/src/modules/ui/table/states/EntityUpdateMutationHookContext.ts
@@ -0,0 +1,3 @@
+import { createContext } from 'react';
+
+export const EntityUpdateMutationHookContext = createContext<any | null>(null);
diff --git a/front/src/modules/ui/table/types/EntityFieldMetadata.ts b/front/src/modules/ui/table/types/EntityFieldMetadata.ts
index fa78483deb..4231306ffb 100644
--- a/front/src/modules/ui/table/types/EntityFieldMetadata.ts
+++ b/front/src/modules/ui/table/types/EntityFieldMetadata.ts
@@ -1,10 +1,6 @@
-export type EntityFieldType =
-  | 'text'
-  | 'number'
-  | 'date'
-  | 'select'
-  | 'checkbox'
-  | 'icon';
+import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
+
+export type EntityFieldType = 'text' | 'relation';
 
 export type EntityFieldMetadata = {
   fieldName: string;
@@ -13,4 +9,5 @@ export type EntityFieldMetadata = {
   icon: JSX.Element;
   columnSize: number;
   filterIcon?: JSX.Element;
+  relationType?: Entity; // TODO: condition this type with type === "relation"
 };
diff --git a/front/src/modules/users/components/UserPicker.tsx b/front/src/modules/users/components/UserPicker.tsx
new file mode 100644
index 0000000000..5ad2b5a568
--- /dev/null
+++ b/front/src/modules/users/components/UserPicker.tsx
@@ -0,0 +1,56 @@
+import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
+import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
+import { SingleEntitySelect } from '@/ui/relation-picker/components/SingleEntitySelect';
+import { relationPickerSearchFilterScopedState } from '@/ui/relation-picker/states/relationPickerSearchFilterScopedState';
+import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
+import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
+import { useSearchUserQuery } from '~/generated/graphql';
+
+export type OwnProps = {
+  userId: string;
+  onSubmit: (newUserId: string) => void;
+  onCancel?: () => void;
+};
+
+type UserForSelect = EntityForSelect & {
+  entityType: Entity.User;
+};
+
+export function UserPicker({ userId, onSubmit, onCancel }: OwnProps) {
+  const [searchFilter] = useRecoilScopedState(
+    relationPickerSearchFilterScopedState,
+  );
+
+  const users = useFilteredSearchEntityQuery({
+    queryHook: useSearchUserQuery,
+    selectedIds: [userId],
+    searchFilter: searchFilter,
+    mappingFunction: (user) => ({
+      entityType: Entity.User,
+      id: user.id,
+      name: user.displayName,
+      avatarType: 'rounded',
+      avatarUrl: user.avatarUrl ?? '',
+    }),
+    orderByField: 'firstName',
+    searchOnFields: ['firstName', 'lastName'],
+  });
+
+  async function handleEntitySelected(
+    selectedUser: UserForSelect | null | undefined,
+  ) {
+    onSubmit(selectedUser?.id ?? '');
+  }
+
+  return (
+    <SingleEntitySelect
+      onEntitySelected={handleEntitySelected}
+      onCancel={onCancel}
+      entities={{
+        loading: users.loading,
+        entitiesToSelect: users.entitiesToSelect,
+        selectedEntity: users.selectedEntities[0],
+      }}
+    />
+  );
+}