perf: apply record optimistic effects with cache.modify on mutation (#3540)
* perf: apply record optimistic effects with cache.modify on mutation Closes #3509 * refactor: return early when created records do not match filter * fix: fix id generation on record creation * fix: comment filtering behavior on record creation * Fixed typing error * refactor: review - use ?? * refactor: review - add variables in readFieldValueToSort * docs: review - add comments for variables.first in triggerUpdateRecordOptimisticEffect * refactor: review - add intermediary variable for 'not' filter in useMultiObjectSearchMatchesSearchFilterAndToSelectQuery * refactor: review - add filter utils * fix: fix tests --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,117 +0,0 @@
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getRecordOptimisticEffectDefinition = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}): OptimisticEffectDefinition => ({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
resolver: ({
|
||||
currentCacheData: currentData,
|
||||
createdRecords,
|
||||
updatedRecords,
|
||||
deletedRecordIds,
|
||||
variables,
|
||||
}) => {
|
||||
const newRecordPaginatedCacheField = produce<ObjectRecordConnection<any>>(
|
||||
currentData as ObjectRecordConnection<any>,
|
||||
(draft) => {
|
||||
const existingDataIsEmpty = !draft || !draft.edges || !draft.edges[0];
|
||||
|
||||
if (isNonEmptyArray(createdRecords)) {
|
||||
if (existingDataIsEmpty) {
|
||||
return {
|
||||
__typename: `${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}Connection`,
|
||||
edges: createdRecords.map((createdRecord) => ({
|
||||
__typename: `${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}Edge`,
|
||||
node: createdRecord,
|
||||
cursor: '',
|
||||
})),
|
||||
pageInfo: {
|
||||
endCursor: '',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
for (const createdRecord of createdRecords) {
|
||||
const existingRecord = draft.edges.find(
|
||||
(edge) => edge.node.id === createdRecord.id,
|
||||
);
|
||||
|
||||
if (existingRecord) {
|
||||
existingRecord.node = createdRecord;
|
||||
continue;
|
||||
}
|
||||
|
||||
draft.edges.unshift({
|
||||
node: createdRecord,
|
||||
cursor: '',
|
||||
__typename: `${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}Edge`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(deletedRecordIds)) {
|
||||
draft.edges = draft.edges.filter(
|
||||
(edge) => !deletedRecordIds.includes(edge.node.id),
|
||||
);
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(updatedRecords)) {
|
||||
for (const updatedRecord of updatedRecords) {
|
||||
const updatedRecordIsOutOfQueryFilter =
|
||||
isDefined(variables.filter) &&
|
||||
!isRecordMatchingFilter({
|
||||
record: updatedRecord,
|
||||
filter: variables.filter,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (updatedRecordIsOutOfQueryFilter) {
|
||||
draft.edges = draft.edges.filter(
|
||||
(edge) => edge.node.id !== updatedRecord.id,
|
||||
);
|
||||
} else {
|
||||
const foundUpdatedRecordInCacheQuery = draft.edges.find(
|
||||
(edge) => edge.node.id === updatedRecord.id,
|
||||
);
|
||||
|
||||
if (foundUpdatedRecordInCacheQuery) {
|
||||
foundUpdatedRecordInCacheQuery.node = updatedRecord;
|
||||
} else {
|
||||
// TODO: add order by
|
||||
draft.edges.push({
|
||||
node: updatedRecord,
|
||||
cursor: '',
|
||||
__typename: `${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}Edge`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return newRecordPaginatedCacheField;
|
||||
},
|
||||
objectMetadataItem,
|
||||
});
|
||||
@ -1,77 +1,67 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
||||
import { useGenerateCachedObjectRecord } from '@/object-record/hooks/useGenerateCachedObjectRecord';
|
||||
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
|
||||
export const useCreateManyRecords = <T extends ObjectRecord>({
|
||||
export const useCreateManyRecords = <
|
||||
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem, createManyRecordsMutation } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { generateEmptyRecord } = useGenerateEmptyRecord({
|
||||
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const createManyRecords = async (data: Partial<T>[]) => {
|
||||
const withIds = data.map((record) => ({
|
||||
...record,
|
||||
id: (record.id as string) ?? v4(),
|
||||
}));
|
||||
const createManyRecords = async (data: Partial<CreatedObjectRecord>[]) => {
|
||||
const optimisticallyCreatedRecords = data.map((record) =>
|
||||
generateCachedObjectRecord<CreatedObjectRecord>(record),
|
||||
);
|
||||
|
||||
withIds.forEach((record) => {
|
||||
const emptyRecord: T | undefined = generateEmptyRecord({
|
||||
id: record.id,
|
||||
} as T);
|
||||
const sanitizedCreateManyRecordsInput = data.map((input, index) =>
|
||||
sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: { ...input, id: optimisticallyCreatedRecords[index].id },
|
||||
}),
|
||||
);
|
||||
|
||||
if (emptyRecord) {
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
createdRecords: [emptyRecord],
|
||||
});
|
||||
}
|
||||
});
|
||||
const mutationResponseField = getCreateManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const createdObjects = await apolloClient.mutate({
|
||||
mutation: createManyRecordsMutation,
|
||||
variables: {
|
||||
data: withIds,
|
||||
data: sanitizedCreateManyRecordsInput,
|
||||
},
|
||||
optimisticResponse: {
|
||||
[`create${capitalize(objectMetadataItem.namePlural)}`]: withIds.map(
|
||||
(record) => generateEmptyRecord({ id: record.id }),
|
||||
),
|
||||
[mutationResponseField]: optimisticallyCreatedRecords,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
|
||||
if (!records?.length) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdObjects.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createdRecords =
|
||||
createdObjects.data[
|
||||
`create${capitalize(objectMetadataItem.namePlural)}`
|
||||
] ?? [];
|
||||
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
createdRecords,
|
||||
});
|
||||
|
||||
return createdRecords as T[];
|
||||
return createdObjects.data?.[mutationResponseField] ?? [];
|
||||
};
|
||||
|
||||
return { createManyRecords };
|
||||
|
||||
@ -1,73 +1,66 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
||||
import { useGenerateCachedObjectRecord } from '@/object-record/hooks/useGenerateCachedObjectRecord';
|
||||
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useCreateOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const useCreateOneRecord = <T>({
|
||||
export const useCreateOneRecord = <
|
||||
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
objectNameSingular,
|
||||
}: useCreateOneRecordProps) => {
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
|
||||
{
|
||||
objectNameSingular,
|
||||
},
|
||||
{ objectNameSingular },
|
||||
);
|
||||
|
||||
// TODO: type this with a minimal type at least with Record<string, any>
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { generateEmptyRecord } = useGenerateEmptyRecord({
|
||||
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const createOneRecord = async (input: Record<string, any>) => {
|
||||
const recordId = v4();
|
||||
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
||||
const optimisticallyCreatedRecord =
|
||||
generateCachedObjectRecord<CreatedObjectRecord>(input);
|
||||
|
||||
const generatedEmptyRecord = generateEmptyRecord({
|
||||
id: recordId,
|
||||
createdAt: new Date().toISOString(),
|
||||
...input,
|
||||
});
|
||||
|
||||
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
||||
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
|
||||
objectMetadataItem,
|
||||
recordInput: input,
|
||||
recordInput: { ...input, id: optimisticallyCreatedRecord.id },
|
||||
});
|
||||
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
createdRecords: [generatedEmptyRecord],
|
||||
});
|
||||
const mutationResponseField =
|
||||
getCreateOneRecordMutationResponseField(objectNameSingular);
|
||||
|
||||
const createdObject = await apolloClient.mutate({
|
||||
mutation: createOneRecordMutation,
|
||||
variables: {
|
||||
input: { id: recordId, ...sanitizedUpdateOneRecordInput },
|
||||
input: sanitizedCreateOneRecordInput,
|
||||
},
|
||||
optimisticResponse: {
|
||||
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
|
||||
generatedEmptyRecord,
|
||||
[mutationResponseField]: optimisticallyCreatedRecord,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
|
||||
if (!record) return;
|
||||
|
||||
triggerCreateRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records: [record],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdObject.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createdObject.data[
|
||||
`create${capitalize(objectMetadataItem.nameSingular)}`
|
||||
] as T;
|
||||
return createdObject.data?.[mutationResponseField] ?? null;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
@ -12,39 +11,19 @@ type useDeleteOneRecordProps = {
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
export const useDeleteManyRecords = <T>({
|
||||
export const useDeleteManyRecords = ({
|
||||
objectNameSingular,
|
||||
refetchFindManyQuery = false,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
const { performOptimisticEvict } = useOptimisticEvict();
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const {
|
||||
objectMetadataItem,
|
||||
deleteManyRecordsMutation,
|
||||
findManyRecordsQuery,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { objectMetadataItem, deleteManyRecordsMutation } =
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const deleteManyRecords = async (idsToDelete: string[]) => {
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
deletedRecordIds: idsToDelete,
|
||||
});
|
||||
|
||||
idsToDelete.forEach((idToDelete) => {
|
||||
performOptimisticEvict(
|
||||
capitalize(objectMetadataItem.nameSingular),
|
||||
'id',
|
||||
idToDelete,
|
||||
);
|
||||
});
|
||||
|
||||
const deleteRecordFilter: ObjectRecordQueryFilter = {
|
||||
id: {
|
||||
in: idsToDelete,
|
||||
@ -56,14 +35,26 @@ export const useDeleteManyRecords = <T>({
|
||||
filter: deleteRecordFilter,
|
||||
// atMost: idsToDelete.length,
|
||||
},
|
||||
refetchQueries: refetchFindManyQuery
|
||||
? [getOperationName(findManyRecordsQuery) ?? '']
|
||||
: [],
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDelete,
|
||||
})),
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
|
||||
if (!records?.length) return;
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return deletedRecords.data[
|
||||
`delete${capitalize(objectMetadataItem.namePlural)}`
|
||||
] as T;
|
||||
return deletedRecords.data?.[mutationResponseField] ?? null;
|
||||
};
|
||||
|
||||
return { deleteManyRecords };
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useDeleteOneRecordProps = {
|
||||
@ -12,57 +11,50 @@ type useDeleteOneRecordProps = {
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
export const useDeleteOneRecord = <T>({
|
||||
export const useDeleteOneRecord = ({
|
||||
objectNameSingular,
|
||||
refetchFindManyQuery = false,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
const { performOptimisticEvict } = useOptimisticEvict();
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem, deleteOneRecordMutation, findManyRecordsQuery } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem(
|
||||
{ objectNameSingular },
|
||||
);
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const mutationResponseField =
|
||||
getDeleteOneRecordMutationResponseField(objectNameSingular);
|
||||
|
||||
const deleteOneRecord = useCallback(
|
||||
async (idToDelete: string) => {
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
deletedRecordIds: [idToDelete],
|
||||
});
|
||||
|
||||
performOptimisticEvict(
|
||||
capitalize(objectMetadataItem.nameSingular),
|
||||
'id',
|
||||
idToDelete,
|
||||
);
|
||||
|
||||
const deletedRecord = await apolloClient.mutate({
|
||||
mutation: deleteOneRecordMutation,
|
||||
variables: {
|
||||
idToDelete,
|
||||
variables: { idToDelete },
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: {
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDelete,
|
||||
},
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const record = data?.[mutationResponseField];
|
||||
|
||||
if (!record) return;
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
records: [record],
|
||||
});
|
||||
},
|
||||
refetchQueries: refetchFindManyQuery
|
||||
? [getOperationName(findManyRecordsQuery) ?? '']
|
||||
: [],
|
||||
});
|
||||
|
||||
return deletedRecord.data[
|
||||
`delete${capitalize(objectMetadataItem.nameSingular)}`
|
||||
] as T;
|
||||
return deletedRecord.data?.[mutationResponseField] ?? null;
|
||||
},
|
||||
[
|
||||
triggerOptimisticEffects,
|
||||
objectMetadataItem.nameSingular,
|
||||
performOptimisticEvict,
|
||||
apolloClient,
|
||||
deleteOneRecordMutation,
|
||||
refetchFindManyQuery,
|
||||
findManyRecordsQuery,
|
||||
mutationResponseField,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -6,14 +6,12 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useRecordOptimisticEffect } from '@/object-metadata/hooks/useRecordOptimisticEffect';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
|
||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@ -34,14 +32,12 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
onCompleted,
|
||||
skip,
|
||||
useRecordsWithoutConnection = false,
|
||||
}: ObjectMetadataItemIdentifier & {
|
||||
filter?: ObjectRecordQueryFilter;
|
||||
orderBy?: OrderByField;
|
||||
limit?: number;
|
||||
onCompleted?: (data: ObjectRecordConnection<T>) => void;
|
||||
skip?: boolean;
|
||||
useRecordsWithoutConnection?: boolean;
|
||||
}) => {
|
||||
}: ObjectMetadataItemIdentifier &
|
||||
ObjectRecordQueryVariables & {
|
||||
onCompleted?: (data: ObjectRecordConnection<T>) => void;
|
||||
skip?: boolean;
|
||||
useRecordsWithoutConnection?: boolean;
|
||||
}) => {
|
||||
const findManyQueryStateIdentifier =
|
||||
objectNameSingular +
|
||||
JSON.stringify(filter) +
|
||||
@ -64,13 +60,6 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
useRecordOptimisticEffect({
|
||||
objectMetadataItem,
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
});
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useGenerateCachedObjectRecord = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const generateCachedObjectRecord = <
|
||||
GeneratedObjectRecord extends ObjectRecord,
|
||||
>(
|
||||
input: Record<string, unknown>,
|
||||
) => {
|
||||
const recordSchema = z.object(
|
||||
Object.fromEntries(
|
||||
objectMetadataItem.fields.map((fieldMetadataItem) => [
|
||||
fieldMetadataItem.name,
|
||||
z.unknown().default(generateEmptyFieldValue(fieldMetadataItem)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
...recordSchema.parse({
|
||||
id: v4(),
|
||||
createdAt: new Date().toISOString(),
|
||||
...input,
|
||||
}),
|
||||
} as GeneratedObjectRecord & { __typename: string };
|
||||
};
|
||||
|
||||
return {
|
||||
generateCachedObjectRecord,
|
||||
};
|
||||
};
|
||||
@ -5,6 +5,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getCreateManyRecordsMutationResponseField = (
|
||||
objectNamePlural: string,
|
||||
) => `create${capitalize(objectNamePlural)}`;
|
||||
|
||||
export const useGenerateCreateManyRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
@ -16,11 +20,15 @@ export const useGenerateCreateManyRecordMutation = ({
|
||||
return EMPTY_MUTATION;
|
||||
}
|
||||
|
||||
const mutationResponseField = getCreateManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
return gql`
|
||||
mutation Create${capitalize(
|
||||
objectMetadataItem.namePlural,
|
||||
)}($data: [${capitalize(objectMetadataItem.nameSingular)}CreateInput!]!) {
|
||||
create${capitalize(objectMetadataItem.namePlural)}(data: $data) {
|
||||
${mutationResponseField}(data: $data) {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
|
||||
@ -5,6 +5,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getCreateOneRecordMutationResponseField = (
|
||||
objectNameSingular: string,
|
||||
) => `create${capitalize(objectNameSingular)}`;
|
||||
|
||||
export const useGenerateCreateOneRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
@ -18,9 +22,13 @@ export const useGenerateCreateOneRecordMutation = ({
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
|
||||
const mutationResponseField = getCreateOneRecordMutationResponseField(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
return gql`
|
||||
mutation CreateOne${capitalizedObjectName}($input: ${capitalizedObjectName}CreateInput!) {
|
||||
create${capitalizedObjectName}(data: $input) {
|
||||
${mutationResponseField}(data: $input) {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
|
||||
@ -4,6 +4,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getDeleteManyRecordsMutationResponseField = (
|
||||
objectNamePlural: string,
|
||||
) => `delete${capitalize(objectNamePlural)}`;
|
||||
|
||||
export const useGenerateDeleteManyRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
@ -15,11 +19,15 @@ export const useGenerateDeleteManyRecordMutation = ({
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
|
||||
|
||||
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
return gql`
|
||||
mutation DeleteMany${capitalizedObjectName}($filter: ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}FilterInput!) {
|
||||
delete${capitalizedObjectName}(filter: $filter) {
|
||||
${mutationResponseField}(filter: $filter) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
|
||||
|
||||
export const useGenerateEmptyRecord = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
// Todo fix typing once we generate the return base on Metadata
|
||||
const generateEmptyRecord = <T extends ObjectRecord>(input: T) => {
|
||||
// Todo replace this by runtime typing
|
||||
const validatedInput = input as T;
|
||||
|
||||
const emptyRecord = {} as any;
|
||||
|
||||
for (const fieldMetadataItem of objectMetadataItem.fields) {
|
||||
emptyRecord[fieldMetadataItem.name] =
|
||||
validatedInput[fieldMetadataItem.name] ??
|
||||
generateEmptyFieldValue(fieldMetadataItem);
|
||||
}
|
||||
|
||||
return emptyRecord as T;
|
||||
};
|
||||
|
||||
return {
|
||||
generateEmptyRecord,
|
||||
};
|
||||
};
|
||||
@ -5,13 +5,9 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getUpdateOneRecordMutationGraphQLField = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
return `update${capitalize(objectNameSingular)}`;
|
||||
};
|
||||
export const getUpdateOneRecordMutationResponseField = (
|
||||
objectNameSingular: string,
|
||||
) => `update${capitalize(objectNameSingular)}`;
|
||||
|
||||
export const useGenerateUpdateOneRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
@ -26,14 +22,13 @@ export const useGenerateUpdateOneRecordMutation = ({
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
|
||||
const graphQLFieldForUpdateOneRecordMutation =
|
||||
getUpdateOneRecordMutationGraphQLField({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
const mutationResponseField = getUpdateOneRecordMutationResponseField(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
return gql`
|
||||
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
|
||||
${graphQLFieldForUpdateOneRecordMutation}(id: $idToUpdate, data: $input) {
|
||||
${mutationResponseField}(id: $idToUpdate, data: $input) {
|
||||
id
|
||||
${objectMetadataItem.fields
|
||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { gql, useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
||||
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useGetRecordFromCache = ({
|
||||
@ -13,9 +13,11 @@ export const useGetRecordFromCache = ({
|
||||
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
return (recordId: string) => {
|
||||
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
|
||||
recordId: string,
|
||||
) => {
|
||||
if (!objectMetadataItem) {
|
||||
return EMPTY_MUTATION;
|
||||
return null;
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
@ -35,7 +37,7 @@ export const useGetRecordFromCache = ({
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
return cache.readFragment({
|
||||
return cache.readFragment<CachedObjectRecord>({
|
||||
id: cachedRecordId,
|
||||
fragment: cacheReadFragment,
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Modifier, Reference } from '@apollo/client/cache';
|
||||
import { Modifiers } from '@apollo/client/cache';
|
||||
|
||||
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useModifyRecordFromCache = ({
|
||||
@ -10,23 +10,20 @@ export const useModifyRecordFromCache = ({
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
const { cache } = useApolloClient();
|
||||
|
||||
return (
|
||||
return <CachedObjectRecord extends ObjectRecord>(
|
||||
recordId: string,
|
||||
fieldModifiers: Record<string, Modifier<Reference>>,
|
||||
fieldModifiers: Modifiers<CachedObjectRecord>,
|
||||
) => {
|
||||
if (!objectMetadataItem) {
|
||||
return EMPTY_MUTATION;
|
||||
}
|
||||
if (!objectMetadataItem) return;
|
||||
|
||||
const cache = apolloClient.cache;
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
cache.modify<Record<string, Reference>>({
|
||||
cache.modify<CachedObjectRecord>({
|
||||
id: cachedRecordId,
|
||||
fields: fieldModifiers,
|
||||
});
|
||||
|
||||
@ -55,7 +55,7 @@ export const useObjectRecordBoard = () => {
|
||||
|
||||
useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.PipelineStep,
|
||||
filter: {},
|
||||
filter,
|
||||
onCompleted: useCallback(
|
||||
(data: ObjectRecordConnection<PipelineStep>) => {
|
||||
setSavedPipelineSteps(data.edges.map((edge) => edge.node));
|
||||
|
||||
@ -1,27 +1,23 @@
|
||||
import { Reference, useApolloClient } from '@apollo/client';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
type useUpdateOneRecordProps = {
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const useUpdateOneRecord = <T>({
|
||||
export const useUpdateOneRecord = <
|
||||
UpdatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
objectNameSingular,
|
||||
}: useUpdateOneRecordProps) => {
|
||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
useObjectMetadataItem({ objectNameSingular });
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
@ -30,13 +26,15 @@ export const useUpdateOneRecord = <T>({
|
||||
updateOneRecordInput,
|
||||
}: {
|
||||
idToUpdate: string;
|
||||
updateOneRecordInput: Record<string, unknown>;
|
||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||
}) => {
|
||||
const cachedRecord = getRecordFromCache(idToUpdate);
|
||||
const cachedRecord = getRecordFromCache<UpdatedObjectRecord>(idToUpdate);
|
||||
|
||||
const optimisticallyUpdatedRecord: Record<string, any> = {
|
||||
const optimisticallyUpdatedRecord = {
|
||||
...(cachedRecord ?? {}),
|
||||
...updateOneRecordInput,
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToUpdate,
|
||||
};
|
||||
|
||||
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
||||
@ -44,82 +42,32 @@ export const useUpdateOneRecord = <T>({
|
||||
recordInput: updateOneRecordInput,
|
||||
});
|
||||
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
updatedRecords: [optimisticallyUpdatedRecord],
|
||||
});
|
||||
const mutationResponseField =
|
||||
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
||||
|
||||
const updatedRecord = await apolloClient.mutate({
|
||||
mutation: updateOneRecordMutation,
|
||||
variables: {
|
||||
idToUpdate,
|
||||
input: {
|
||||
...sanitizedUpdateOneRecordInput,
|
||||
},
|
||||
input: sanitizedUpdateOneRecordInput,
|
||||
},
|
||||
optimisticResponse: {
|
||||
[`update${capitalize(objectMetadataItem.nameSingular)}`]:
|
||||
optimisticallyUpdatedRecord,
|
||||
[mutationResponseField]: optimisticallyUpdatedRecord,
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const response =
|
||||
data?.[`update${capitalize(objectMetadataItem.nameSingular)}`];
|
||||
const record = data?.[mutationResponseField];
|
||||
|
||||
if (!response) return;
|
||||
if (!record) return;
|
||||
|
||||
cache.modify<Record<string, Reference>>({
|
||||
fields: {
|
||||
[objectMetadataItem.namePlural]: (
|
||||
existingConnectionRef,
|
||||
{ readField, storeFieldName },
|
||||
) => {
|
||||
if (
|
||||
readField('__typename', existingConnectionRef) !==
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Connection`
|
||||
)
|
||||
return existingConnectionRef;
|
||||
|
||||
const { variables } = parseApolloStoreFieldName(storeFieldName);
|
||||
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
existingConnectionRef,
|
||||
);
|
||||
|
||||
if (
|
||||
variables?.filter &&
|
||||
!isRecordMatchingFilter({
|
||||
record: response,
|
||||
filter: variables.filter,
|
||||
objectMetadataItem,
|
||||
}) &&
|
||||
edges?.length
|
||||
) {
|
||||
return {
|
||||
...existingConnectionRef,
|
||||
edges: edges.filter(
|
||||
(edge) =>
|
||||
readField('id', readField('node', edge)) !== response.id,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return existingConnectionRef;
|
||||
},
|
||||
},
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
record,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!updatedRecord?.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatedData = updatedRecord.data[
|
||||
`update${capitalize(objectMetadataItem.nameSingular)}`
|
||||
] as T;
|
||||
|
||||
return updatedData;
|
||||
return updatedRecord?.data?.[mutationResponseField] ?? null;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -4,7 +4,6 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
|
||||
import { Opportunity } from '@/pipeline/types/Opportunity';
|
||||
|
||||
import { useRemoveRecordBoardCardIdsInternal } from './useRemoveRecordBoardCardIdsInternal';
|
||||
|
||||
@ -12,10 +11,9 @@ export const useDeleteSelectedRecordBoardCardsInternal = () => {
|
||||
const removeCardIds = useRemoveRecordBoardCardIdsInternal();
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { deleteManyRecords: deleteManyOpportunities } =
|
||||
useDeleteManyRecords<Opportunity>({
|
||||
objectNameSingular: CoreObjectNameSingular.Opportunity,
|
||||
});
|
||||
const { deleteManyRecords: deleteManyOpportunities } = useDeleteManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Opportunity,
|
||||
});
|
||||
|
||||
const { selectedCardIdsSelector } = useRecordBoardScopedStates();
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
URLFilter,
|
||||
UUIDFilter,
|
||||
} from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { Field } from '~/generated/graphql';
|
||||
|
||||
@ -24,7 +25,7 @@ export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
||||
export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
rawUIFilters: ObjectDropdownFilter[],
|
||||
fields: Pick<Field, 'id' | 'name'>[],
|
||||
): ObjectRecordQueryFilter => {
|
||||
): ObjectRecordQueryFilter | undefined => {
|
||||
const objectRecordFilters: ObjectRecordQueryFilter[] = [];
|
||||
|
||||
for (const rawUIFilter of rawUIFilters) {
|
||||
@ -134,13 +135,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsNot:
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as UUIDFilter,
|
||||
},
|
||||
});
|
||||
if (parsedRecordIds.length) {
|
||||
objectRecordFilters.push({
|
||||
not: {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as UUIDFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
@ -257,5 +260,5 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
}
|
||||
}
|
||||
|
||||
return { and: objectRecordFilters };
|
||||
return andFilterVariables(objectRecordFilters);
|
||||
};
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { Reference } from '@apollo/client';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { LightIconButton, MenuItem } from 'tsup.ui.index';
|
||||
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldDisplay } from '@/object-record/field/components/FieldDisplay';
|
||||
@ -13,7 +13,6 @@ import { usePersistField } from '@/object-record/field/hooks/usePersistField';
|
||||
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { IconDotsVertical, IconUnlink } from '@/ui/display/icon';
|
||||
@ -71,14 +70,10 @@ export const RecordRelationFieldCardContent = ({
|
||||
objectMetadataNameSingular,
|
||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
const { modifyRecordFromCache } = useObjectMetadataItem({
|
||||
objectNameSingular: objectMetadataNameSingular ?? '',
|
||||
});
|
||||
|
||||
const modifyRecordFromCache = useModifyRecordFromCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||
const {
|
||||
labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata,
|
||||
@ -104,13 +99,13 @@ export const RecordRelationFieldCardContent = ({
|
||||
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownScopeId);
|
||||
|
||||
// TODO: temporary as ChipDisplay expect to find the entity in the entityFieldsFamilyState
|
||||
const setEntityFields = useSetRecoilState(
|
||||
const setRelationEntityFields = useSetRecoilState(
|
||||
entityFieldsFamilyState(relationRecord.id),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setEntityFields(relationRecord);
|
||||
}, [relationRecord, setEntityFields]);
|
||||
setRelationEntityFields(relationRecord);
|
||||
}, [relationRecord, setRelationEntityFields]);
|
||||
|
||||
if (!FieldContextProvider) return null;
|
||||
|
||||
@ -137,15 +132,18 @@ export const RecordRelationFieldCardContent = ({
|
||||
});
|
||||
|
||||
modifyRecordFromCache(entityId, {
|
||||
[fieldName]: (relationRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
||||
[fieldName]: (cachedRelationConnection, { readField }) => {
|
||||
const edges = readField<CachedObjectRecordEdge[]>(
|
||||
'edges',
|
||||
cachedRelationConnection,
|
||||
);
|
||||
|
||||
if (!edges) {
|
||||
return relationRef;
|
||||
return cachedRelationConnection;
|
||||
}
|
||||
|
||||
return {
|
||||
...relationRef,
|
||||
...cachedRelationConnection,
|
||||
edges: edges.filter(({ node }) => {
|
||||
const id = readField('id', node);
|
||||
return id !== relationRecord.id;
|
||||
|
||||
@ -139,7 +139,6 @@ export const RecordRelationFieldCardSection = () => {
|
||||
],
|
||||
orderByField: 'createdAt',
|
||||
selectedIds: relationRecordIds,
|
||||
excludeEntityIds: relationRecordIds,
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
|
||||
@ -42,19 +42,6 @@ const response = {
|
||||
};
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query,
|
||||
variables: {
|
||||
filterNameSingular: { and: [{}, { id: { in: ['1'] } }] },
|
||||
orderByNameSingular: { createdAt: 'DescNullsLast' },
|
||||
limitNameSingular: 60,
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: response,
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query,
|
||||
@ -72,8 +59,20 @@ const mocks = [
|
||||
request: {
|
||||
query,
|
||||
variables: {
|
||||
orderByNameSingular: { createdAt: 'DescNullsLast' },
|
||||
limitNameSingular: 60,
|
||||
filterNameSingular: { and: [{}, { not: { id: { in: ['1'] } } }] },
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: response,
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query,
|
||||
variables: {
|
||||
limitNameSingular: 60,
|
||||
filterNameSingular: { not: { id: { in: ['1'] } } },
|
||||
orderByNameSingular: { createdAt: 'DescNullsLast' },
|
||||
},
|
||||
},
|
||||
|
||||
@ -53,23 +53,9 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({
|
||||
|
||||
if (!isNonEmptyArray(selectedIds)) return null;
|
||||
|
||||
const searchFilter =
|
||||
searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
|
||||
|
||||
return [
|
||||
`filter${capitalize(nameSingular)}`,
|
||||
{
|
||||
and: [
|
||||
{
|
||||
...searchFilter,
|
||||
},
|
||||
{
|
||||
id: {
|
||||
in: selectedIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
searchFilterPerMetadataItemNameSingular[nameSingular],
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
@ -14,7 +13,7 @@ import {
|
||||
import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||
import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
|
||||
import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
@ -58,35 +57,19 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
|
||||
)
|
||||
.map(({ id }) => id);
|
||||
|
||||
const searchFilter =
|
||||
searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
|
||||
|
||||
const excludedIdsUnion = [...selectedIds, ...excludedIds];
|
||||
const excludedIdsFilter = excludedIdsUnion.length
|
||||
? { not: { id: { in: excludedIdsUnion } } }
|
||||
: undefined;
|
||||
|
||||
const noFilter =
|
||||
!isNonEmptyArray(excludedIdsUnion) &&
|
||||
isDeeplyEqual(searchFilter, {});
|
||||
const searchFilters = [
|
||||
searchFilterPerMetadataItemNameSingular[nameSingular],
|
||||
excludedIdsFilter,
|
||||
];
|
||||
|
||||
return [
|
||||
`filter${capitalize(nameSingular)}`,
|
||||
!noFilter
|
||||
? {
|
||||
and: [
|
||||
{
|
||||
...searchFilter,
|
||||
},
|
||||
isNonEmptyArray(excludedIdsUnion)
|
||||
? {
|
||||
not: {
|
||||
id: {
|
||||
in: [...selectedIds, ...excludedIds],
|
||||
},
|
||||
},
|
||||
}
|
||||
: {},
|
||||
],
|
||||
}
|
||||
: {},
|
||||
andFilterVariables(searchFilters),
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
|
||||
@ -5,7 +5,8 @@ import { OrderBy } from '@/object-metadata/types/OrderBy';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
|
||||
import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
|
||||
import { orFilterVariables } from '@/object-record/utils/orFilterVariables';
|
||||
|
||||
export const DEFAULT_SEARCH_REQUEST_LIMIT = 60;
|
||||
|
||||
@ -37,85 +38,62 @@ export const useRecordsForSelect = ({
|
||||
];
|
||||
|
||||
const orderByField = getObjectOrderByField(sortOrder);
|
||||
const selectedIdsFilter = { id: { in: selectedIds } };
|
||||
|
||||
const { loading: selectedRecordsLoading, records: selectedRecordsData } =
|
||||
useFindManyRecords({
|
||||
filter: {
|
||||
id: {
|
||||
in: selectedIds,
|
||||
},
|
||||
},
|
||||
filter: selectedIdsFilter,
|
||||
orderBy: orderByField,
|
||||
objectNameSingular,
|
||||
skip: !selectedIds.length,
|
||||
});
|
||||
|
||||
const searchFilter = filters
|
||||
.map(({ fieldNames, filter }) => {
|
||||
if (!isNonEmptyString(filter)) {
|
||||
return undefined;
|
||||
}
|
||||
const searchFilters = filters.map(({ fieldNames, filter }) => {
|
||||
if (!isNonEmptyString(filter)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
or: fieldNames.map((fieldName) => {
|
||||
const fieldNameParts = fieldName.split('.');
|
||||
return orFilterVariables(
|
||||
fieldNames.map((fieldName) => {
|
||||
const [parentFieldName, subFieldName] = fieldName.split('.');
|
||||
|
||||
if (fieldNameParts.length > 1) {
|
||||
// Composite field
|
||||
|
||||
return {
|
||||
[fieldNameParts[0]]: {
|
||||
[fieldNameParts[1]]: {
|
||||
ilike: `%${filter}%`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (subFieldName) {
|
||||
// Composite field
|
||||
return {
|
||||
[fieldName]: {
|
||||
ilike: `%${filter}%`,
|
||||
[parentFieldName]: {
|
||||
[subFieldName]: {
|
||||
ilike: `%${filter}%`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
return {
|
||||
[fieldName]: {
|
||||
ilike: `%${filter}%`,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const {
|
||||
loading: filteredSelectedRecordsLoading,
|
||||
records: filteredSelectedRecordsData,
|
||||
} = useFindManyRecords({
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
and: searchFilter,
|
||||
},
|
||||
{
|
||||
id: {
|
||||
in: selectedIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
filter: andFilterVariables([...searchFilters, selectedIdsFilter]),
|
||||
orderBy: orderByField,
|
||||
objectNameSingular,
|
||||
skip: !selectedIds.length,
|
||||
});
|
||||
|
||||
const notFilterIds = [...selectedIds, ...excludeEntityIds];
|
||||
const notFilter = notFilterIds.length
|
||||
? { not: { id: { in: notFilterIds } } }
|
||||
: undefined;
|
||||
const { loading: recordsToSelectLoading, records: recordsToSelectData } =
|
||||
useFindManyRecords({
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
and: searchFilter,
|
||||
},
|
||||
{
|
||||
not: {
|
||||
id: {
|
||||
in: [...selectedIds, ...excludeEntityIds],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
filter: andFilterVariables([...searchFilters, notFilter]),
|
||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||
orderBy: orderByField,
|
||||
objectNameSingular,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export type ObjectRecordEdge<T extends ObjectRecord> = {
|
||||
export type ObjectRecordEdge<T extends ObjectRecord = ObjectRecord> = {
|
||||
__typename?: string;
|
||||
node: T;
|
||||
cursor: string;
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const andFilterVariables = (
|
||||
filters: (ObjectRecordQueryFilter | undefined)[],
|
||||
): ObjectRecordQueryFilter | undefined => {
|
||||
const definedFilters = filters.filter(isDefined);
|
||||
|
||||
if (!definedFilters.length) return undefined;
|
||||
|
||||
return definedFilters.length === 1
|
||||
? definedFilters[0]
|
||||
: { and: definedFilters };
|
||||
};
|
||||
@ -4,6 +4,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getDeleteOneRecordMutationResponseField = (
|
||||
objectNameSingular: string,
|
||||
) => `delete${capitalize(objectNameSingular)}`;
|
||||
|
||||
export const generateDeleteOneRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
@ -15,9 +19,13 @@ export const generateDeleteOneRecordMutation = ({
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
|
||||
const mutationResponseField = getDeleteOneRecordMutationResponseField(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
return gql`
|
||||
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
|
||||
delete${capitalizedObjectName}(id: $idToDelete) {
|
||||
${mutationResponseField}(id: $idToDelete) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const orFilterVariables = (
|
||||
filters: (ObjectRecordQueryFilter | undefined)[],
|
||||
): ObjectRecordQueryFilter | undefined => {
|
||||
const definedFilters = filters.filter(isDefined);
|
||||
|
||||
if (!definedFilters.length) return undefined;
|
||||
|
||||
return definedFilters.length === 1
|
||||
? definedFilters[0]
|
||||
: { or: definedFilters };
|
||||
};
|
||||
Reference in New Issue
Block a user