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:
@ -62,10 +62,9 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => {
|
|||||||
[attachment?.id],
|
[attachment?.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { deleteOneRecord: deleteOneAttachment } =
|
const { deleteOneRecord: deleteOneAttachment } = useDeleteOneRecord({
|
||||||
useDeleteOneRecord<Attachment>({
|
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
deleteOneAttachment(attachment.id);
|
deleteOneAttachment(attachment.id);
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export const useUploadAttachmentFile = () => {
|
|||||||
fullPath: attachmentUrl,
|
fullPath: attachmentUrl,
|
||||||
type: getFileType(file.name),
|
type: getFileType(file.name),
|
||||||
[targetableObjectFieldIdName]: targetableObject.id,
|
[targetableObjectFieldIdName]: targetableObject.id,
|
||||||
};
|
} as Partial<Attachment>;
|
||||||
|
|
||||||
await createOneAttachment(attachmentToCreate);
|
await createOneAttachment(attachmentToCreate);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,100 +0,0 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
||||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
|
||||||
import { optimisticEffectState } from '@/apollo/optimistic-effect/states/optimisticEffectState';
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
|
|
||||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<MockedProvider addTypename={false}>
|
|
||||||
<RecoilRoot>{children}</RecoilRoot>
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('useOptimisticEffect', () => {
|
|
||||||
it('should work as expected', async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => {
|
|
||||||
const optimisticEffect = useRecoilValue(optimisticEffectState);
|
|
||||||
const client = useApolloClient();
|
|
||||||
const { findManyRecordsQuery } = useObjectMetadataItem({
|
|
||||||
objectNameSingular: 'person',
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...useOptimisticEffect({ objectNameSingular: 'person' }),
|
|
||||||
optimisticEffect,
|
|
||||||
cache: client.cache,
|
|
||||||
findManyRecordsQuery,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
wrapper: Wrapper,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
registerOptimisticEffect,
|
|
||||||
unregisterOptimisticEffect,
|
|
||||||
triggerOptimisticEffects,
|
|
||||||
optimisticEffect,
|
|
||||||
findManyRecordsQuery,
|
|
||||||
} = result.current;
|
|
||||||
|
|
||||||
expect(registerOptimisticEffect).toBeDefined();
|
|
||||||
expect(typeof registerOptimisticEffect).toBe('function');
|
|
||||||
expect(optimisticEffect).toEqual({});
|
|
||||||
|
|
||||||
const optimisticEffectDefinition = {
|
|
||||||
variables: {},
|
|
||||||
definition: {
|
|
||||||
typename: 'Person',
|
|
||||||
resolver: () => ({
|
|
||||||
people: [],
|
|
||||||
pageInfo: {
|
|
||||||
endCursor: '',
|
|
||||||
hasNextPage: false,
|
|
||||||
hasPreviousPage: false,
|
|
||||||
startCursor: '',
|
|
||||||
},
|
|
||||||
edges: [],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
registerOptimisticEffect(optimisticEffectDefinition);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.optimisticEffect).toHaveProperty('Person-{}');
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
result.current.cache.readQuery({ query: findManyRecordsQuery }),
|
|
||||||
).toBeNull();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
triggerOptimisticEffects({
|
|
||||||
typename: 'Person',
|
|
||||||
createdRecords: [{ id: 'id-0' }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(
|
|
||||||
result.current.cache.readQuery({ query: findManyRecordsQuery }),
|
|
||||||
).toHaveProperty('people');
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
unregisterOptimisticEffect(optimisticEffectDefinition);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.optimisticEffect).not.toHaveProperty('Person-{}');
|
|
||||||
expect(result.current.optimisticEffect).toEqual({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,198 +0,0 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
|
||||||
import { useRecoilCallback } from 'recoil';
|
|
||||||
|
|
||||||
import { computeOptimisticEffectKey } from '@/apollo/optimistic-effect/utils/computeOptimisticEffectKey';
|
|
||||||
import {
|
|
||||||
EMPTY_QUERY,
|
|
||||||
useObjectMetadataItem,
|
|
||||||
} from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
|
||||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
|
||||||
|
|
||||||
import { optimisticEffectState } from '../states/optimisticEffectState';
|
|
||||||
import {
|
|
||||||
OptimisticEffect,
|
|
||||||
OptimisticEffectWriter,
|
|
||||||
} from '../types/internal/OptimisticEffect';
|
|
||||||
import { OptimisticEffectDefinition } from '../types/OptimisticEffectDefinition';
|
|
||||||
|
|
||||||
export const useOptimisticEffect = ({
|
|
||||||
objectNameSingular,
|
|
||||||
}: ObjectMetadataItemIdentifier) => {
|
|
||||||
const apolloClient = useApolloClient();
|
|
||||||
|
|
||||||
const { findManyRecordsQuery, objectMetadataItem } = useObjectMetadataItem({
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const unregisterOptimisticEffect = useRecoilCallback(
|
|
||||||
({ snapshot, set }) =>
|
|
||||||
({
|
|
||||||
variables,
|
|
||||||
definition,
|
|
||||||
}: {
|
|
||||||
variables: ObjectRecordQueryVariables;
|
|
||||||
definition: OptimisticEffectDefinition;
|
|
||||||
}) => {
|
|
||||||
const optimisticEffects = snapshot
|
|
||||||
.getLoadable(optimisticEffectState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
const computedKey = computeOptimisticEffectKey({
|
|
||||||
variables,
|
|
||||||
definition,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { [computedKey]: _, ...rest } = optimisticEffects;
|
|
||||||
|
|
||||||
set(optimisticEffectState, rest);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const registerOptimisticEffect = useRecoilCallback(
|
|
||||||
({ snapshot, set }) =>
|
|
||||||
({
|
|
||||||
variables,
|
|
||||||
definition,
|
|
||||||
}: {
|
|
||||||
variables: ObjectRecordQueryVariables;
|
|
||||||
definition: OptimisticEffectDefinition;
|
|
||||||
}) => {
|
|
||||||
if (findManyRecordsQuery === EMPTY_QUERY) {
|
|
||||||
throw new Error(
|
|
||||||
`Trying to register an optimistic effect for unknown object ${objectNameSingular}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const optimisticEffects = snapshot
|
|
||||||
.getLoadable(optimisticEffectState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
const optimisticEffectWriter: OptimisticEffectWriter = ({
|
|
||||||
cache,
|
|
||||||
createdRecords,
|
|
||||||
updatedRecords,
|
|
||||||
deletedRecordIds,
|
|
||||||
query,
|
|
||||||
variables,
|
|
||||||
objectMetadataItem,
|
|
||||||
}) => {
|
|
||||||
if (objectMetadataItem) {
|
|
||||||
const existingData = cache.readQuery({
|
|
||||||
query: findManyRecordsQuery,
|
|
||||||
variables,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!existingData &&
|
|
||||||
(isNonEmptyArray(updatedRecords) ||
|
|
||||||
isNonEmptyArray(deletedRecordIds))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.writeQuery({
|
|
||||||
query: findManyRecordsQuery,
|
|
||||||
variables,
|
|
||||||
data: {
|
|
||||||
[objectMetadataItem.namePlural]: definition.resolver({
|
|
||||||
currentCacheData: (existingData as any)?.[
|
|
||||||
objectMetadataItem.namePlural
|
|
||||||
],
|
|
||||||
updatedRecords,
|
|
||||||
createdRecords,
|
|
||||||
deletedRecordIds,
|
|
||||||
variables,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingData = cache.readQuery({
|
|
||||||
query: query ?? findManyRecordsQuery,
|
|
||||||
variables,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const computedKey = computeOptimisticEffectKey({
|
|
||||||
variables,
|
|
||||||
definition,
|
|
||||||
});
|
|
||||||
|
|
||||||
const optimisticEffect = {
|
|
||||||
variables,
|
|
||||||
typename: definition.typename,
|
|
||||||
query: definition.query,
|
|
||||||
writer: optimisticEffectWriter,
|
|
||||||
objectMetadataItem,
|
|
||||||
} satisfies OptimisticEffect;
|
|
||||||
|
|
||||||
set(optimisticEffectState, {
|
|
||||||
...optimisticEffects,
|
|
||||||
[computedKey]: optimisticEffect,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[findManyRecordsQuery, objectNameSingular, objectMetadataItem],
|
|
||||||
);
|
|
||||||
|
|
||||||
const triggerOptimisticEffects = useRecoilCallback(
|
|
||||||
({ snapshot }) =>
|
|
||||||
({
|
|
||||||
typename,
|
|
||||||
createdRecords = [],
|
|
||||||
updatedRecords = [],
|
|
||||||
deletedRecordIds,
|
|
||||||
}: {
|
|
||||||
typename: string;
|
|
||||||
createdRecords?: Record<string, unknown>[];
|
|
||||||
updatedRecords?: Record<string, unknown>[];
|
|
||||||
deletedRecordIds?: string[];
|
|
||||||
}) => {
|
|
||||||
const optimisticEffects = snapshot
|
|
||||||
.getLoadable(optimisticEffectState)
|
|
||||||
.getValue();
|
|
||||||
|
|
||||||
for (const optimisticEffect of Object.values(optimisticEffects)) {
|
|
||||||
// We need to update the typename when createObject type differs from listObject types
|
|
||||||
// It is the case for apiKey, where the creation route returns an ApiKeyToken type
|
|
||||||
const formattedCreatedRecords = createdRecords.map((createdRecord) =>
|
|
||||||
typename.endsWith('Edge')
|
|
||||||
? createdRecord
|
|
||||||
: { ...createdRecord, __typename: typename },
|
|
||||||
);
|
|
||||||
|
|
||||||
const formattedUpdatedRecords = updatedRecords.map((updatedRecord) =>
|
|
||||||
typename.endsWith('Edge')
|
|
||||||
? updatedRecord
|
|
||||||
: { ...updatedRecord, __typename: typename },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (optimisticEffect.typename === typename) {
|
|
||||||
optimisticEffect.writer({
|
|
||||||
cache: apolloClient.cache,
|
|
||||||
query: optimisticEffect.query,
|
|
||||||
createdRecords: formattedCreatedRecords,
|
|
||||||
updatedRecords: formattedUpdatedRecords,
|
|
||||||
deletedRecordIds,
|
|
||||||
variables: optimisticEffect.variables,
|
|
||||||
objectMetadataItem: optimisticEffect.objectMetadataItem,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[apolloClient.cache],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
registerOptimisticEffect,
|
|
||||||
triggerOptimisticEffects,
|
|
||||||
unregisterOptimisticEffect,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { atom } from 'recoil';
|
|
||||||
|
|
||||||
import { OptimisticEffect } from '../types/internal/OptimisticEffect';
|
|
||||||
|
|
||||||
export const optimisticEffectState = atom<Record<string, OptimisticEffect>>({
|
|
||||||
key: 'optimisticEffectState',
|
|
||||||
default: {},
|
|
||||||
});
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { DocumentNode } from 'graphql';
|
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
|
|
||||||
import { OptimisticEffectResolver } from './OptimisticEffectResolver';
|
|
||||||
|
|
||||||
export type OptimisticEffectDefinition = {
|
|
||||||
query?: DocumentNode;
|
|
||||||
typename: string;
|
|
||||||
resolver: OptimisticEffectResolver;
|
|
||||||
objectMetadataItem?: ObjectMetadataItem;
|
|
||||||
};
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { OperationVariables } from '@apollo/client';
|
|
||||||
|
|
||||||
export type OptimisticEffectResolver = ({
|
|
||||||
currentCacheData,
|
|
||||||
createdRecords,
|
|
||||||
updatedRecords,
|
|
||||||
deletedRecordIds,
|
|
||||||
variables,
|
|
||||||
}: {
|
|
||||||
currentCacheData: any; //TODO: Change when decommissioning v1
|
|
||||||
createdRecords?: Record<string, unknown>[];
|
|
||||||
updatedRecords?: Record<string, unknown>[];
|
|
||||||
deletedRecordIds?: string[];
|
|
||||||
variables: OperationVariables;
|
|
||||||
}) => void;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { ApolloCache, DocumentNode } from '@apollo/client';
|
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
|
||||||
|
|
||||||
export type OptimisticEffectWriter = ({
|
|
||||||
cache,
|
|
||||||
query,
|
|
||||||
createdRecords,
|
|
||||||
updatedRecords,
|
|
||||||
deletedRecordIds,
|
|
||||||
variables,
|
|
||||||
objectMetadataItem,
|
|
||||||
}: {
|
|
||||||
cache: ApolloCache<any>;
|
|
||||||
query?: DocumentNode;
|
|
||||||
createdRecords?: Record<string, unknown>[];
|
|
||||||
updatedRecords?: Record<string, unknown>[];
|
|
||||||
deletedRecordIds?: string[];
|
|
||||||
variables: ObjectRecordQueryVariables;
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
}) => void;
|
|
||||||
|
|
||||||
export type OptimisticEffect = {
|
|
||||||
query?: DocumentNode;
|
|
||||||
typename: string;
|
|
||||||
variables: ObjectRecordQueryVariables;
|
|
||||||
writer: OptimisticEffectWriter;
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
|
|
||||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
|
||||||
|
|
||||||
export const computeOptimisticEffectKey = ({
|
|
||||||
variables,
|
|
||||||
definition,
|
|
||||||
}: {
|
|
||||||
variables: ObjectRecordQueryVariables;
|
|
||||||
definition: OptimisticEffectDefinition;
|
|
||||||
}) => {
|
|
||||||
const computedKey =
|
|
||||||
(definition.objectMetadataItem?.namePlural ?? definition.typename) +
|
|
||||||
'-' +
|
|
||||||
JSON.stringify(variables);
|
|
||||||
|
|
||||||
return computedKey;
|
|
||||||
};
|
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { StoreValue } from '@apollo/client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const isCachedObjectConnection = (
|
||||||
|
objectNameSingular: string,
|
||||||
|
storeValue: StoreValue,
|
||||||
|
): storeValue is CachedObjectRecordConnection => {
|
||||||
|
const objectConnectionTypeName = `${capitalize(
|
||||||
|
objectNameSingular,
|
||||||
|
)}Connection`;
|
||||||
|
const cachedObjectConnectionSchema = z.object({
|
||||||
|
__typename: z.literal(objectConnectionTypeName),
|
||||||
|
});
|
||||||
|
const cachedConnectionValidation =
|
||||||
|
cachedObjectConnectionSchema.safeParse(storeValue);
|
||||||
|
|
||||||
|
return cachedConnectionValidation.success;
|
||||||
|
};
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import { Reference, StoreObject } from '@apollo/client';
|
||||||
|
import { ReadFieldFunction } from '@apollo/client/cache/core/types/common';
|
||||||
|
|
||||||
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
|
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
||||||
|
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { sortAsc, sortDesc, sortNullsFirst, sortNullsLast } from '~/utils/sort';
|
||||||
|
|
||||||
|
export const sortCachedObjectEdges = ({
|
||||||
|
edges,
|
||||||
|
orderBy,
|
||||||
|
readCacheField,
|
||||||
|
}: {
|
||||||
|
edges: CachedObjectRecordEdge[];
|
||||||
|
orderBy: OrderByField;
|
||||||
|
readCacheField: ReadFieldFunction;
|
||||||
|
}) => {
|
||||||
|
const [orderByFieldName, orderByFieldValue] = Object.entries(orderBy)[0];
|
||||||
|
const [orderBySubFieldName, orderBySubFieldValue] =
|
||||||
|
typeof orderByFieldValue === 'string'
|
||||||
|
? []
|
||||||
|
: Object.entries(orderByFieldValue)[0];
|
||||||
|
|
||||||
|
const readFieldValueToSort = (
|
||||||
|
edge: CachedObjectRecordEdge,
|
||||||
|
): string | number | null => {
|
||||||
|
const recordFromCache = edge.node;
|
||||||
|
const fieldValue =
|
||||||
|
readCacheField<Reference | StoreObject | string | number | null>(
|
||||||
|
orderByFieldName,
|
||||||
|
recordFromCache,
|
||||||
|
) ?? null;
|
||||||
|
const isSubFieldFilter = isDefined(fieldValue) && !!orderBySubFieldName;
|
||||||
|
|
||||||
|
if (!isSubFieldFilter) return fieldValue as string | number | null;
|
||||||
|
|
||||||
|
const subFieldValue =
|
||||||
|
readCacheField<string | number | null>(
|
||||||
|
orderBySubFieldName,
|
||||||
|
fieldValue as Reference | StoreObject,
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
return subFieldValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderByValue = orderBySubFieldValue || (orderByFieldValue as OrderBy);
|
||||||
|
|
||||||
|
const isAsc = orderByValue.startsWith('Asc');
|
||||||
|
const isNullsFirst = orderByValue.endsWith('NullsFirst');
|
||||||
|
|
||||||
|
return [...edges].sort((edgeA, edgeB) => {
|
||||||
|
const fieldValueA = readFieldValueToSort(edgeA);
|
||||||
|
const fieldValueB = readFieldValueToSort(edgeB);
|
||||||
|
|
||||||
|
if (fieldValueA === null || fieldValueB === null) {
|
||||||
|
return isNullsFirst
|
||||||
|
? sortNullsFirst(fieldValueA, fieldValueB)
|
||||||
|
: sortNullsLast(fieldValueA, fieldValueB);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAsc
|
||||||
|
? sortAsc(fieldValueA, fieldValueB)
|
||||||
|
: sortDesc(fieldValueA, fieldValueB);
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
|
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
||||||
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are.
|
||||||
|
We need to refactor how the record creation works in the RecordTable so the created record row is temporarily displayed with a local state,
|
||||||
|
then we'll be able to uncomment the code below so the cached lists are updated coherently with the variables.
|
||||||
|
*/
|
||||||
|
export const triggerCreateRecordsOptimisticEffect = ({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
cache: ApolloCache<unknown>;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
records: CachedObjectRecord[];
|
||||||
|
}) => {
|
||||||
|
const objectEdgeTypeName = `${capitalize(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
)}Edge`;
|
||||||
|
|
||||||
|
cache.modify<StoreObject>({
|
||||||
|
fields: {
|
||||||
|
[objectMetadataItem.namePlural]: (
|
||||||
|
cachedConnection,
|
||||||
|
{
|
||||||
|
INVALIDATE: _INVALIDATE,
|
||||||
|
readField,
|
||||||
|
storeFieldName: _storeFieldName,
|
||||||
|
toReference,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!isCachedObjectConnection(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
cachedConnection,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return cachedConnection;
|
||||||
|
|
||||||
|
/* const { variables } =
|
||||||
|
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||||
|
storeFieldName,
|
||||||
|
); */
|
||||||
|
|
||||||
|
const cachedEdges = readField<CachedObjectRecordEdge[]>(
|
||||||
|
'edges',
|
||||||
|
cachedConnection,
|
||||||
|
);
|
||||||
|
const nextCachedEdges = cachedEdges ? [...cachedEdges] : [];
|
||||||
|
|
||||||
|
const hasAddedRecords = records
|
||||||
|
.map((record) => {
|
||||||
|
/* const matchesFilter =
|
||||||
|
!variables?.filter ||
|
||||||
|
isRecordMatchingFilter({
|
||||||
|
record,
|
||||||
|
filter: variables.filter,
|
||||||
|
objectMetadataItem,
|
||||||
|
}); */
|
||||||
|
|
||||||
|
if (/* matchesFilter && */ record.id) {
|
||||||
|
const nodeReference = toReference(record);
|
||||||
|
|
||||||
|
if (nodeReference) {
|
||||||
|
nextCachedEdges.unshift({
|
||||||
|
__typename: objectEdgeTypeName,
|
||||||
|
node: nodeReference,
|
||||||
|
cursor: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.some((hasAddedRecord) => hasAddedRecord);
|
||||||
|
|
||||||
|
if (!hasAddedRecords) return cachedConnection;
|
||||||
|
|
||||||
|
/* if (variables?.orderBy) {
|
||||||
|
nextCachedEdges = sortCachedObjectEdges({
|
||||||
|
edges: nextCachedEdges,
|
||||||
|
orderBy: variables.orderBy,
|
||||||
|
readCacheField: readField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(variables?.first)) {
|
||||||
|
if (
|
||||||
|
cachedEdges?.length === variables.first &&
|
||||||
|
nextCachedEdges.length < variables.first
|
||||||
|
) {
|
||||||
|
return INVALIDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextCachedEdges.length > variables.first) {
|
||||||
|
nextCachedEdges.splice(variables.first);
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
return { ...cachedConnection, edges: nextCachedEdges };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
|
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
||||||
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
|
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||||
|
|
||||||
|
export const triggerDeleteRecordsOptimisticEffect = ({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
records,
|
||||||
|
}: {
|
||||||
|
cache: ApolloCache<unknown>;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
records: Pick<CachedObjectRecord, 'id' | '__typename'>[];
|
||||||
|
}) => {
|
||||||
|
cache.modify<StoreObject>({
|
||||||
|
fields: {
|
||||||
|
[objectMetadataItem.namePlural]: (
|
||||||
|
cachedConnection,
|
||||||
|
{ INVALIDATE, readField, storeFieldName },
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!isCachedObjectConnection(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
cachedConnection,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return cachedConnection;
|
||||||
|
|
||||||
|
const { variables } =
|
||||||
|
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||||
|
storeFieldName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const recordIds = records.map(({ id }) => id);
|
||||||
|
|
||||||
|
const cachedEdges = readField<CachedObjectRecordEdge[]>(
|
||||||
|
'edges',
|
||||||
|
cachedConnection,
|
||||||
|
);
|
||||||
|
const nextCachedEdges =
|
||||||
|
cachedEdges?.filter((cachedEdge) => {
|
||||||
|
const nodeId = readField<string>('id', cachedEdge.node);
|
||||||
|
return nodeId && !recordIds.includes(nodeId);
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDefined(variables?.first) &&
|
||||||
|
cachedEdges?.length === variables.first &&
|
||||||
|
nextCachedEdges.length < variables.first
|
||||||
|
) {
|
||||||
|
return INVALIDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...cachedConnection, edges: nextCachedEdges };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
records.forEach((record) => cache.evict({ id: cache.identify(record) }));
|
||||||
|
};
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||||
|
|
||||||
|
import { isCachedObjectConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectConnection';
|
||||||
|
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||||
|
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||||
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
|
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const triggerUpdateRecordOptimisticEffect = ({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
record,
|
||||||
|
}: {
|
||||||
|
cache: ApolloCache<unknown>;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
record: CachedObjectRecord;
|
||||||
|
}) => {
|
||||||
|
const objectEdgeTypeName = `${capitalize(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
)}Edge`;
|
||||||
|
|
||||||
|
cache.modify<StoreObject>({
|
||||||
|
fields: {
|
||||||
|
[objectMetadataItem.namePlural]: (
|
||||||
|
cachedConnection,
|
||||||
|
{ INVALIDATE, readField, storeFieldName, toReference },
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!isCachedObjectConnection(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
cachedConnection,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return cachedConnection;
|
||||||
|
|
||||||
|
const { variables } =
|
||||||
|
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||||
|
storeFieldName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cachedEdges = readField<CachedObjectRecordEdge[]>(
|
||||||
|
'edges',
|
||||||
|
cachedConnection,
|
||||||
|
);
|
||||||
|
let nextCachedEdges = cachedEdges ? [...cachedEdges] : [];
|
||||||
|
|
||||||
|
if (variables?.filter) {
|
||||||
|
const matchesFilter = isRecordMatchingFilter({
|
||||||
|
record,
|
||||||
|
filter: variables.filter,
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
const recordIndex = nextCachedEdges.findIndex(
|
||||||
|
(cachedEdge) => readField('id', cachedEdge.node) === record.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchesFilter && recordIndex === -1) {
|
||||||
|
const nodeReference = toReference(record);
|
||||||
|
nodeReference &&
|
||||||
|
nextCachedEdges.push({
|
||||||
|
__typename: objectEdgeTypeName,
|
||||||
|
node: nodeReference,
|
||||||
|
cursor: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchesFilter && recordIndex > -1) {
|
||||||
|
nextCachedEdges.splice(recordIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variables?.orderBy) {
|
||||||
|
nextCachedEdges = sortCachedObjectEdges({
|
||||||
|
edges: nextCachedEdges,
|
||||||
|
orderBy: variables.orderBy,
|
||||||
|
readCacheField: readField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(variables?.first)) {
|
||||||
|
// If previous edges length was exactly at the required limit,
|
||||||
|
// but after update next edges length is under the limit,
|
||||||
|
// we cannot for sure know if re-fetching the query
|
||||||
|
// would return more edges, so we cannot optimistically deduce
|
||||||
|
// the query's result.
|
||||||
|
// In this case, invalidate the cache entry so it can be re-fetched.
|
||||||
|
if (
|
||||||
|
cachedEdges?.length === variables.first &&
|
||||||
|
nextCachedEdges.length < variables.first
|
||||||
|
) {
|
||||||
|
return INVALIDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If next edges length exceeds the required limit,
|
||||||
|
// trim the next edges array to the correct length.
|
||||||
|
if (nextCachedEdges.length > variables.first) {
|
||||||
|
nextCachedEdges.splice(variables.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...cachedConnection, edges: nextCachedEdges };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
|
export type CachedObjectRecord = ObjectRecord & { __typename: string };
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
|
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||||
|
|
||||||
|
export type CachedObjectRecordConnection = Omit<
|
||||||
|
ObjectRecordConnection,
|
||||||
|
'edges'
|
||||||
|
> & {
|
||||||
|
edges: CachedObjectRecordEdge[];
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { Reference } from '@apollo/client';
|
||||||
|
|
||||||
|
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||||
|
|
||||||
|
export type CachedObjectRecordEdge = Omit<ObjectRecordEdge, 'node'> & {
|
||||||
|
node: Reference;
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||||
|
|
||||||
|
export type CachedObjectRecordQueryVariables = Omit<
|
||||||
|
ObjectRecordQueryVariables,
|
||||||
|
'limit'
|
||||||
|
> & { first?: ObjectRecordQueryVariables['limit'] };
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
|
||||||
|
|
||||||
import { useRecordOptimisticEffect } from '@/object-metadata/hooks/useRecordOptimisticEffect';
|
|
||||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
|
||||||
|
|
||||||
const mockRegisterOptimisticEffect = jest.fn();
|
|
||||||
|
|
||||||
jest.mock('@/apollo/optimistic-effect/hooks/useOptimisticEffect', () => ({
|
|
||||||
useOptimisticEffect: jest.fn(() => ({
|
|
||||||
registerOptimisticEffect: mockRegisterOptimisticEffect,
|
|
||||||
unregisterOptimisticEffect: jest.fn(),
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
||||||
<RecoilRoot>
|
|
||||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
|
||||||
</RecoilRoot>
|
|
||||||
);
|
|
||||||
|
|
||||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
|
||||||
|
|
||||||
describe('useRecordOptimisticEffect', () => {
|
|
||||||
it('should work as expected', async () => {
|
|
||||||
const objectMetadataItem = mockObjectMetadataItems.find(
|
|
||||||
(item) => item.namePlural === 'people',
|
|
||||||
)!;
|
|
||||||
|
|
||||||
renderHook(() => useRecordOptimisticEffect({ objectMetadataItem }), {
|
|
||||||
wrapper: Wrapper,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockRegisterOptimisticEffect).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
||||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
|
||||||
import { getRecordOptimisticEffectDefinition } from '@/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition';
|
|
||||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
|
||||||
|
|
||||||
export const useRecordOptimisticEffect = ({
|
|
||||||
objectMetadataItem,
|
|
||||||
filter,
|
|
||||||
orderBy,
|
|
||||||
limit,
|
|
||||||
}: {
|
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
|
||||||
filter?: ObjectRecordQueryFilter;
|
|
||||||
orderBy?: OrderByField;
|
|
||||||
limit?: number;
|
|
||||||
}) => {
|
|
||||||
const { registerOptimisticEffect, unregisterOptimisticEffect } =
|
|
||||||
useOptimisticEffect({
|
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
registerOptimisticEffect({
|
|
||||||
definition: getRecordOptimisticEffectDefinition({
|
|
||||||
objectMetadataItem,
|
|
||||||
}),
|
|
||||||
variables: {
|
|
||||||
filter,
|
|
||||||
orderBy,
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unregisterOptimisticEffect({
|
|
||||||
definition: getRecordOptimisticEffectDefinition({
|
|
||||||
objectMetadataItem,
|
|
||||||
}),
|
|
||||||
variables: {
|
|
||||||
filter,
|
|
||||||
orderBy,
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
registerOptimisticEffect,
|
|
||||||
filter,
|
|
||||||
orderBy,
|
|
||||||
limit,
|
|
||||||
objectMetadataItem,
|
|
||||||
unregisterOptimisticEffect,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
@ -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 { 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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
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 { 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,
|
objectNameSingular,
|
||||||
}: ObjectMetadataItemIdentifier) => {
|
}: ObjectMetadataItemIdentifier) => {
|
||||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { objectMetadataItem, createManyRecordsMutation } =
|
const { objectMetadataItem, createManyRecordsMutation } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { generateEmptyRecord } = useGenerateEmptyRecord({
|
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const createManyRecords = async (data: Partial<T>[]) => {
|
const createManyRecords = async (data: Partial<CreatedObjectRecord>[]) => {
|
||||||
const withIds = data.map((record) => ({
|
const optimisticallyCreatedRecords = data.map((record) =>
|
||||||
...record,
|
generateCachedObjectRecord<CreatedObjectRecord>(record),
|
||||||
id: (record.id as string) ?? v4(),
|
);
|
||||||
}));
|
|
||||||
|
|
||||||
withIds.forEach((record) => {
|
const sanitizedCreateManyRecordsInput = data.map((input, index) =>
|
||||||
const emptyRecord: T | undefined = generateEmptyRecord({
|
sanitizeRecordInput({
|
||||||
id: record.id,
|
objectMetadataItem,
|
||||||
} as T);
|
recordInput: { ...input, id: optimisticallyCreatedRecords[index].id },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (emptyRecord) {
|
const mutationResponseField = getCreateManyRecordsMutationResponseField(
|
||||||
triggerOptimisticEffects({
|
objectMetadataItem.namePlural,
|
||||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
);
|
||||||
createdRecords: [emptyRecord],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdObjects = await apolloClient.mutate({
|
const createdObjects = await apolloClient.mutate({
|
||||||
mutation: createManyRecordsMutation,
|
mutation: createManyRecordsMutation,
|
||||||
variables: {
|
variables: {
|
||||||
data: withIds,
|
data: sanitizedCreateManyRecordsInput,
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
[`create${capitalize(objectMetadataItem.namePlural)}`]: withIds.map(
|
[mutationResponseField]: optimisticallyCreatedRecords,
|
||||||
(record) => generateEmptyRecord({ id: record.id }),
|
},
|
||||||
),
|
update: (cache, { data }) => {
|
||||||
|
const records = data?.[mutationResponseField];
|
||||||
|
|
||||||
|
if (!records?.length) return;
|
||||||
|
|
||||||
|
triggerCreateRecordsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
records,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createdObjects.data) {
|
return createdObjects.data?.[mutationResponseField] ?? [];
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdRecords =
|
|
||||||
createdObjects.data[
|
|
||||||
`create${capitalize(objectMetadataItem.namePlural)}`
|
|
||||||
] ?? [];
|
|
||||||
|
|
||||||
triggerOptimisticEffects({
|
|
||||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
|
||||||
createdRecords,
|
|
||||||
});
|
|
||||||
|
|
||||||
return createdRecords as T[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { createManyRecords };
|
return { createManyRecords };
|
||||||
|
|||||||
@ -1,73 +1,66 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
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 { 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 { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
|
||||||
|
|
||||||
type useCreateOneRecordProps = {
|
type useCreateOneRecordProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateOneRecord = <T>({
|
export const useCreateOneRecord = <
|
||||||
|
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||||
|
>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useCreateOneRecordProps) => {
|
}: useCreateOneRecordProps) => {
|
||||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
|
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
|
||||||
{
|
{ objectNameSingular },
|
||||||
objectNameSingular,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: type this with a minimal type at least with Record<string, any>
|
// TODO: type this with a minimal type at least with Record<string, any>
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const { generateEmptyRecord } = useGenerateEmptyRecord({
|
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createOneRecord = async (input: Record<string, any>) => {
|
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
|
||||||
const recordId = v4();
|
const optimisticallyCreatedRecord =
|
||||||
|
generateCachedObjectRecord<CreatedObjectRecord>(input);
|
||||||
|
|
||||||
const generatedEmptyRecord = generateEmptyRecord({
|
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
|
||||||
id: recordId,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
...input,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
recordInput: input,
|
recordInput: { ...input, id: optimisticallyCreatedRecord.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
triggerOptimisticEffects({
|
const mutationResponseField =
|
||||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
getCreateOneRecordMutationResponseField(objectNameSingular);
|
||||||
createdRecords: [generatedEmptyRecord],
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdObject = await apolloClient.mutate({
|
const createdObject = await apolloClient.mutate({
|
||||||
mutation: createOneRecordMutation,
|
mutation: createOneRecordMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: { id: recordId, ...sanitizedUpdateOneRecordInput },
|
input: sanitizedCreateOneRecordInput,
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
|
[mutationResponseField]: optimisticallyCreatedRecord,
|
||||||
generatedEmptyRecord,
|
},
|
||||||
|
update: (cache, { data }) => {
|
||||||
|
const record = data?.[mutationResponseField];
|
||||||
|
|
||||||
|
if (!record) return;
|
||||||
|
|
||||||
|
triggerCreateRecordsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
records: [record],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createdObject.data) {
|
return createdObject.data?.[mutationResponseField] ?? null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createdObject.data[
|
|
||||||
`create${capitalize(objectMetadataItem.nameSingular)}`
|
|
||||||
] as T;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
|
||||||
|
|
||||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||||
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
||||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
@ -12,39 +11,19 @@ type useDeleteOneRecordProps = {
|
|||||||
refetchFindManyQuery?: boolean;
|
refetchFindManyQuery?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteManyRecords = <T>({
|
export const useDeleteManyRecords = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
refetchFindManyQuery = false,
|
|
||||||
}: useDeleteOneRecordProps) => {
|
}: useDeleteOneRecordProps) => {
|
||||||
const { performOptimisticEvict } = useOptimisticEvict();
|
const { objectMetadataItem, deleteManyRecordsMutation } =
|
||||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
useObjectMetadataItem({ objectNameSingular });
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
objectMetadataItem,
|
|
||||||
deleteManyRecordsMutation,
|
|
||||||
findManyRecordsQuery,
|
|
||||||
} = useObjectMetadataItem({
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
);
|
||||||
|
|
||||||
const deleteManyRecords = async (idsToDelete: string[]) => {
|
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 = {
|
const deleteRecordFilter: ObjectRecordQueryFilter = {
|
||||||
id: {
|
id: {
|
||||||
in: idsToDelete,
|
in: idsToDelete,
|
||||||
@ -56,14 +35,26 @@ export const useDeleteManyRecords = <T>({
|
|||||||
filter: deleteRecordFilter,
|
filter: deleteRecordFilter,
|
||||||
// atMost: idsToDelete.length,
|
// atMost: idsToDelete.length,
|
||||||
},
|
},
|
||||||
refetchQueries: refetchFindManyQuery
|
optimisticResponse: {
|
||||||
? [getOperationName(findManyRecordsQuery) ?? '']
|
[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[
|
return deletedRecords.data?.[mutationResponseField] ?? null;
|
||||||
`delete${capitalize(objectMetadataItem.namePlural)}`
|
|
||||||
] as T;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { deleteManyRecords };
|
return { deleteManyRecords };
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { getOperationName } from '@apollo/client/utilities';
|
|
||||||
|
|
||||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||||
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useDeleteOneRecordProps = {
|
type useDeleteOneRecordProps = {
|
||||||
@ -12,57 +11,50 @@ type useDeleteOneRecordProps = {
|
|||||||
refetchFindManyQuery?: boolean;
|
refetchFindManyQuery?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteOneRecord = <T>({
|
export const useDeleteOneRecord = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
refetchFindManyQuery = false,
|
|
||||||
}: useDeleteOneRecordProps) => {
|
}: useDeleteOneRecordProps) => {
|
||||||
const { performOptimisticEvict } = useOptimisticEvict();
|
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem(
|
||||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
{ objectNameSingular },
|
||||||
objectNameSingular,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const { objectMetadataItem, deleteOneRecordMutation, findManyRecordsQuery } =
|
|
||||||
useObjectMetadataItem({
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
const mutationResponseField =
|
||||||
|
getDeleteOneRecordMutationResponseField(objectNameSingular);
|
||||||
|
|
||||||
const deleteOneRecord = useCallback(
|
const deleteOneRecord = useCallback(
|
||||||
async (idToDelete: string) => {
|
async (idToDelete: string) => {
|
||||||
triggerOptimisticEffects({
|
|
||||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
|
||||||
deletedRecordIds: [idToDelete],
|
|
||||||
});
|
|
||||||
|
|
||||||
performOptimisticEvict(
|
|
||||||
capitalize(objectMetadataItem.nameSingular),
|
|
||||||
'id',
|
|
||||||
idToDelete,
|
|
||||||
);
|
|
||||||
|
|
||||||
const deletedRecord = await apolloClient.mutate({
|
const deletedRecord = await apolloClient.mutate({
|
||||||
mutation: deleteOneRecordMutation,
|
mutation: deleteOneRecordMutation,
|
||||||
variables: {
|
variables: { idToDelete },
|
||||||
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[
|
return deletedRecord.data?.[mutationResponseField] ?? null;
|
||||||
`delete${capitalize(objectMetadataItem.nameSingular)}`
|
|
||||||
] as T;
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
triggerOptimisticEffects,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
performOptimisticEvict,
|
|
||||||
apolloClient,
|
apolloClient,
|
||||||
deleteOneRecordMutation,
|
deleteOneRecordMutation,
|
||||||
refetchFindManyQuery,
|
mutationResponseField,
|
||||||
findManyRecordsQuery,
|
objectMetadataItem,
|
||||||
|
objectNameSingular,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -6,14 +6,12 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useRecordOptimisticEffect } from '@/object-metadata/hooks/useRecordOptimisticEffect';
|
|
||||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
|
||||||
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
|
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
|
||||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||||
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||||
|
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||||
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
|
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
|
||||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
|
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
@ -34,14 +32,12 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
onCompleted,
|
onCompleted,
|
||||||
skip,
|
skip,
|
||||||
useRecordsWithoutConnection = false,
|
useRecordsWithoutConnection = false,
|
||||||
}: ObjectMetadataItemIdentifier & {
|
}: ObjectMetadataItemIdentifier &
|
||||||
filter?: ObjectRecordQueryFilter;
|
ObjectRecordQueryVariables & {
|
||||||
orderBy?: OrderByField;
|
onCompleted?: (data: ObjectRecordConnection<T>) => void;
|
||||||
limit?: number;
|
skip?: boolean;
|
||||||
onCompleted?: (data: ObjectRecordConnection<T>) => void;
|
useRecordsWithoutConnection?: boolean;
|
||||||
skip?: boolean;
|
}) => {
|
||||||
useRecordsWithoutConnection?: boolean;
|
|
||||||
}) => {
|
|
||||||
const findManyQueryStateIdentifier =
|
const findManyQueryStateIdentifier =
|
||||||
objectNameSingular +
|
objectNameSingular +
|
||||||
JSON.stringify(filter) +
|
JSON.stringify(filter) +
|
||||||
@ -64,13 +60,6 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
useRecordOptimisticEffect({
|
|
||||||
objectMetadataItem,
|
|
||||||
filter,
|
|
||||||
orderBy,
|
|
||||||
limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const getCreateManyRecordsMutationResponseField = (
|
||||||
|
objectNamePlural: string,
|
||||||
|
) => `create${capitalize(objectNamePlural)}`;
|
||||||
|
|
||||||
export const useGenerateCreateManyRecordMutation = ({
|
export const useGenerateCreateManyRecordMutation = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
@ -16,11 +20,15 @@ export const useGenerateCreateManyRecordMutation = ({
|
|||||||
return EMPTY_MUTATION;
|
return EMPTY_MUTATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mutationResponseField = getCreateManyRecordsMutationResponseField(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
);
|
||||||
|
|
||||||
return gql`
|
return gql`
|
||||||
mutation Create${capitalize(
|
mutation Create${capitalize(
|
||||||
objectMetadataItem.namePlural,
|
objectMetadataItem.namePlural,
|
||||||
)}($data: [${capitalize(objectMetadataItem.nameSingular)}CreateInput!]!) {
|
)}($data: [${capitalize(objectMetadataItem.nameSingular)}CreateInput!]!) {
|
||||||
create${capitalize(objectMetadataItem.namePlural)}(data: $data) {
|
${mutationResponseField}(data: $data) {
|
||||||
id
|
id
|
||||||
${objectMetadataItem.fields
|
${objectMetadataItem.fields
|
||||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const getCreateOneRecordMutationResponseField = (
|
||||||
|
objectNameSingular: string,
|
||||||
|
) => `create${capitalize(objectNameSingular)}`;
|
||||||
|
|
||||||
export const useGenerateCreateOneRecordMutation = ({
|
export const useGenerateCreateOneRecordMutation = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
@ -18,9 +22,13 @@ export const useGenerateCreateOneRecordMutation = ({
|
|||||||
|
|
||||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||||
|
|
||||||
|
const mutationResponseField = getCreateOneRecordMutationResponseField(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
return gql`
|
return gql`
|
||||||
mutation CreateOne${capitalizedObjectName}($input: ${capitalizedObjectName}CreateInput!) {
|
mutation CreateOne${capitalizedObjectName}($input: ${capitalizedObjectName}CreateInput!) {
|
||||||
create${capitalizedObjectName}(data: $input) {
|
${mutationResponseField}(data: $input) {
|
||||||
id
|
id
|
||||||
${objectMetadataItem.fields
|
${objectMetadataItem.fields
|
||||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||||
|
|||||||
@ -4,6 +4,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const getDeleteManyRecordsMutationResponseField = (
|
||||||
|
objectNamePlural: string,
|
||||||
|
) => `delete${capitalize(objectNamePlural)}`;
|
||||||
|
|
||||||
export const useGenerateDeleteManyRecordMutation = ({
|
export const useGenerateDeleteManyRecordMutation = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
@ -15,11 +19,15 @@ export const useGenerateDeleteManyRecordMutation = ({
|
|||||||
|
|
||||||
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
|
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
|
||||||
|
|
||||||
|
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
);
|
||||||
|
|
||||||
return gql`
|
return gql`
|
||||||
mutation DeleteMany${capitalizedObjectName}($filter: ${capitalize(
|
mutation DeleteMany${capitalizedObjectName}($filter: ${capitalize(
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
)}FilterInput!) {
|
)}FilterInput!) {
|
||||||
delete${capitalizedObjectName}(filter: $filter) {
|
${mutationResponseField}(filter: $filter) {
|
||||||
id
|
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
export const getUpdateOneRecordMutationGraphQLField = ({
|
export const getUpdateOneRecordMutationResponseField = (
|
||||||
objectNameSingular,
|
objectNameSingular: string,
|
||||||
}: {
|
) => `update${capitalize(objectNameSingular)}`;
|
||||||
objectNameSingular: string;
|
|
||||||
}) => {
|
|
||||||
return `update${capitalize(objectNameSingular)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGenerateUpdateOneRecordMutation = ({
|
export const useGenerateUpdateOneRecordMutation = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
@ -26,14 +22,13 @@ export const useGenerateUpdateOneRecordMutation = ({
|
|||||||
|
|
||||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||||
|
|
||||||
const graphQLFieldForUpdateOneRecordMutation =
|
const mutationResponseField = getUpdateOneRecordMutationResponseField(
|
||||||
getUpdateOneRecordMutationGraphQLField({
|
objectMetadataItem.nameSingular,
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return gql`
|
return gql`
|
||||||
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
|
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
|
||||||
${graphQLFieldForUpdateOneRecordMutation}(id: $idToUpdate, data: $input) {
|
${mutationResponseField}(id: $idToUpdate, data: $input) {
|
||||||
id
|
id
|
||||||
${objectMetadataItem.fields
|
${objectMetadataItem.fields
|
||||||
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { gql, useApolloClient } from '@apollo/client';
|
import { gql, useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
||||||
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
export const useGetRecordFromCache = ({
|
export const useGetRecordFromCache = ({
|
||||||
@ -13,9 +13,11 @@ export const useGetRecordFromCache = ({
|
|||||||
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
return (recordId: string) => {
|
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
|
||||||
|
recordId: string,
|
||||||
|
) => {
|
||||||
if (!objectMetadataItem) {
|
if (!objectMetadataItem) {
|
||||||
return EMPTY_MUTATION;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||||
@ -35,7 +37,7 @@ export const useGetRecordFromCache = ({
|
|||||||
id: recordId,
|
id: recordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return cache.readFragment({
|
return cache.readFragment<CachedObjectRecord>({
|
||||||
id: cachedRecordId,
|
id: cachedRecordId,
|
||||||
fragment: cacheReadFragment,
|
fragment: cacheReadFragment,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
export const useModifyRecordFromCache = ({
|
export const useModifyRecordFromCache = ({
|
||||||
@ -10,23 +10,20 @@ export const useModifyRecordFromCache = ({
|
|||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
}) => {
|
}) => {
|
||||||
const apolloClient = useApolloClient();
|
const { cache } = useApolloClient();
|
||||||
|
|
||||||
return (
|
return <CachedObjectRecord extends ObjectRecord>(
|
||||||
recordId: string,
|
recordId: string,
|
||||||
fieldModifiers: Record<string, Modifier<Reference>>,
|
fieldModifiers: Modifiers<CachedObjectRecord>,
|
||||||
) => {
|
) => {
|
||||||
if (!objectMetadataItem) {
|
if (!objectMetadataItem) return;
|
||||||
return EMPTY_MUTATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache = apolloClient.cache;
|
|
||||||
const cachedRecordId = cache.identify({
|
const cachedRecordId = cache.identify({
|
||||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||||
id: recordId,
|
id: recordId,
|
||||||
});
|
});
|
||||||
|
|
||||||
cache.modify<Record<string, Reference>>({
|
cache.modify<CachedObjectRecord>({
|
||||||
id: cachedRecordId,
|
id: cachedRecordId,
|
||||||
fields: fieldModifiers,
|
fields: fieldModifiers,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export const useObjectRecordBoard = () => {
|
|||||||
|
|
||||||
useFindManyRecords({
|
useFindManyRecords({
|
||||||
objectNameSingular: CoreObjectNameSingular.PipelineStep,
|
objectNameSingular: CoreObjectNameSingular.PipelineStep,
|
||||||
filter: {},
|
filter,
|
||||||
onCompleted: useCallback(
|
onCompleted: useCallback(
|
||||||
(data: ObjectRecordConnection<PipelineStep>) => {
|
(data: ObjectRecordConnection<PipelineStep>) => {
|
||||||
setSavedPipelineSteps(data.edges.map((edge) => edge.node));
|
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 { 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 { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
|
||||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useUpdateOneRecordProps = {
|
type useUpdateOneRecordProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateOneRecord = <T>({
|
export const useUpdateOneRecord = <
|
||||||
|
UpdatedObjectRecord extends ObjectRecord = ObjectRecord,
|
||||||
|
>({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useUpdateOneRecordProps) => {
|
}: useUpdateOneRecordProps) => {
|
||||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({ objectNameSingular });
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
|
||||||
objectNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
@ -30,13 +26,15 @@ export const useUpdateOneRecord = <T>({
|
|||||||
updateOneRecordInput,
|
updateOneRecordInput,
|
||||||
}: {
|
}: {
|
||||||
idToUpdate: string;
|
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 ?? {}),
|
...(cachedRecord ?? {}),
|
||||||
...updateOneRecordInput,
|
...updateOneRecordInput,
|
||||||
|
__typename: capitalize(objectNameSingular),
|
||||||
|
id: idToUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
|
||||||
@ -44,82 +42,32 @@ export const useUpdateOneRecord = <T>({
|
|||||||
recordInput: updateOneRecordInput,
|
recordInput: updateOneRecordInput,
|
||||||
});
|
});
|
||||||
|
|
||||||
triggerOptimisticEffects({
|
const mutationResponseField =
|
||||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
getUpdateOneRecordMutationResponseField(objectNameSingular);
|
||||||
updatedRecords: [optimisticallyUpdatedRecord],
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedRecord = await apolloClient.mutate({
|
const updatedRecord = await apolloClient.mutate({
|
||||||
mutation: updateOneRecordMutation,
|
mutation: updateOneRecordMutation,
|
||||||
variables: {
|
variables: {
|
||||||
idToUpdate,
|
idToUpdate,
|
||||||
input: {
|
input: sanitizedUpdateOneRecordInput,
|
||||||
...sanitizedUpdateOneRecordInput,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
[`update${capitalize(objectMetadataItem.nameSingular)}`]:
|
[mutationResponseField]: optimisticallyUpdatedRecord,
|
||||||
optimisticallyUpdatedRecord,
|
|
||||||
},
|
},
|
||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const response =
|
const record = data?.[mutationResponseField];
|
||||||
data?.[`update${capitalize(objectMetadataItem.nameSingular)}`];
|
|
||||||
|
|
||||||
if (!response) return;
|
if (!record) return;
|
||||||
|
|
||||||
cache.modify<Record<string, Reference>>({
|
triggerUpdateRecordOptimisticEffect({
|
||||||
fields: {
|
cache,
|
||||||
[objectMetadataItem.namePlural]: (
|
objectMetadataItem,
|
||||||
existingConnectionRef,
|
record,
|
||||||
{ 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;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!updatedRecord?.data) {
|
return updatedRecord?.data?.[mutationResponseField] ?? null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedData = updatedRecord.data[
|
|
||||||
`update${capitalize(objectMetadataItem.nameSingular)}`
|
|
||||||
] as T;
|
|
||||||
|
|
||||||
return updatedData;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||||
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
|
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
|
||||||
import { Opportunity } from '@/pipeline/types/Opportunity';
|
|
||||||
|
|
||||||
import { useRemoveRecordBoardCardIdsInternal } from './useRemoveRecordBoardCardIdsInternal';
|
import { useRemoveRecordBoardCardIdsInternal } from './useRemoveRecordBoardCardIdsInternal';
|
||||||
|
|
||||||
@ -12,10 +11,9 @@ export const useDeleteSelectedRecordBoardCardsInternal = () => {
|
|||||||
const removeCardIds = useRemoveRecordBoardCardIdsInternal();
|
const removeCardIds = useRemoveRecordBoardCardIdsInternal();
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const { deleteManyRecords: deleteManyOpportunities } =
|
const { deleteManyRecords: deleteManyOpportunities } = useDeleteManyRecords({
|
||||||
useDeleteManyRecords<Opportunity>({
|
objectNameSingular: CoreObjectNameSingular.Opportunity,
|
||||||
objectNameSingular: CoreObjectNameSingular.Opportunity,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const { selectedCardIdsSelector } = useRecordBoardScopedStates();
|
const { selectedCardIdsSelector } = useRecordBoardScopedStates();
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
URLFilter,
|
URLFilter,
|
||||||
UUIDFilter,
|
UUIDFilter,
|
||||||
} from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
} from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||||
|
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
|
||||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
import { Field } from '~/generated/graphql';
|
import { Field } from '~/generated/graphql';
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
|||||||
export const turnObjectDropdownFilterIntoQueryFilter = (
|
export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||||
rawUIFilters: ObjectDropdownFilter[],
|
rawUIFilters: ObjectDropdownFilter[],
|
||||||
fields: Pick<Field, 'id' | 'name'>[],
|
fields: Pick<Field, 'id' | 'name'>[],
|
||||||
): ObjectRecordQueryFilter => {
|
): ObjectRecordQueryFilter | undefined => {
|
||||||
const objectRecordFilters: ObjectRecordQueryFilter[] = [];
|
const objectRecordFilters: ObjectRecordQueryFilter[] = [];
|
||||||
|
|
||||||
for (const rawUIFilter of rawUIFilters) {
|
for (const rawUIFilter of rawUIFilters) {
|
||||||
@ -134,13 +135,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case ViewFilterOperand.IsNot:
|
case ViewFilterOperand.IsNot:
|
||||||
objectRecordFilters.push({
|
if (parsedRecordIds.length) {
|
||||||
not: {
|
objectRecordFilters.push({
|
||||||
[correspondingField.name + 'Id']: {
|
not: {
|
||||||
in: parsedRecordIds,
|
[correspondingField.name + 'Id']: {
|
||||||
} as UUIDFilter,
|
in: parsedRecordIds,
|
||||||
},
|
} as UUIDFilter,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
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 { useContext, useEffect } from 'react';
|
||||||
import { Reference } from '@apollo/client';
|
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { LightIconButton, MenuItem } from 'tsup.ui.index';
|
import { LightIconButton, MenuItem } from 'tsup.ui.index';
|
||||||
|
|
||||||
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { FieldDisplay } from '@/object-record/field/components/FieldDisplay';
|
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 { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
|
||||||
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
|
||||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||||
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
|
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { IconDotsVertical, IconUnlink } from '@/ui/display/icon';
|
import { IconDotsVertical, IconUnlink } from '@/ui/display/icon';
|
||||||
@ -71,14 +70,10 @@ export const RecordRelationFieldCardContent = ({
|
|||||||
objectMetadataNameSingular,
|
objectMetadataNameSingular,
|
||||||
} = fieldDefinition.metadata as FieldRelationMetadata;
|
} = fieldDefinition.metadata as FieldRelationMetadata;
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { modifyRecordFromCache } = useObjectMetadataItem({
|
||||||
objectNameSingular: objectMetadataNameSingular ?? '',
|
objectNameSingular: objectMetadataNameSingular ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const modifyRecordFromCache = useModifyRecordFromCache({
|
|
||||||
objectMetadataItem,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||||
const {
|
const {
|
||||||
labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata,
|
labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata,
|
||||||
@ -104,13 +99,13 @@ export const RecordRelationFieldCardContent = ({
|
|||||||
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownScopeId);
|
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownScopeId);
|
||||||
|
|
||||||
// TODO: temporary as ChipDisplay expect to find the entity in the entityFieldsFamilyState
|
// TODO: temporary as ChipDisplay expect to find the entity in the entityFieldsFamilyState
|
||||||
const setEntityFields = useSetRecoilState(
|
const setRelationEntityFields = useSetRecoilState(
|
||||||
entityFieldsFamilyState(relationRecord.id),
|
entityFieldsFamilyState(relationRecord.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEntityFields(relationRecord);
|
setRelationEntityFields(relationRecord);
|
||||||
}, [relationRecord, setEntityFields]);
|
}, [relationRecord, setRelationEntityFields]);
|
||||||
|
|
||||||
if (!FieldContextProvider) return null;
|
if (!FieldContextProvider) return null;
|
||||||
|
|
||||||
@ -137,15 +132,18 @@ export const RecordRelationFieldCardContent = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
modifyRecordFromCache(entityId, {
|
modifyRecordFromCache(entityId, {
|
||||||
[fieldName]: (relationRef, { readField }) => {
|
[fieldName]: (cachedRelationConnection, { readField }) => {
|
||||||
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
const edges = readField<CachedObjectRecordEdge[]>(
|
||||||
|
'edges',
|
||||||
|
cachedRelationConnection,
|
||||||
|
);
|
||||||
|
|
||||||
if (!edges) {
|
if (!edges) {
|
||||||
return relationRef;
|
return cachedRelationConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...relationRef,
|
...cachedRelationConnection,
|
||||||
edges: edges.filter(({ node }) => {
|
edges: edges.filter(({ node }) => {
|
||||||
const id = readField('id', node);
|
const id = readField('id', node);
|
||||||
return id !== relationRecord.id;
|
return id !== relationRecord.id;
|
||||||
|
|||||||
@ -139,7 +139,6 @@ export const RecordRelationFieldCardSection = () => {
|
|||||||
],
|
],
|
||||||
orderByField: 'createdAt',
|
orderByField: 'createdAt',
|
||||||
selectedIds: relationRecordIds,
|
selectedIds: relationRecordIds,
|
||||||
excludeEntityIds: relationRecordIds,
|
|
||||||
objectNameSingular: relationObjectMetadataNameSingular,
|
objectNameSingular: relationObjectMetadataNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -42,19 +42,6 @@ const response = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mocks = [
|
const mocks = [
|
||||||
{
|
|
||||||
request: {
|
|
||||||
query,
|
|
||||||
variables: {
|
|
||||||
filterNameSingular: { and: [{}, { id: { in: ['1'] } }] },
|
|
||||||
orderByNameSingular: { createdAt: 'DescNullsLast' },
|
|
||||||
limitNameSingular: 60,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
result: jest.fn(() => ({
|
|
||||||
data: response,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
query,
|
query,
|
||||||
@ -72,8 +59,20 @@ const mocks = [
|
|||||||
request: {
|
request: {
|
||||||
query,
|
query,
|
||||||
variables: {
|
variables: {
|
||||||
|
orderByNameSingular: { createdAt: 'DescNullsLast' },
|
||||||
limitNameSingular: 60,
|
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' },
|
orderByNameSingular: { createdAt: 'DescNullsLast' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -53,23 +53,9 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({
|
|||||||
|
|
||||||
if (!isNonEmptyArray(selectedIds)) return null;
|
if (!isNonEmptyArray(selectedIds)) return null;
|
||||||
|
|
||||||
const searchFilter =
|
|
||||||
searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`filter${capitalize(nameSingular)}`,
|
`filter${capitalize(nameSingular)}`,
|
||||||
{
|
searchFilterPerMetadataItemNameSingular[nameSingular],
|
||||||
and: [
|
|
||||||
{
|
|
||||||
...searchFilter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
in: selectedIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
.filter(isDefined),
|
.filter(isDefined),
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
@ -14,7 +13,7 @@ import {
|
|||||||
import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||||
import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
|
import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
|
||||||
import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem';
|
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 { isDefined } from '~/utils/isDefined';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
@ -58,35 +57,19 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
|
|||||||
)
|
)
|
||||||
.map(({ id }) => id);
|
.map(({ id }) => id);
|
||||||
|
|
||||||
const searchFilter =
|
|
||||||
searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
|
|
||||||
|
|
||||||
const excludedIdsUnion = [...selectedIds, ...excludedIds];
|
const excludedIdsUnion = [...selectedIds, ...excludedIds];
|
||||||
|
const excludedIdsFilter = excludedIdsUnion.length
|
||||||
|
? { not: { id: { in: excludedIdsUnion } } }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const noFilter =
|
const searchFilters = [
|
||||||
!isNonEmptyArray(excludedIdsUnion) &&
|
searchFilterPerMetadataItemNameSingular[nameSingular],
|
||||||
isDeeplyEqual(searchFilter, {});
|
excludedIdsFilter,
|
||||||
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`filter${capitalize(nameSingular)}`,
|
`filter${capitalize(nameSingular)}`,
|
||||||
!noFilter
|
andFilterVariables(searchFilters),
|
||||||
? {
|
|
||||||
and: [
|
|
||||||
{
|
|
||||||
...searchFilter,
|
|
||||||
},
|
|
||||||
isNonEmptyArray(excludedIdsUnion)
|
|
||||||
? {
|
|
||||||
not: {
|
|
||||||
id: {
|
|
||||||
in: [...selectedIds, ...excludedIds],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
.filter(isDefined),
|
.filter(isDefined),
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
|
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
|
||||||
import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields';
|
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;
|
export const DEFAULT_SEARCH_REQUEST_LIMIT = 60;
|
||||||
|
|
||||||
@ -37,85 +38,62 @@ export const useRecordsForSelect = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const orderByField = getObjectOrderByField(sortOrder);
|
const orderByField = getObjectOrderByField(sortOrder);
|
||||||
|
const selectedIdsFilter = { id: { in: selectedIds } };
|
||||||
|
|
||||||
const { loading: selectedRecordsLoading, records: selectedRecordsData } =
|
const { loading: selectedRecordsLoading, records: selectedRecordsData } =
|
||||||
useFindManyRecords({
|
useFindManyRecords({
|
||||||
filter: {
|
filter: selectedIdsFilter,
|
||||||
id: {
|
|
||||||
in: selectedIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: orderByField,
|
orderBy: orderByField,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
skip: !selectedIds.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchFilter = filters
|
const searchFilters = filters.map(({ fieldNames, filter }) => {
|
||||||
.map(({ fieldNames, filter }) => {
|
if (!isNonEmptyString(filter)) {
|
||||||
if (!isNonEmptyString(filter)) {
|
return undefined;
|
||||||
return undefined;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return orFilterVariables(
|
||||||
or: fieldNames.map((fieldName) => {
|
fieldNames.map((fieldName) => {
|
||||||
const fieldNameParts = fieldName.split('.');
|
const [parentFieldName, subFieldName] = fieldName.split('.');
|
||||||
|
|
||||||
if (fieldNameParts.length > 1) {
|
if (subFieldName) {
|
||||||
// Composite field
|
// Composite field
|
||||||
|
|
||||||
return {
|
|
||||||
[fieldNameParts[0]]: {
|
|
||||||
[fieldNameParts[1]]: {
|
|
||||||
ilike: `%${filter}%`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
[fieldName]: {
|
[parentFieldName]: {
|
||||||
ilike: `%${filter}%`,
|
[subFieldName]: {
|
||||||
|
ilike: `%${filter}%`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}
|
||||||
};
|
|
||||||
})
|
return {
|
||||||
.filter(isDefined);
|
[fieldName]: {
|
||||||
|
ilike: `%${filter}%`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: filteredSelectedRecordsLoading,
|
loading: filteredSelectedRecordsLoading,
|
||||||
records: filteredSelectedRecordsData,
|
records: filteredSelectedRecordsData,
|
||||||
} = useFindManyRecords({
|
} = useFindManyRecords({
|
||||||
filter: {
|
filter: andFilterVariables([...searchFilters, selectedIdsFilter]),
|
||||||
and: [
|
|
||||||
{
|
|
||||||
and: searchFilter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
in: selectedIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: orderByField,
|
orderBy: orderByField,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
skip: !selectedIds.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const notFilterIds = [...selectedIds, ...excludeEntityIds];
|
||||||
|
const notFilter = notFilterIds.length
|
||||||
|
? { not: { id: { in: notFilterIds } } }
|
||||||
|
: undefined;
|
||||||
const { loading: recordsToSelectLoading, records: recordsToSelectData } =
|
const { loading: recordsToSelectLoading, records: recordsToSelectData } =
|
||||||
useFindManyRecords({
|
useFindManyRecords({
|
||||||
filter: {
|
filter: andFilterVariables([...searchFilters, notFilter]),
|
||||||
and: [
|
|
||||||
{
|
|
||||||
and: searchFilter,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
not: {
|
|
||||||
id: {
|
|
||||||
in: [...selectedIds, ...excludeEntityIds],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||||
orderBy: orderByField,
|
orderBy: orderByField,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
export type ObjectRecordEdge<T extends ObjectRecord> = {
|
export type ObjectRecordEdge<T extends ObjectRecord = ObjectRecord> = {
|
||||||
__typename?: string;
|
__typename?: string;
|
||||||
node: T;
|
node: T;
|
||||||
cursor: string;
|
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const getDeleteOneRecordMutationResponseField = (
|
||||||
|
objectNameSingular: string,
|
||||||
|
) => `delete${capitalize(objectNameSingular)}`;
|
||||||
|
|
||||||
export const generateDeleteOneRecordMutation = ({
|
export const generateDeleteOneRecordMutation = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
}: {
|
}: {
|
||||||
@ -15,9 +19,13 @@ export const generateDeleteOneRecordMutation = ({
|
|||||||
|
|
||||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||||
|
|
||||||
|
const mutationResponseField = getDeleteOneRecordMutationResponseField(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
return gql`
|
return gql`
|
||||||
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
|
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
|
||||||
delete${capitalizedObjectName}(id: $idToDelete) {
|
${mutationResponseField}(id: $idToDelete) {
|
||||||
id
|
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 };
|
||||||
|
};
|
||||||
@ -37,17 +37,10 @@ const data = {
|
|||||||
id: 'columnId',
|
id: 'columnId',
|
||||||
position: 1,
|
position: 1,
|
||||||
name: 'Column Title',
|
name: 'Column Title',
|
||||||
pipeline: { connect: { id: currentPipelineId } },
|
|
||||||
type: 'ongoing',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const variables = {
|
export const variables = {
|
||||||
input: {
|
input: data,
|
||||||
id: mockId,
|
|
||||||
variables: {
|
|
||||||
data,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteVariables = { idToDelete: 'columnId' };
|
export const deleteVariables = { idToDelete: 'columnId' };
|
||||||
|
|||||||
@ -13,10 +13,9 @@ export const usePipelineSteps = () => {
|
|||||||
objectNameSingular: CoreObjectNameSingular.PipelineStep,
|
objectNameSingular: CoreObjectNameSingular.PipelineStep,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { deleteOneRecord: deleteOnePipelineStep } =
|
const { deleteOneRecord: deleteOnePipelineStep } = useDeleteOneRecord({
|
||||||
useDeleteOneRecord<PipelineStep>({
|
objectNameSingular: CoreObjectNameSingular.PipelineStep,
|
||||||
objectNameSingular: CoreObjectNameSingular.PipelineStep,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const handlePipelineStepAdd = useRecoilCallback(
|
const handlePipelineStepAdd = useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
@ -25,16 +24,10 @@ export const usePipelineSteps = () => {
|
|||||||
if (!currentPipeline?.id) return;
|
if (!currentPipeline?.id) return;
|
||||||
|
|
||||||
return createOnePipelineStep?.({
|
return createOnePipelineStep?.({
|
||||||
variables: {
|
color: boardColumn.colorCode ?? 'gray',
|
||||||
data: {
|
id: boardColumn.id,
|
||||||
color: boardColumn.colorCode ?? 'gray',
|
position: boardColumn.position,
|
||||||
id: boardColumn.id,
|
name: boardColumn.title,
|
||||||
position: boardColumn.position,
|
|
||||||
name: boardColumn.title,
|
|
||||||
pipeline: { connect: { id: currentPipeline.id } },
|
|
||||||
type: 'ongoing',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[createOnePipelineStep],
|
[createOnePipelineStep],
|
||||||
|
|||||||
@ -6,8 +6,9 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|||||||
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
|
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
|
||||||
|
import { orFilterVariables } from '@/object-record/utils/orFilterVariables';
|
||||||
import { assertNotNull } from '~/utils/assert';
|
import { assertNotNull } from '~/utils/assert';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
type SearchFilter = { fieldNames: string[]; filter: string | number };
|
type SearchFilter = { fieldNames: string[]; filter: string | number };
|
||||||
|
|
||||||
@ -40,63 +41,63 @@ export const useFilteredSearchEntityQuery = ({
|
|||||||
...mapToObjectRecordIdentifier(record),
|
...mapToObjectRecordIdentifier(record),
|
||||||
record,
|
record,
|
||||||
});
|
});
|
||||||
|
const selectedIdsFilter = { id: { in: selectedIds } };
|
||||||
|
|
||||||
const { loading: selectedRecordsLoading, records: selectedRecords } =
|
const { loading: selectedRecordsLoading, records: selectedRecords } =
|
||||||
useFindManyRecords({
|
useFindManyRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: { id: { in: selectedIds } },
|
filter: selectedIdsFilter,
|
||||||
orderBy: { [orderByField]: sortOrder },
|
orderBy: { [orderByField]: sortOrder },
|
||||||
|
skip: !selectedIds.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchFilter = filters
|
const searchFilters = filters.map(({ fieldNames, filter }) => {
|
||||||
.map(({ fieldNames, filter }) => {
|
if (!isNonEmptyString(filter)) {
|
||||||
if (!isNonEmptyString(filter)) {
|
return undefined;
|
||||||
return undefined;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return orFilterVariables(
|
||||||
or: fieldNames.map((fieldName) => {
|
fieldNames.map((fieldName) => {
|
||||||
const fieldNameParts = fieldName.split('.');
|
const [parentFieldName, subFieldName] = fieldName.split('.');
|
||||||
|
|
||||||
if (fieldNameParts.length > 1) {
|
if (subFieldName) {
|
||||||
// Composite field
|
// Composite field
|
||||||
|
|
||||||
return {
|
|
||||||
[fieldNameParts[0]]: {
|
|
||||||
[fieldNameParts[1]]: {
|
|
||||||
ilike: `%${filter}%`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
[fieldName]: {
|
[parentFieldName]: {
|
||||||
ilike: `%${filter}%`,
|
[subFieldName]: {
|
||||||
|
ilike: `%${filter}%`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}
|
||||||
};
|
|
||||||
})
|
return {
|
||||||
.filter(isDefined);
|
[fieldName]: {
|
||||||
|
ilike: `%${filter}%`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: filteredSelectedRecordsLoading,
|
loading: filteredSelectedRecordsLoading,
|
||||||
records: filteredSelectedRecords,
|
records: filteredSelectedRecords,
|
||||||
} = useFindManyRecords({
|
} = useFindManyRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: { and: [{ and: searchFilter }, { id: { in: selectedIds } }] },
|
filter: andFilterVariables([...searchFilters, selectedIdsFilter]),
|
||||||
orderBy: { [orderByField]: sortOrder },
|
orderBy: { [orderByField]: sortOrder },
|
||||||
|
skip: !selectedIds.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const notFilterIds = [...selectedIds, ...excludeEntityIds];
|
||||||
|
const notFilter = notFilterIds.length
|
||||||
|
? { not: { id: { in: notFilterIds } } }
|
||||||
|
: undefined;
|
||||||
const { loading: recordsToSelectLoading, records: recordsToSelect } =
|
const { loading: recordsToSelectLoading, records: recordsToSelect } =
|
||||||
useFindManyRecords({
|
useFindManyRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: {
|
filter: andFilterVariables([...searchFilters, notFilter]),
|
||||||
and: [
|
|
||||||
{ and: searchFilter },
|
|
||||||
{ not: { id: { in: [...selectedIds, ...excludeEntityIds] } } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||||
orderBy: { [orderByField]: sortOrder },
|
orderBy: { [orderByField]: sortOrder },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,4 +5,5 @@ export type ApiKey = {
|
|||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
|
revokedAt: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -39,10 +39,9 @@ export const SettingsWorkspaceMembers = () => {
|
|||||||
const { records: workspaceMembers } = useFindManyRecords<WorkspaceMember>({
|
const { records: workspaceMembers } = useFindManyRecords<WorkspaceMember>({
|
||||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||||
});
|
});
|
||||||
const { deleteOneRecord: deleteOneWorkspaceMember } =
|
const { deleteOneRecord: deleteOneWorkspaceMember } = useDeleteOneRecord({
|
||||||
useDeleteOneRecord<WorkspaceMember>({
|
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
});
|
||||||
});
|
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { Reference } from '@apollo/client';
|
import { Reference } from '@apollo/client';
|
||||||
|
|
||||||
|
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||||
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
@ -227,16 +228,16 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
modifyViewFromCache(view.id, {
|
modifyViewFromCache(view.id, {
|
||||||
viewFields: (viewFieldsRef, { readField }) => {
|
viewFields: (cachedViewFieldsConnection, { readField }) => {
|
||||||
const edges = readField<{ node: Reference }[]>(
|
const edges = readField<CachedObjectRecordEdge[]>(
|
||||||
'edges',
|
'edges',
|
||||||
viewFieldsRef,
|
cachedViewFieldsConnection,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!edges) return viewFieldsRef;
|
if (!edges) return cachedViewFieldsConnection;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...viewFieldsRef,
|
...cachedViewFieldsConnection,
|
||||||
edges: [...edges, { node: viewFieldToCreate }],
|
edges: [...edges, { node: viewFieldToCreate }],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
|||||||
) => {
|
) => {
|
||||||
const newApiKey = await createOneApiKey?.({
|
const newApiKey = await createOneApiKey?.({
|
||||||
name: name,
|
name: name,
|
||||||
expiresAt: newExpiresAt,
|
expiresAt: newExpiresAt ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!newApiKey) {
|
if (!newApiKey) {
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
// There is a feature request for receiving variables in `cache.modify`:
|
// There is a feature request for receiving variables in `cache.modify`:
|
||||||
// @see https://github.com/apollographql/apollo-feature-requests/issues/259
|
// @see https://github.com/apollographql/apollo-feature-requests/issues/259
|
||||||
// @see https://github.com/apollographql/apollo-client/issues/7129
|
// @see https://github.com/apollographql/apollo-client/issues/7129
|
||||||
|
|
||||||
// For now we need to parse `storeFieldName` to retrieve the variables.
|
// For now we need to parse `storeFieldName` to retrieve the variables.
|
||||||
export const parseApolloStoreFieldName = (storeFieldName: string) => {
|
export const parseApolloStoreFieldName = <
|
||||||
|
Variables extends Record<string, unknown>,
|
||||||
|
>(
|
||||||
|
storeFieldName: string,
|
||||||
|
) => {
|
||||||
const matches = storeFieldName.match(/([a-zA-Z][a-zA-Z0-9 ]*)\((.*)\)/);
|
const matches = storeFieldName.match(/([a-zA-Z][a-zA-Z0-9 ]*)\((.*)\)/);
|
||||||
|
|
||||||
if (!matches?.[1]) return {};
|
if (!matches?.[1]) return {};
|
||||||
|
|
||||||
const [, fieldName, stringifiedVariables] = matches;
|
const [, , stringifiedVariables] = matches;
|
||||||
|
const fieldName = matches[1] as string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const variables = stringifiedVariables
|
const variables = stringifiedVariables
|
||||||
? (JSON.parse(stringifiedVariables) as Record<string, unknown>)
|
? (JSON.parse(stringifiedVariables) as Variables)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return { fieldName, variables };
|
return { fieldName, variables };
|
||||||
|
|||||||
21
packages/twenty-front/src/utils/sort.ts
Normal file
21
packages/twenty-front/src/utils/sort.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Maybe } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const sortNullsFirst = (
|
||||||
|
fieldValueA: Maybe<unknown>,
|
||||||
|
fieldValueB: Maybe<unknown>,
|
||||||
|
) => (fieldValueA === null ? -1 : fieldValueB === null ? 1 : 0);
|
||||||
|
|
||||||
|
export const sortNullsLast = (
|
||||||
|
fieldValueA: Maybe<unknown>,
|
||||||
|
fieldValueB: Maybe<unknown>,
|
||||||
|
) => sortNullsFirst(fieldValueB, fieldValueA);
|
||||||
|
|
||||||
|
export const sortAsc = (
|
||||||
|
fieldValueA: string | number,
|
||||||
|
fieldValueB: string | number,
|
||||||
|
) => (fieldValueA < fieldValueB ? -1 : 1);
|
||||||
|
|
||||||
|
export const sortDesc = (
|
||||||
|
fieldValueA: string | number,
|
||||||
|
fieldValueB: string | number,
|
||||||
|
) => sortAsc(fieldValueB, fieldValueA);
|
||||||
Reference in New Issue
Block a user