fix: detach relation records in cache on record deletion (#3707)
* fix: detach relation records in cache on record deletion * fix: fix useGetRelationMetadata tests
This commit is contained in:
@ -1,76 +0,0 @@
|
|||||||
import { useRecoilCallback } from 'recoil';
|
|
||||||
|
|
||||||
import { TriggerUpdateRelationFieldOptimisticEffectParams } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationFieldOptimisticEffect';
|
|
||||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
export const useGetRelationFieldsToOptimisticallyUpdate = () =>
|
|
||||||
useRecoilCallback(
|
|
||||||
({ snapshot }) =>
|
|
||||||
<UpdatedObjectRecord extends ObjectRecord = ObjectRecord>({
|
|
||||||
cachedRecord,
|
|
||||||
objectMetadataItem,
|
|
||||||
updateRecordInput,
|
|
||||||
}: {
|
|
||||||
cachedRecord: UpdatedObjectRecord & { __typename: string };
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
updateRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
|
||||||
}) =>
|
|
||||||
Object.entries(updateRecordInput).reduce<
|
|
||||||
Pick<
|
|
||||||
TriggerUpdateRelationFieldOptimisticEffectParams,
|
|
||||||
| 'relationObjectMetadataNameSingular'
|
|
||||||
| 'relationFieldName'
|
|
||||||
| 'previousRelationRecord'
|
|
||||||
| 'nextRelationRecord'
|
|
||||||
>[]
|
|
||||||
>((result, [fieldName, nextRelationRecord]) => {
|
|
||||||
const fieldDefinition = objectMetadataItem.fields.find(
|
|
||||||
(fieldMetadataItem) => fieldMetadataItem.name === fieldName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldDefinition?.type !== FieldMetadataType.Relation)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
const relationObjectMetadataNameSingular = (
|
|
||||||
fieldDefinition.toRelationMetadata?.fromObjectMetadata ||
|
|
||||||
fieldDefinition.fromRelationMetadata?.toObjectMetadata
|
|
||||||
)?.nameSingular;
|
|
||||||
const relationFieldMetadataId =
|
|
||||||
fieldDefinition.toRelationMetadata?.fromFieldMetadataId ||
|
|
||||||
fieldDefinition.fromRelationMetadata?.toFieldMetadataId;
|
|
||||||
|
|
||||||
if (!relationObjectMetadataNameSingular || !relationFieldMetadataId)
|
|
||||||
return result;
|
|
||||||
|
|
||||||
const relationObjectMetadataItem = snapshot
|
|
||||||
.getLoadable(
|
|
||||||
objectMetadataItemFamilySelector({
|
|
||||||
objectName: relationObjectMetadataNameSingular,
|
|
||||||
objectNameType: 'singular',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.valueOrThrow();
|
|
||||||
|
|
||||||
if (!relationObjectMetadataItem) return result;
|
|
||||||
|
|
||||||
const relationFieldName = relationObjectMetadataItem.fields.find(
|
|
||||||
(fieldMetadataItem) =>
|
|
||||||
fieldMetadataItem.id === relationFieldMetadataId,
|
|
||||||
)?.name;
|
|
||||||
|
|
||||||
if (!relationFieldName) return result;
|
|
||||||
|
|
||||||
return [
|
|
||||||
...result,
|
|
||||||
{
|
|
||||||
relationObjectMetadataNameSingular,
|
|
||||||
relationFieldName,
|
|
||||||
previousRelationRecord: cachedRecord[fieldName],
|
|
||||||
nextRelationRecord,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, []),
|
|
||||||
);
|
|
||||||
@ -4,15 +4,24 @@ import { z } from 'zod';
|
|||||||
import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection';
|
import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
export const isCachedObjectConnection = (
|
export const isCachedObjectRecordConnection = (
|
||||||
objectNameSingular: string,
|
objectNameSingular: string,
|
||||||
storeValue: StoreValue,
|
storeValue: StoreValue,
|
||||||
): storeValue is CachedObjectRecordConnection => {
|
): storeValue is CachedObjectRecordConnection => {
|
||||||
const objectConnectionTypeName = `${capitalize(
|
const objectConnectionTypeName = `${capitalize(
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
)}Connection`;
|
)}Connection`;
|
||||||
|
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
|
||||||
const cachedObjectConnectionSchema = z.object({
|
const cachedObjectConnectionSchema = z.object({
|
||||||
__typename: z.literal(objectConnectionTypeName),
|
__typename: z.literal(objectConnectionTypeName),
|
||||||
|
edges: z.array(
|
||||||
|
z.object({
|
||||||
|
__typename: z.literal(objectEdgeTypeName),
|
||||||
|
node: z.object({
|
||||||
|
__ref: z.string().startsWith(`${capitalize(objectNameSingular)}:`),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
const cachedConnectionValidation =
|
const cachedConnectionValidation =
|
||||||
cachedObjectConnectionSchema.safeParse(storeValue);
|
cachedObjectConnectionSchema.safeParse(storeValue);
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const isObjectRecordConnection = (
|
||||||
|
objectNameSingular: string,
|
||||||
|
value: unknown,
|
||||||
|
): value is ObjectRecordConnection => {
|
||||||
|
const objectConnectionTypeName = `${capitalize(
|
||||||
|
objectNameSingular,
|
||||||
|
)}Connection`;
|
||||||
|
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
|
||||||
|
const objectConnectionSchema = z.object({
|
||||||
|
__typename: z.literal(objectConnectionTypeName),
|
||||||
|
edges: z.array(
|
||||||
|
z.object({
|
||||||
|
__typename: z.literal(objectEdgeTypeName),
|
||||||
|
node: z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const connectionValidation = objectConnectionSchema.safeParse(value);
|
||||||
|
|
||||||
|
return connectionValidation.success;
|
||||||
|
};
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
|
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||||
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const triggerAttachRelationOptimisticEffect = ({
|
||||||
|
cache,
|
||||||
|
objectNameSingular,
|
||||||
|
recordId,
|
||||||
|
relationObjectMetadataNameSingular,
|
||||||
|
relationFieldName,
|
||||||
|
relationRecordId,
|
||||||
|
}: {
|
||||||
|
cache: ApolloCache<unknown>;
|
||||||
|
objectNameSingular: string;
|
||||||
|
recordId: string;
|
||||||
|
relationObjectMetadataNameSingular: string;
|
||||||
|
relationFieldName: string;
|
||||||
|
relationRecordId: string;
|
||||||
|
}) => {
|
||||||
|
const recordTypeName = capitalize(objectNameSingular);
|
||||||
|
const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular);
|
||||||
|
|
||||||
|
cache.modify<StoreObject>({
|
||||||
|
id: cache.identify({
|
||||||
|
id: relationRecordId,
|
||||||
|
__typename: relationRecordTypeName,
|
||||||
|
}),
|
||||||
|
fields: {
|
||||||
|
[relationFieldName]: (cachedFieldValue, { toReference }) => {
|
||||||
|
const nodeReference = toReference({
|
||||||
|
id: recordId,
|
||||||
|
__typename: recordTypeName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!nodeReference) return cachedFieldValue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isCachedObjectRecordConnection(objectNameSingular, cachedFieldValue)
|
||||||
|
) {
|
||||||
|
// To many objects => add record to next relation field list
|
||||||
|
const nextEdges: CachedObjectRecordEdge[] = [
|
||||||
|
...cachedFieldValue.edges,
|
||||||
|
{
|
||||||
|
__typename: `${recordTypeName}Edge`,
|
||||||
|
node: nodeReference,
|
||||||
|
cursor: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return { ...cachedFieldValue, edges: nextEdges };
|
||||||
|
}
|
||||||
|
|
||||||
|
// To one object => attach next relation record
|
||||||
|
return nodeReference;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
@ -36,7 +36,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
!isCachedObjectConnection(
|
!isCachedObjectRecordConnection(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
cachedConnection,
|
cachedConnection,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||||
@ -24,7 +24,7 @@ export const triggerDeleteRecordsOptimisticEffect = ({
|
|||||||
{ DELETE, readField, storeFieldName },
|
{ DELETE, readField, storeFieldName },
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
!isCachedObjectConnection(
|
!isCachedObjectRecordConnection(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
cachedConnection,
|
cachedConnection,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
|
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const triggerDetachRelationOptimisticEffect = ({
|
||||||
|
cache,
|
||||||
|
objectNameSingular,
|
||||||
|
recordId,
|
||||||
|
relationObjectMetadataNameSingular,
|
||||||
|
relationFieldName,
|
||||||
|
relationRecordId,
|
||||||
|
}: {
|
||||||
|
cache: ApolloCache<unknown>;
|
||||||
|
objectNameSingular: string;
|
||||||
|
recordId: string;
|
||||||
|
relationObjectMetadataNameSingular: string;
|
||||||
|
relationFieldName: string;
|
||||||
|
relationRecordId: string;
|
||||||
|
}) => {
|
||||||
|
const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular);
|
||||||
|
|
||||||
|
cache.modify<StoreObject>({
|
||||||
|
id: cache.identify({
|
||||||
|
id: relationRecordId,
|
||||||
|
__typename: relationRecordTypeName,
|
||||||
|
}),
|
||||||
|
fields: {
|
||||||
|
[relationFieldName]: (cachedFieldValue, { isReference, readField }) => {
|
||||||
|
// To many objects => remove record from previous relation field list
|
||||||
|
if (
|
||||||
|
isCachedObjectRecordConnection(objectNameSingular, cachedFieldValue)
|
||||||
|
) {
|
||||||
|
const nextEdges = cachedFieldValue.edges.filter(
|
||||||
|
({ node }) => readField('id', node) !== recordId,
|
||||||
|
);
|
||||||
|
return { ...cachedFieldValue, edges: nextEdges };
|
||||||
|
}
|
||||||
|
|
||||||
|
// To one object => detach previous relation record
|
||||||
|
if (isReference(cachedFieldValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedFieldValue;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||||
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
@ -31,7 +31,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
|||||||
{ DELETE, readField, storeFieldName, toReference },
|
{ DELETE, readField, storeFieldName, toReference },
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
!isCachedObjectConnection(
|
!isCachedObjectRecordConnection(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
cachedConnection,
|
cachedConnection,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
|
||||||
|
|
||||||
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
|
||||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
|
||||||
|
|
||||||
export type TriggerUpdateRelationFieldOptimisticEffectParams = {
|
|
||||||
cache: ApolloCache<unknown>;
|
|
||||||
objectNameSingular: string;
|
|
||||||
record: ObjectRecord;
|
|
||||||
relationObjectMetadataNameSingular: string;
|
|
||||||
relationFieldName: string;
|
|
||||||
previousRelationRecord: ObjectRecord | null;
|
|
||||||
nextRelationRecord: ObjectRecord | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const triggerUpdateRelationFieldOptimisticEffect = ({
|
|
||||||
cache,
|
|
||||||
objectNameSingular,
|
|
||||||
record,
|
|
||||||
relationObjectMetadataNameSingular,
|
|
||||||
relationFieldName,
|
|
||||||
previousRelationRecord,
|
|
||||||
nextRelationRecord,
|
|
||||||
}: TriggerUpdateRelationFieldOptimisticEffectParams) => {
|
|
||||||
const recordTypeName = capitalize(objectNameSingular);
|
|
||||||
const relationRecordTypeName = capitalize(relationObjectMetadataNameSingular);
|
|
||||||
|
|
||||||
if (previousRelationRecord) {
|
|
||||||
cache.modify<StoreObject>({
|
|
||||||
id: cache.identify({
|
|
||||||
...previousRelationRecord,
|
|
||||||
__typename: relationRecordTypeName,
|
|
||||||
}),
|
|
||||||
fields: {
|
|
||||||
[relationFieldName]: (cachedFieldValue, { isReference, readField }) => {
|
|
||||||
// To many objects => remove record from previous relation field list
|
|
||||||
if (isCachedObjectConnection(objectNameSingular, cachedFieldValue)) {
|
|
||||||
const nextEdges = cachedFieldValue.edges.filter(
|
|
||||||
({ node }) => readField('id', node) !== record.id,
|
|
||||||
);
|
|
||||||
return { ...cachedFieldValue, edges: nextEdges };
|
|
||||||
}
|
|
||||||
|
|
||||||
// To one object => detach previous relation record
|
|
||||||
if (isReference(cachedFieldValue)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextRelationRecord) {
|
|
||||||
cache.modify<StoreObject>({
|
|
||||||
id: cache.identify({
|
|
||||||
...nextRelationRecord,
|
|
||||||
__typename: relationRecordTypeName,
|
|
||||||
}),
|
|
||||||
fields: {
|
|
||||||
[relationFieldName]: (cachedFieldValue, { toReference }) => {
|
|
||||||
const nodeReference = toReference(record);
|
|
||||||
|
|
||||||
if (!nodeReference) return cachedFieldValue;
|
|
||||||
|
|
||||||
if (isCachedObjectConnection(objectNameSingular, cachedFieldValue)) {
|
|
||||||
// To many objects => add record to next relation field list
|
|
||||||
const nextEdges: CachedObjectRecordEdge[] = [
|
|
||||||
...cachedFieldValue.edges,
|
|
||||||
{
|
|
||||||
__typename: `${recordTypeName}Edge`,
|
|
||||||
node: nodeReference,
|
|
||||||
cursor: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return { ...cachedFieldValue, edges: nextEdges };
|
|
||||||
}
|
|
||||||
|
|
||||||
// To one object => attach next relation record
|
|
||||||
return nodeReference;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { ReactNode, useEffect } from 'react';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||||
|
|
||||||
|
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<MockedProvider addTypename={false}>
|
||||||
|
<TestApolloMetadataClientProvider>
|
||||||
|
{children}
|
||||||
|
</TestApolloMetadataClientProvider>
|
||||||
|
</MockedProvider>
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useGetRelationMetadata', () => {
|
||||||
|
it('should return correct properties', async () => {
|
||||||
|
const objectMetadataItems = getObjectMetadataItemsMock();
|
||||||
|
const objectMetadata = objectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === 'person',
|
||||||
|
)!;
|
||||||
|
const fieldMetadataItem = objectMetadata.fields.find(
|
||||||
|
(field) => field.name === 'opportunities',
|
||||||
|
)!;
|
||||||
|
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => {
|
||||||
|
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMetadataItems(objectMetadataItems);
|
||||||
|
}, [setMetadataItems]);
|
||||||
|
|
||||||
|
return useGetRelationMetadata();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
wrapper: Wrapper,
|
||||||
|
initialProps: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
relationFieldMetadataItem,
|
||||||
|
relationObjectMetadataItem,
|
||||||
|
relationType,
|
||||||
|
} = result.current({ fieldMetadataItem }) ?? {};
|
||||||
|
|
||||||
|
const expectedRelationObjectMetadataItem = objectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === 'opportunity',
|
||||||
|
);
|
||||||
|
const expectedRelationFieldMetadataItem =
|
||||||
|
expectedRelationObjectMetadataItem?.fields.find(
|
||||||
|
(field) => field.name === 'person',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(relationObjectMetadataItem).toEqual(
|
||||||
|
expectedRelationObjectMetadataItem,
|
||||||
|
);
|
||||||
|
expect(relationFieldMetadataItem).toEqual(
|
||||||
|
expectedRelationFieldMetadataItem,
|
||||||
|
);
|
||||||
|
expect(relationType).toBe('ONE_TO_MANY');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
|
||||||
|
|
||||||
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
|
||||||
|
|
||||||
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
|
|
||||||
|
|
||||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
||||||
<RecoilRoot>
|
|
||||||
<MockedProvider addTypename={false}>
|
|
||||||
<TestApolloMetadataClientProvider>
|
|
||||||
{children}
|
|
||||||
</TestApolloMetadataClientProvider>
|
|
||||||
</MockedProvider>
|
|
||||||
</RecoilRoot>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('useRelationMetadata', () => {
|
|
||||||
it('should return correct properties', async () => {
|
|
||||||
const { result, rerender } = renderHook(
|
|
||||||
({ fieldMetadataItem }: { fieldMetadataItem?: FieldMetadataItem }) =>
|
|
||||||
useRelationMetadata({ fieldMetadataItem }),
|
|
||||||
{
|
|
||||||
wrapper: Wrapper,
|
|
||||||
initialProps: {},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
relationFieldMetadataItem,
|
|
||||||
relationObjectMetadataItem,
|
|
||||||
relationType,
|
|
||||||
} = result.current;
|
|
||||||
|
|
||||||
expect(relationFieldMetadataItem).toBeUndefined();
|
|
||||||
expect(relationObjectMetadataItem).toBeUndefined();
|
|
||||||
expect(relationType).toBeUndefined();
|
|
||||||
|
|
||||||
const objectMetadataItems = getObjectMetadataItemsMock();
|
|
||||||
const objectMetadata = objectMetadataItems.find(
|
|
||||||
(item) => item.nameSingular === 'person',
|
|
||||||
)!;
|
|
||||||
const fieldMetadataItem = objectMetadata.fields.find(
|
|
||||||
(field) => field.name === 'opportunities',
|
|
||||||
)!;
|
|
||||||
|
|
||||||
rerender({ fieldMetadataItem });
|
|
||||||
|
|
||||||
expect(result.current.relationType).toBe('ONE_TO_MANY');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||||
|
import { RelationType } from '@/settings/data-model/types/RelationType';
|
||||||
|
import {
|
||||||
|
FieldMetadataType,
|
||||||
|
RelationMetadataType,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||||
|
|
||||||
|
export const useGetRelationMetadata = () =>
|
||||||
|
useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
({ fieldMetadataItem }: { fieldMetadataItem: FieldMetadataItem }) => {
|
||||||
|
if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null;
|
||||||
|
|
||||||
|
const relationMetadata =
|
||||||
|
fieldMetadataItem.fromRelationMetadata ||
|
||||||
|
fieldMetadataItem.toRelationMetadata;
|
||||||
|
|
||||||
|
if (!relationMetadata) return null;
|
||||||
|
|
||||||
|
const relationFieldMetadataId =
|
||||||
|
'toFieldMetadataId' in relationMetadata
|
||||||
|
? relationMetadata.toFieldMetadataId
|
||||||
|
: relationMetadata.fromFieldMetadataId;
|
||||||
|
|
||||||
|
if (!relationFieldMetadataId) return null;
|
||||||
|
|
||||||
|
const relationType =
|
||||||
|
relationMetadata.relationType === RelationMetadataType.OneToMany &&
|
||||||
|
fieldMetadataItem.toRelationMetadata
|
||||||
|
? 'MANY_TO_ONE'
|
||||||
|
: (relationMetadata.relationType as RelationType);
|
||||||
|
|
||||||
|
const relationObjectMetadataNameSingular =
|
||||||
|
'toObjectMetadata' in relationMetadata
|
||||||
|
? relationMetadata.toObjectMetadata.nameSingular
|
||||||
|
: relationMetadata.fromObjectMetadata.nameSingular;
|
||||||
|
|
||||||
|
const relationObjectMetadataItem = snapshot
|
||||||
|
.getLoadable(
|
||||||
|
objectMetadataItemFamilySelector({
|
||||||
|
objectName: relationObjectMetadataNameSingular,
|
||||||
|
objectNameType: 'singular',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.valueOrThrow();
|
||||||
|
|
||||||
|
if (!relationObjectMetadataItem) return null;
|
||||||
|
|
||||||
|
const relationFieldMetadataItem =
|
||||||
|
relationObjectMetadataItem.fields.find(
|
||||||
|
(field) => field.id === relationFieldMetadataId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!relationFieldMetadataItem) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
relationFieldMetadataItem,
|
||||||
|
relationObjectMetadataItem,
|
||||||
|
relationType,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { RelationType } from '@/settings/data-model/types/RelationType';
|
|
||||||
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
import { useObjectMetadataItemForSettings } from '../hooks/useObjectMetadataItemForSettings';
|
|
||||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
|
||||||
|
|
||||||
export const useRelationMetadata = ({
|
|
||||||
fieldMetadataItem,
|
|
||||||
}: {
|
|
||||||
fieldMetadataItem?: FieldMetadataItem;
|
|
||||||
}) => {
|
|
||||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
|
||||||
|
|
||||||
const relationMetadata =
|
|
||||||
fieldMetadataItem?.fromRelationMetadata ||
|
|
||||||
fieldMetadataItem?.toRelationMetadata;
|
|
||||||
|
|
||||||
const relationType =
|
|
||||||
relationMetadata?.relationType === RelationMetadataType.OneToMany &&
|
|
||||||
fieldMetadataItem?.toRelationMetadata
|
|
||||||
? 'MANY_TO_ONE'
|
|
||||||
: (relationMetadata?.relationType as RelationType | undefined);
|
|
||||||
|
|
||||||
const relationObjectMetadataId =
|
|
||||||
relationMetadata && 'toObjectMetadata' in relationMetadata
|
|
||||||
? relationMetadata.toObjectMetadata.id
|
|
||||||
: relationMetadata?.fromObjectMetadata.id;
|
|
||||||
|
|
||||||
const relationObjectMetadataItem = relationObjectMetadataId
|
|
||||||
? findObjectMetadataItemById(relationObjectMetadataId)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const relationFieldMetadataId =
|
|
||||||
relationMetadata && 'toFieldMetadataId' in relationMetadata
|
|
||||||
? relationMetadata.toFieldMetadataId
|
|
||||||
: relationMetadata?.fromFieldMetadataId;
|
|
||||||
|
|
||||||
const relationFieldMetadataItem = relationFieldMetadataId
|
|
||||||
? relationObjectMetadataItem?.fields?.find(
|
|
||||||
(field) => field.id === relationFieldMetadataId,
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
relationFieldMetadataItem,
|
|
||||||
relationObjectMetadataItem,
|
|
||||||
relationType,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -15,6 +15,7 @@ export const useGetRecordFromCache = ({
|
|||||||
|
|
||||||
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
|
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
|
||||||
recordId: string,
|
recordId: string,
|
||||||
|
cache = apolloClient.cache,
|
||||||
) => {
|
) => {
|
||||||
if (!objectMetadataItem) {
|
if (!objectMetadataItem) {
|
||||||
return null;
|
return null;
|
||||||
@ -31,7 +32,6 @@ export const useGetRecordFromCache = ({
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const cache = apolloClient.cache;
|
|
||||||
const cachedRecordId = cache.identify({
|
const cachedRecordId = cache.identify({
|
||||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||||
id: recordId,
|
id: recordId,
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
|
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||||
|
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
||||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useDeleteOneRecordProps = {
|
type useDeleteOneRecordProps = {
|
||||||
@ -14,9 +19,11 @@ type useDeleteOneRecordProps = {
|
|||||||
export const useDeleteManyRecords = ({
|
export const useDeleteManyRecords = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useDeleteOneRecordProps) => {
|
}: useDeleteOneRecordProps) => {
|
||||||
const { objectMetadataItem, deleteManyRecordsMutation } =
|
const { objectMetadataItem, deleteManyRecordsMutation, getRecordFromCache } =
|
||||||
useObjectMetadataItem({ objectNameSingular });
|
useObjectMetadataItem({ objectNameSingular });
|
||||||
|
|
||||||
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
||||||
@ -24,16 +31,10 @@ export const useDeleteManyRecords = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const deleteManyRecords = async (idsToDelete: string[]) => {
|
const deleteManyRecords = async (idsToDelete: string[]) => {
|
||||||
const deleteRecordFilter: ObjectRecordQueryFilter = {
|
|
||||||
id: {
|
|
||||||
in: idsToDelete,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const deletedRecords = await apolloClient.mutate({
|
const deletedRecords = await apolloClient.mutate({
|
||||||
mutation: deleteManyRecordsMutation,
|
mutation: deleteManyRecordsMutation,
|
||||||
variables: {
|
variables: {
|
||||||
filter: deleteRecordFilter,
|
filter: { id: { in: idsToDelete } },
|
||||||
// atMost: idsToDelete.length,
|
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
|
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
|
||||||
@ -46,10 +47,49 @@ export const useDeleteManyRecords = ({
|
|||||||
|
|
||||||
if (!records?.length) return;
|
if (!records?.length) return;
|
||||||
|
|
||||||
|
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
||||||
|
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
||||||
|
|
||||||
|
if (!relationMetadata) return;
|
||||||
|
|
||||||
|
const { relationObjectMetadataItem, relationFieldMetadataItem } =
|
||||||
|
relationMetadata;
|
||||||
|
|
||||||
|
records.forEach((record) => {
|
||||||
|
const cachedRecord = getRecordFromCache(record.id, cache);
|
||||||
|
|
||||||
|
if (!cachedRecord) return;
|
||||||
|
|
||||||
|
const previousFieldValue:
|
||||||
|
| ObjectRecordConnection
|
||||||
|
| ObjectRecord
|
||||||
|
| null = cachedRecord[fieldMetadataItem.name];
|
||||||
|
|
||||||
|
const relationRecordIds = isObjectRecordConnection(
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
previousFieldValue,
|
||||||
|
)
|
||||||
|
? previousFieldValue.edges.map(({ node }) => node.id)
|
||||||
|
: [previousFieldValue?.id].filter(isDefined);
|
||||||
|
|
||||||
|
relationRecordIds.forEach((relationRecordId) =>
|
||||||
|
triggerDetachRelationOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectNameSingular,
|
||||||
|
recordId: record.id,
|
||||||
|
relationObjectMetadataNameSingular:
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
relationFieldName: relationFieldMetadataItem.name,
|
||||||
|
relationRecordId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
triggerDeleteRecordsOptimisticEffect({
|
triggerDeleteRecordsOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
records,
|
records: records,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
|
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||||
|
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||||
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
|
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useDeleteOneRecordProps = {
|
type useDeleteOneRecordProps = {
|
||||||
@ -14,9 +20,10 @@ type useDeleteOneRecordProps = {
|
|||||||
export const useDeleteOneRecord = ({
|
export const useDeleteOneRecord = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useDeleteOneRecordProps) => {
|
}: useDeleteOneRecordProps) => {
|
||||||
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem(
|
const { objectMetadataItem, deleteOneRecordMutation, getRecordFromCache } =
|
||||||
{ objectNameSingular },
|
useObjectMetadataItem({ objectNameSingular });
|
||||||
);
|
|
||||||
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
@ -39,6 +46,43 @@ export const useDeleteOneRecord = ({
|
|||||||
|
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
|
|
||||||
|
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
||||||
|
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
||||||
|
|
||||||
|
if (!relationMetadata) return;
|
||||||
|
|
||||||
|
const { relationObjectMetadataItem, relationFieldMetadataItem } =
|
||||||
|
relationMetadata;
|
||||||
|
|
||||||
|
const cachedRecord = getRecordFromCache(record.id, cache);
|
||||||
|
|
||||||
|
if (!cachedRecord) return;
|
||||||
|
|
||||||
|
const previousFieldValue:
|
||||||
|
| ObjectRecordConnection
|
||||||
|
| ObjectRecord
|
||||||
|
| null = cachedRecord[fieldMetadataItem.name];
|
||||||
|
|
||||||
|
const relationRecordIds = isObjectRecordConnection(
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
previousFieldValue,
|
||||||
|
)
|
||||||
|
? previousFieldValue.edges.map(({ node }) => node.id)
|
||||||
|
: [previousFieldValue?.id].filter(isDefined);
|
||||||
|
|
||||||
|
relationRecordIds.forEach((relationRecordId) =>
|
||||||
|
triggerDetachRelationOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectNameSingular,
|
||||||
|
recordId: record.id,
|
||||||
|
relationObjectMetadataNameSingular:
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
relationFieldName: relationFieldMetadataItem.name,
|
||||||
|
relationRecordId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
triggerDeleteRecordsOptimisticEffect({
|
triggerDeleteRecordsOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -52,6 +96,8 @@ export const useDeleteOneRecord = ({
|
|||||||
[
|
[
|
||||||
apolloClient,
|
apolloClient,
|
||||||
deleteOneRecordMutation,
|
deleteOneRecordMutation,
|
||||||
|
getRecordFromCache,
|
||||||
|
getRelationMetadata,
|
||||||
mutationResponseField,
|
mutationResponseField,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import { useGetRelationFieldsToOptimisticallyUpdate } from '@/apollo/optimistic-effect/hooks/useGetRelationFieldsToOptimisticallyUpdate';
|
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||||
|
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
|
||||||
|
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||||
import { triggerUpdateRelationFieldOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationFieldOptimisticEffect';
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useUpdateOneRecordProps = {
|
type useUpdateOneRecordProps = {
|
||||||
@ -21,8 +24,7 @@ export const useUpdateOneRecord = <
|
|||||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||||
useObjectMetadataItem({ objectNameSingular });
|
useObjectMetadataItem({ objectNameSingular });
|
||||||
|
|
||||||
const getRelationFieldsToOptimisticallyUpdate =
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
useGetRelationFieldsToOptimisticallyUpdate();
|
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
@ -48,14 +50,6 @@ export const useUpdateOneRecord = <
|
|||||||
id: idToUpdate,
|
id: idToUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedRelationFields = cachedRecord
|
|
||||||
? getRelationFieldsToOptimisticallyUpdate({
|
|
||||||
cachedRecord,
|
|
||||||
objectMetadataItem,
|
|
||||||
updateRecordInput: updateOneRecordInput,
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const mutationResponseField =
|
const mutationResponseField =
|
||||||
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
||||||
|
|
||||||
@ -73,29 +67,59 @@ export const useUpdateOneRecord = <
|
|||||||
|
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
|
|
||||||
|
objectMetadataItem.fields.forEach((fieldMetadataItem) => {
|
||||||
|
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
||||||
|
|
||||||
|
if (!relationMetadata) return;
|
||||||
|
|
||||||
|
const { relationObjectMetadataItem, relationFieldMetadataItem } =
|
||||||
|
relationMetadata;
|
||||||
|
|
||||||
|
const previousFieldValue = cachedRecord?.[fieldMetadataItem.name];
|
||||||
|
const nextFieldValue =
|
||||||
|
updateOneRecordInput[fieldMetadataItem.name] ?? null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(fieldMetadataItem.name in updateOneRecordInput) ||
|
||||||
|
isObjectRecordConnection(
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
previousFieldValue,
|
||||||
|
) ||
|
||||||
|
isDeeplyEqual(previousFieldValue, nextFieldValue)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousFieldValue) {
|
||||||
|
triggerDetachRelationOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectNameSingular,
|
||||||
|
recordId: record.id,
|
||||||
|
relationObjectMetadataNameSingular:
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
relationFieldName: relationFieldMetadataItem.name,
|
||||||
|
relationRecordId: previousFieldValue.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextFieldValue) {
|
||||||
|
triggerAttachRelationOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectNameSingular,
|
||||||
|
recordId: record.id,
|
||||||
|
relationObjectMetadataNameSingular:
|
||||||
|
relationObjectMetadataItem.nameSingular,
|
||||||
|
relationFieldName: relationFieldMetadataItem.name,
|
||||||
|
relationRecordId: nextFieldValue.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
triggerUpdateRecordOptimisticEffect({
|
triggerUpdateRecordOptimisticEffect({
|
||||||
cache,
|
cache,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
record,
|
record,
|
||||||
});
|
});
|
||||||
|
|
||||||
updatedRelationFields.forEach(
|
|
||||||
({
|
|
||||||
relationObjectMetadataNameSingular,
|
|
||||||
relationFieldName,
|
|
||||||
previousRelationRecord,
|
|
||||||
nextRelationRecord,
|
|
||||||
}) =>
|
|
||||||
triggerUpdateRelationFieldOptimisticEffect({
|
|
||||||
cache,
|
|
||||||
objectNameSingular,
|
|
||||||
record,
|
|
||||||
relationObjectMetadataNameSingular,
|
|
||||||
relationFieldName,
|
|
||||||
previousRelationRecord,
|
|
||||||
nextRelationRecord,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
import { FieldIdentifierType } from '@/settings/data-model/types/FieldIdentifierType';
|
import { FieldIdentifierType } from '@/settings/data-model/types/FieldIdentifierType';
|
||||||
@ -53,9 +53,13 @@ export const SettingsObjectFieldItemTableRow = ({
|
|||||||
const fieldDataTypeIsSupported =
|
const fieldDataTypeIsSupported =
|
||||||
fieldMetadataItem.type in settingsFieldMetadataTypes;
|
fieldMetadataItem.type in settingsFieldMetadataTypes;
|
||||||
|
|
||||||
const { relationObjectMetadataItem, relationType } = useRelationMetadata({
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
fieldMetadataItem,
|
|
||||||
});
|
const { relationObjectMetadataItem, relationType } =
|
||||||
|
useMemo(
|
||||||
|
() => getRelationMetadata({ fieldMetadataItem }),
|
||||||
|
[fieldMetadataItem, getRelationMetadata],
|
||||||
|
) ?? {};
|
||||||
|
|
||||||
if (!fieldDataTypeIsSupported) return null;
|
if (!fieldDataTypeIsSupported) return null;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||||
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
|
|
||||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
@ -43,11 +43,21 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
|
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
const {
|
const {
|
||||||
relationFieldMetadataItem,
|
relationFieldMetadataItem,
|
||||||
relationObjectMetadataItem,
|
relationObjectMetadataItem,
|
||||||
relationType,
|
relationType,
|
||||||
} = useRelationMetadata({ fieldMetadataItem: activeMetadataField });
|
} =
|
||||||
|
useMemo(
|
||||||
|
() =>
|
||||||
|
activeMetadataField
|
||||||
|
? getRelationMetadata({
|
||||||
|
fieldMetadataItem: activeMetadataField,
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
[activeMetadataField, getRelationMetadata],
|
||||||
|
) ?? {};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formValues,
|
formValues,
|
||||||
|
|||||||
Reference in New Issue
Block a user