Added generic relation cell (#969)

* Added generic relation cell

* Deactivated debug

* Added default warning

* Put back display component

* Removed unused types
This commit is contained in:
Lucas Bordeau
2023-07-28 01:28:42 +02:00
committed by GitHub
parent 3b796ee68c
commit f4b8a3decb
21 changed files with 447 additions and 63 deletions

View File

@ -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}

View File

@ -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],
}}
/>
);
}

View File

@ -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,
},
];

View File

@ -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 },
},
});
}
};
}

View File

@ -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 },
},
});
};
}

View File

@ -1,23 +0,0 @@
import { EntityFieldMetadata } from '@/ui/table/types/EntityFieldMetadata';
import { GenericEditableTextCell } from './GenericEditableTextCell';
type OwnProps = {
entityFieldMetadata: EntityFieldMetadata;
};
export function GenericEditableCell({ entityFieldMetadata }: OwnProps) {
switch (entityFieldMetadata.type) {
case 'text':
return (
<GenericEditableTextCell
fieldName={entityFieldMetadata.fieldName}
placeholder={entityFieldMetadata.label}
editModeHorizontalAlign="left"
/>
);
default:
return <></>;
}
}

View File

@ -1,44 +0,0 @@
import { useRecoilValue } from 'recoil';
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { GenericEditableTextCellEditMode } from './GenericEditableTextCellEditMode';
type OwnProps = {
fieldName: string;
editModeHorizontalAlign?: 'left' | 'right';
placeholder?: string;
};
export function GenericEditableTextCell({
fieldName,
editModeHorizontalAlign,
placeholder,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const fieldValue = useRecoilValue<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName,
}),
);
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<GenericEditableTextCellEditMode
fieldName={fieldName}
placeholder={placeholder}
/>
}
nonEditModeContent={
<InplaceInputTextDisplayMode>{fieldValue}</InplaceInputTextDisplayMode>
}
></EditableCell>
);
}

View File

@ -1,47 +0,0 @@
import { useRecoilState } from 'recoil';
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';
type OwnProps = {
fieldName: string;
placeholder?: string;
};
export function GenericEditableTextCellEditMode({
fieldName,
placeholder,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const [fieldValue, setFieldValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName,
}),
);
const useUpdateField = useEntityUpdateFieldHook();
const updateField = useUpdateField?.();
function handleSubmit(newText: string) {
if (newText === fieldValue) return;
setFieldValue(newText);
if (currentRowEntityId && updateField) {
updateField(currentRowEntityId, fieldName, newText);
}
}
return (
<InplaceInputTextEditMode
placeholder={placeholder ?? ''}
autoFocus
value={fieldValue ?? ''}
onSubmit={handleSubmit}
/>
);
}

View File

@ -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}
/>
</>
);