Typed updateRecord hook in generic field logic (#3102)

* Typed updateRecord hook in generic field logic

* Use sanitize instead of additional optimisticInput
This commit is contained in:
Lucas Bordeau 2023-12-21 16:27:26 +01:00 committed by GitHub
parent 0d00e3d62d
commit 180aec5ad8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 124 additions and 150 deletions

View File

@ -42,7 +42,7 @@ export const ActivityBodyEditor = ({
setBody(activityBody);
updateOneRecord?.({
idToUpdate: activity.id,
input: {
updateOneRecordInput: {
body: activityBody,
},
});

View File

@ -90,7 +90,6 @@ export const ActivityEditor = ({
objectRecordId: activity.id,
fieldMetadataName: 'dueAt',
fieldPosition: 0,
forceRefetch: true,
});
const { FieldContextProvider: AssigneeFieldContextProvider } =
@ -99,14 +98,13 @@ export const ActivityEditor = ({
objectRecordId: activity.id,
fieldMetadataName: 'assignee',
fieldPosition: 1,
forceRefetch: true,
});
const updateTitle = useCallback(
(newTitle: string) => {
updateOneActivity?.({
idToUpdate: activity.id,
input: {
updateOneRecordInput: {
title: newTitle ?? '',
},
});
@ -117,10 +115,9 @@ export const ActivityEditor = ({
(value: boolean) => {
updateOneActivity?.({
idToUpdate: activity.id,
input: {
updateOneRecordInput: {
completedAt: value ? new Date().toISOString() : null,
},
forceRefetch: true,
});
},
[activity.id, updateOneActivity],

View File

@ -15,7 +15,7 @@ export const useCompleteTask = (task: Task) => {
const completedAt = value ? new Date().toISOString() : null;
await updateOneActivity?.({
idToUpdate: task.id,
input: {
updateOneRecordInput: {
completedAt,
},
});

View File

@ -2,7 +2,11 @@ import { ReactNode, useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/field/contexts/FieldContext';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { BoardCardIdContext } from '@/object-record/record-board/contexts/BoardCardIdContext';
import { useCurrentRecordBoardCardSelectedInternal } from '@/object-record/record-board/hooks/internal/useCurrentRecordBoardCardSelectedInternal';
@ -145,24 +149,15 @@ export const CompanyBoardCard = () => {
const visibleBoardCardFields = useRecoilValue(visibleBoardCardFieldsSelector);
const useUpdateOneRecordMutation: () => [(params: any) => any, any] = () => {
const useUpdateOneRecordMutation: RecordUpdateHook = () => {
const { updateOneRecord: updateOneOpportunity } = useUpdateOneRecord({
objectNameSingular: 'opportunity',
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneOpportunity?.({
idToUpdate: variables.where.id,
input: variables.data,
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
@ -242,7 +237,7 @@ export const CompanyBoardCard = () => {
type: viewField.type,
metadata: viewField.metadata,
},
useUpdateEntityMutation: useUpdateOneRecordMutation,
useUpdateRecord: useUpdateOneRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>

View File

@ -6,7 +6,11 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/field/contexts/FieldContext';
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
@ -82,23 +86,11 @@ export const RecordShowPage = () => {
? 'Person'
: 'Custom';
const useUpdateOneObjectRecordMutation: () => [
(params: any) => any,
any,
] = () => {
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id,
input: variables.data,
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
@ -172,7 +164,7 @@ export const RecordShowPage = () => {
await updateOneRecord({
idToUpdate: record.id,
input: {
updateOneRecordInput: {
avatarUrl,
},
});
@ -249,8 +241,7 @@ export const RecordShowPage = () => {
labelIdentifierFieldMetadata?.name || '',
},
},
useUpdateEntityMutation:
useUpdateOneObjectRecordMutation,
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
@ -279,8 +270,7 @@ export const RecordShowPage = () => {
position: index,
objectMetadataItem,
}),
useUpdateEntityMutation:
useUpdateOneObjectRecordMutation,
useUpdateRecord: useUpdateOneObjectRecordMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>

View File

@ -4,6 +4,7 @@ import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCom
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordTable } from '@/object-record/record-table/components/RecordTable';
import { TableOptionsDropdownId } from '@/object-record/record-table/constants/TableOptionsDropdownId';
@ -57,19 +58,10 @@ export const RecordTableContainer = ({
recordTableScopeId: recordTableId,
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id,
input: variables.data,
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
@ -111,14 +103,12 @@ export const RecordTableContainer = ({
/>
</SpreadsheetImportProvider>
<RecordTableEffect recordTableId={recordTableId} viewBarId={viewBarId} />
{
<RecordTable
recordTableId={recordTableId}
viewBarId={viewBarId}
updateRecordMutation={updateEntity}
createRecord={createRecord}
/>
}
<RecordTable
recordTableId={recordTableId}
viewBarId={viewBarId}
updateRecordMutation={updateEntity}
createRecord={createRecord}
/>
</StyledContainer>
);
};

View File

@ -3,10 +3,25 @@ import { createContext } from 'react';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldMetadata } from '../types/FieldMetadata';
export type RecordUpdateHookParams = {
variables: {
where: Record<string, unknown>;
updateOneRecordInput: Record<string, unknown>;
};
};
export type RecordUpdateHookReturn = {
loading?: boolean;
};
export type RecordUpdateHook = () => [
(params: RecordUpdateHookParams) => void,
RecordUpdateHookReturn,
];
export type GenericFieldContextType = {
fieldDefinition: FieldDefinition<FieldMetadata>;
// TODO: add better typing for mutation web-hook
useUpdateEntityMutation?: () => [(params: any) => void, any];
useUpdateRecord?: RecordUpdateHook;
entityId: string;
recoilScopeId?: string;
hotkeyScope: string;

View File

@ -31,10 +31,10 @@ export const usePersistField = () => {
const {
entityId,
fieldDefinition,
useUpdateEntityMutation = () => [],
useUpdateRecord = () => [],
} = useContext(FieldContext);
const [updateEntity] = useUpdateEntityMutation();
const [updateRecord] = useUpdateRecord();
const persistField = useRecoilCallback(
({ set }) =>
@ -85,13 +85,12 @@ export const usePersistField = () => {
valueToPersist,
);
updateEntity?.({
updateRecord?.({
variables: {
where: { id: entityId },
data: {
// TODO: find a more elegant way to do this ?
// Maybe have a link between the RELATION field and the UUID field ?
updateOneRecordInput: {
[`${fieldName}Id`]: valueToPersist?.id ?? null,
[`${fieldName}`]: valueToPersist ?? null,
},
},
});
@ -113,10 +112,10 @@ export const usePersistField = () => {
valueToPersist,
);
updateEntity?.({
updateRecord?.({
variables: {
where: { id: entityId },
data: {
updateOneRecordInput: {
[fieldName]: valueToPersist,
},
},
@ -131,7 +130,7 @@ export const usePersistField = () => {
);
}
},
[entityId, fieldDefinition, updateEntity],
[entityId, fieldDefinition, updateRecord],
);
return persistField;

View File

@ -9,10 +9,10 @@ export const useToggleEditOnlyInput = () => {
const {
entityId,
fieldDefinition,
useUpdateEntityMutation = () => [],
useUpdateRecord = () => [],
} = useContext(FieldContext);
const [updateEntity] = useUpdateEntityMutation();
const [updateRecord] = useUpdateRecord();
const toggleField = useRecoilCallback(
({ set, snapshot }) =>
@ -30,10 +30,10 @@ export const useToggleEditOnlyInput = () => {
valueToPersist,
);
updateEntity?.({
updateRecord?.({
variables: {
where: { id: entityId },
data: {
updateOneRecordInput: {
[fieldName]: valueToPersist,
},
},
@ -44,7 +44,7 @@ export const useToggleEditOnlyInput = () => {
);
}
},
[entityId, fieldDefinition, updateEntity],
[entityId, fieldDefinition, updateRecord],
);
return toggleField;

View File

@ -22,7 +22,7 @@ export const FieldContextProvider = ({
recoilScopeId: '1',
hotkeyScope: 'hotkey-scope',
fieldDefinition,
useUpdateEntityMutation: () => [() => undefined, {}],
useUpdateRecord: () => [() => undefined, {}],
}}
>
{children}

View File

@ -37,7 +37,7 @@ const meta: Meta = {
},
},
hotkeyScope: 'hotkey-scope',
useUpdateEntityMutation: () => [() => undefined, undefined],
useUpdateRecord: () => [() => undefined, {}],
}}
>
<NumberFieldValueSetterEffect value={args.value} />

View File

@ -38,7 +38,7 @@ const meta: Meta = {
},
},
hotkeyScope: 'hotkey-scope',
useUpdateEntityMutation: () => [() => undefined, undefined],
useUpdateRecord: () => [() => undefined, {}],
}}
>
<MemoryRouter>

View File

@ -44,22 +44,6 @@ export const RelationFieldInput = ({
onCancel={onCancel}
initialSearchFilter={initialSearchValue}
/>
{/* {fieldDefinition.metadata.fieldName === 'person' ? (
<PeoplePicker
personId={initialValue?.id ?? ''}
companyId={initialValue?.companyId ?? ''}
onSubmit={handleSubmit}
onCancel={onCancel}
initialSearchFilter={initialSearchValue}
/>
) : fieldDefinition.metadata.fieldName === 'company' ? (
<CompanyPicker
companyId={initialValue?.id ?? ''}
onSubmit={handleSubmit}
onCancel={onCancel}
initialSearchFilter={initialSearchValue}
/>
) : null} */}
</StyledRelationPickerContainer>
);
};

View File

@ -2,7 +2,11 @@ import { ReactNode } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { FieldContext } from '@/object-record/field/contexts/FieldContext';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/field/contexts/FieldContext';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
@ -11,13 +15,11 @@ export const useFieldContext = ({
fieldMetadataName,
objectRecordId,
fieldPosition,
forceRefetch,
}: {
objectNameSingular: string;
objectRecordId: string;
fieldMetadataName: string;
fieldPosition: number;
forceRefetch?: boolean;
}) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@ -27,25 +29,15 @@ export const useFieldContext = ({
(field) => field.name === fieldMetadataName,
);
const useUpdateOneObjectMutation: () => [(params: any) => any, any] = () => {
const useUpdateOneObjectMutation: RecordUpdateHook = () => {
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
});
const updateEntity = ({
variables,
}: {
variables: {
where: { id: string };
data: {
[fieldName: string]: any;
};
};
}) => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord?.({
idToUpdate: variables.where.id,
input: variables.data,
forceRefetch,
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
@ -66,7 +58,7 @@ export const useFieldContext = ({
position: fieldPosition,
objectMetadataItem,
}),
useUpdateEntityMutation: useUpdateOneObjectMutation,
useUpdateRecord: useUpdateOneObjectMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>

View File

@ -2,6 +2,7 @@ import { useApolloClient } from '@apollo/client';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { capitalize } from '~/utils/string/capitalize';
type useUpdateOneRecordProps = {
@ -25,37 +26,46 @@ export const useUpdateOneRecord = <T>({
const updateOneRecord = async ({
idToUpdate,
input,
updateOneRecordInput,
}: {
idToUpdate: string;
input: Record<string, any>;
forceRefetch?: boolean;
updateOneRecordInput: Record<string, unknown>;
}) => {
const cachedRecord = getRecordFromCache(idToUpdate);
const optimisticallyUpdatedRecord: Record<string, any> = {
...(cachedRecord ?? {}),
...updateOneRecordInput,
};
const sanitizedUpdateOneRecordInput = Object.fromEntries(
Object.keys(updateOneRecordInput)
.filter((fieldName) => {
const fieldDefinition = objectMetadataItem.fields.find(
(field) => field.name === fieldName,
);
return fieldDefinition?.type !== FieldMetadataType.Relation;
})
.map((fieldName) => [fieldName, updateOneRecordInput[fieldName]]),
);
triggerOptimisticEffects({
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
updatedRecords: [
{
...(cachedRecord ?? {}),
...input,
},
],
updatedRecords: [optimisticallyUpdatedRecord],
});
const updatedRecord = await apolloClient.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate: idToUpdate,
idToUpdate,
input: {
...input,
...sanitizedUpdateOneRecordInput,
},
},
optimisticResponse: {
[`update${capitalize(objectMetadataItem.nameSingular)}`]: {
...(cachedRecord ?? {}),
...input,
},
[`update${capitalize(objectMetadataItem.nameSingular)}`]:
optimisticallyUpdatedRecord,
},
});

View File

@ -83,7 +83,7 @@ export const RecordBoard = ({
async (pipelineProgressId: string, pipelineStepId: string) => {
await updateOneOpportunity?.({
idToUpdate: pipelineProgressId,
input: {
updateOneRecordInput: {
pipelineStepId: pipelineStepId,
},
});

View File

@ -26,7 +26,7 @@ export const useBoardColumnsInternal = () => {
(stage) =>
updateOnePipelineStep?.({
idToUpdate: stage.id,
input: {
updateOneRecordInput: {
position: stage.position,
},
}),

View File

@ -12,7 +12,7 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useViewFields } from '@/views/hooks/internal/useViewFields';
import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField';
import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext';
import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { useRecordTable } from '../hooks/useRecordTable';
import { RecordTableScope } from '../scopes/RecordTableScope';
import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
@ -159,7 +159,7 @@ export const RecordTable = ({
})}
>
<ScrollWrapper>
<EntityUpdateMutationContext.Provider value={updateRecordMutation}>
<RecordUpdateContext.Provider value={updateRecordMutation}>
<StyledTableWithHeader>
<StyledTableContainer>
<div ref={tableBodyRef}>
@ -193,7 +193,7 @@ export const RecordTable = ({
)}
</StyledTableContainer>
</StyledTableWithHeader>
</EntityUpdateMutationContext.Provider>
</RecordUpdateContext.Provider>
</ScrollWrapper>
</RecordTableScope>
);

View File

@ -11,7 +11,7 @@ import { FieldContext } from '../../field/contexts/FieldContext';
import { isFieldRelation } from '../../field/types/guards/isFieldRelation';
import { ColumnContext } from '../contexts/ColumnContext';
import { ColumnIndexContext } from '../contexts/ColumnIndexContext';
import { EntityUpdateMutationContext } from '../contexts/EntityUpdateMutationHookContext';
import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext';
import { RowIdContext } from '../contexts/RowIdContext';
import { TableCell } from '../record-table-cell/components/RecordTableCell';
import { useCurrentRowSelected } from '../record-table-row/hooks/useCurrentRowSelected';
@ -39,7 +39,7 @@ export const RecordTableCell = ({ cellIndex }: { cellIndex: number }) => {
const columnDefinition = useContext(ColumnContext);
const updateEntityMutation = useContext(EntityUpdateMutationContext);
const updateRecord = useContext(RecordUpdateContext);
if (!columnDefinition || !currentRowId) {
return null;
@ -58,7 +58,7 @@ export const RecordTableCell = ({ cellIndex }: { cellIndex: number }) => {
recoilScopeId: currentRowId + columnDefinition.label,
entityId: currentRowId,
fieldDefinition: columnDefinition,
useUpdateEntityMutation: () => [updateEntityMutation, {}],
useUpdateRecord: () => [updateRecord, {}],
hotkeyScope: customHotkeyScope,
basePathToShowPage: objectMetadataConfig?.basePathToShowPage,
isLabelIdentifier:

View File

@ -1,5 +1,7 @@
import { createContext } from 'react';
export const EntityUpdateMutationContext = createContext<(params: any) => void>(
{} as any,
);
import { RecordUpdateHookParams } from '@/object-record/field/contexts/FieldContext';
export const RecordUpdateContext = createContext<
(params: RecordUpdateHookParams) => void
>({} as any);

View File

@ -60,7 +60,7 @@ export const NameFields = ({
if (autoSave) {
await updateOneRecord({
idToUpdate: currentWorkspaceMember?.id,
input: {
updateOneRecordInput: {
name: {
firstName: firstName,
lastName: lastName,

View File

@ -57,7 +57,7 @@ export const ProfilePictureUploader = () => {
await updateOneRecord({
idToUpdate: currentWorkspaceMember?.id,
input: {
updateOneRecordInput: {
avatarUrl,
},
});
@ -84,7 +84,7 @@ export const ProfilePictureUploader = () => {
await updateOneRecord({
idToUpdate: currentWorkspaceMember?.id,
input: {
updateOneRecordInput: {
avatarUrl: null,
},
});

View File

@ -32,7 +32,7 @@ export const useColorScheme = () => {
});
await updateOneWorkspaceMember?.({
idToUpdate: currentWorkspaceMember?.id,
input: {
updateOneRecordInput: {
colorScheme: value,
},
});

View File

@ -93,7 +93,7 @@ export const CreateProfile = () => {
await updateOneRecord({
idToUpdate: currentWorkspaceMember?.id,
input: {
updateOneRecordInput: {
name: {
firstName: data.firstName,
lastName: data.lastName,

View File

@ -36,7 +36,7 @@ export const Opportunities = () => {
}) => {
updateOnePipelineStep?.({
idToUpdate: columnId,
input: { name: title, color },
updateOneRecordInput: { name: title, color },
});
};

View File

@ -65,7 +65,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
const deleteIntegration = async (redirect = true) => {
await updateApiKey?.({
idToUpdate: apiKeyId,
input: { revokedAt: DateTime.now().toString() },
updateOneRecordInput: { revokedAt: DateTime.now().toString() },
});
performOptimisticEvict('ApiKey', 'id', apiKeyId);
if (redirect) {