Feat/activity optimistic activities (#4009)

* Fix naming

* Fixed cache.evict bug for relation target deletion

* Fixed cascade delete activity targets

* Working version

* Fix

* fix

* WIP

* Fixed optimistic effect target inline cell

* Removed openCreateActivityDrawer v1

* Ok for timeline

* Removed console.log

* Fix update record optimistic effect

* Refactored activity queries into useActivities for everything

* Fixed bugs

* Cleaned

* Fix lint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2024-02-20 14:20:45 +01:00
committed by GitHub
parent 6fb0099eb3
commit 36a6558289
68 changed files with 1435 additions and 630 deletions

View File

@ -43,12 +43,19 @@ export const useGenerateObjectRecordOptimisticResponse = ({
);
const relationRecordId = result[relationIdFieldName] as string | null;
const relationRecord = input[fieldMetadataItem.name] as
| ObjectRecord
| undefined;
return {
...result,
[fieldMetadataItem.name]: relationRecordId
? {
__typename: relationRecordTypeName,
id: relationRecordId,
// TODO: there are too many bugs if we don't include the entire relation record
// See if we can find a way to work only with the id and typename
...relationRecord,
}
: null,
};

View File

@ -1,6 +1,5 @@
import { useApolloClient } from '@apollo/client';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
@ -28,11 +27,11 @@ export const useUpsertFindManyRecordsQueryInCache = ({
}) => {
const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({
objectMetadataItem,
depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION,
depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, // TODO: fix this
});
const newObjectRecordConnection = getRecordConnectionFromRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
objectNameSingular: objectMetadataItem.nameSingular,
records: objectRecordsToOverwrite,
});

View File

@ -20,5 +20,6 @@ export const getRecordConnectionFromRecords = <T extends ObjectRecord>({
});
}),
pageInfo: getEmptyPageInfo(),
totalCount: records.length,
} as ObjectRecordConnection<T>;
};

View File

@ -12,6 +12,10 @@ type useDeleteOneRecordProps = {
refetchFindManyQuery?: boolean;
};
type DeleteManyRecordsOptions = {
skipOptimisticEffect?: boolean;
};
export const useDeleteManyRecords = ({
objectNameSingular,
}: useDeleteOneRecordProps) => {
@ -26,34 +30,41 @@ export const useDeleteManyRecords = ({
objectMetadataItem.namePlural,
);
const deleteManyRecords = async (idsToDelete: string[]) => {
const deleteManyRecords = async (
idsToDelete: string[],
options?: DeleteManyRecordsOptions,
) => {
const deletedRecords = await apolloClient.mutate({
mutation: deleteManyRecordsMutation,
variables: {
filter: { id: { in: idsToDelete } },
},
optimisticResponse: {
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
__typename: capitalize(objectNameSingular),
id: idToDelete,
})),
},
update: (cache, { data }) => {
const records = data?.[mutationResponseField];
optimisticResponse: options?.skipOptimisticEffect
? undefined
: {
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
__typename: capitalize(objectNameSingular),
id: idToDelete,
})),
},
update: options?.skipOptimisticEffect
? undefined
: (cache, { data }) => {
const records = data?.[mutationResponseField];
if (!records?.length) return;
if (!records?.length) return;
const cachedRecords = records
.map((record) => getRecordFromCache(record.id, cache))
.filter(isDefined);
const cachedRecords = records
.map((record) => getRecordFromCache(record.id, cache))
.filter(isDefined);
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: cachedRecords,
objectMetadataItems,
});
},
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: cachedRecords,
objectMetadataItems,
});
},
});
return deletedRecords.data?.[mutationResponseField] ?? null;

View File

@ -4,6 +4,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
// TODO: fix connection in relation => automatically change to an array
export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
objectRecordId = '',

View File

@ -79,7 +79,8 @@ export type LeafFilter =
| CurrencyFilter
| URLFilter
| FullNameFilter
| BooleanFilter;
| BooleanFilter
| undefined;
export type AndObjectRecordFilter = {
and?: ObjectRecordQueryFilter[];

View File

@ -0,0 +1,5 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const sortByObjectRecordId = (a: ObjectRecord, b: ObjectRecord) => {
return a.id.localeCompare(b.id);
};

View File

@ -0,0 +1,77 @@
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { sortObjectRecordByDateField } from './sortObjectRecordByDateField';
describe('sortByObjectRecordByCreatedAt', () => {
const recordOldest = { id: '', createdAt: '2022-01-01T00:00:00.000Z' };
const recordNewest = { id: '', createdAt: '2022-01-02T00:00:00.000Z' };
const recordNull1 = { id: '', createdAt: null };
const recordNull2 = { id: '', createdAt: null };
it('should sort in ascending order with null values first', () => {
const sortDirection = 'AscNullsFirst' satisfies OrderBy;
const sortedArray = [
recordNull2,
recordNewest,
recordNull1,
recordOldest,
].sort(sortObjectRecordByDateField('createdAt', sortDirection));
expect(sortedArray).toEqual([
recordNull1,
recordNull2,
recordOldest,
recordNewest,
]);
});
it('should sort in descending order with null values first', () => {
const sortDirection = 'DescNullsFirst' satisfies OrderBy;
const sortedArray = [
recordNull2,
recordOldest,
recordNewest,
recordNull1,
].sort(sortObjectRecordByDateField('createdAt', sortDirection));
expect(sortedArray).toEqual([
recordNull2,
recordNull1,
recordNewest,
recordOldest,
]);
});
it('should sort in ascending order with null values last', () => {
const sortDirection = 'AscNullsLast' satisfies OrderBy;
const sortedArray = [
recordOldest,
recordNull2,
recordNewest,
recordNull1,
].sort(sortObjectRecordByDateField('createdAt', sortDirection));
expect(sortedArray).toEqual([
recordOldest,
recordNewest,
recordNull1,
recordNull2,
]);
});
it('should sort in descending order with null values last', () => {
const sortDirection = 'DescNullsLast' satisfies OrderBy;
const sortedArray = [
recordNull1,
recordOldest,
recordNewest,
recordNull2,
].sort(sortObjectRecordByDateField('createdAt', sortDirection));
expect(sortedArray).toEqual([
recordNewest,
recordOldest,
recordNull1,
recordNull2,
]);
});
});

View File

@ -0,0 +1,68 @@
import { DateTime } from 'luxon';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from '~/utils/isDefined';
const SORT_BEFORE = -1;
const SORT_AFTER = 1;
const SORT_EQUAL = 0;
export const sortObjectRecordByDateField =
<T extends ObjectRecord>(dateField: keyof T, sortDirection: OrderBy) =>
(a: T, b: T) => {
const aDate = a[dateField];
const bDate = b[dateField];
if (!isDefined(aDate) && !isDefined(bDate)) {
return SORT_EQUAL;
}
if (!isDefined(aDate)) {
if (sortDirection === 'AscNullsFirst') {
return SORT_BEFORE;
} else if (sortDirection === 'DescNullsFirst') {
return SORT_BEFORE;
} else if (sortDirection === 'AscNullsLast') {
return SORT_AFTER;
} else if (sortDirection === 'DescNullsLast') {
return SORT_AFTER;
}
throw new Error(`Invalid sortDirection: ${sortDirection}`);
}
if (!isDefined(bDate)) {
if (sortDirection === 'AscNullsFirst') {
return SORT_AFTER;
} else if (sortDirection === 'DescNullsFirst') {
return SORT_AFTER;
} else if (sortDirection === 'AscNullsLast') {
return SORT_BEFORE;
} else if (sortDirection === 'DescNullsLast') {
return SORT_BEFORE;
}
throw new Error(`Invalid sortDirection: ${sortDirection}`);
}
const differenceInMs = DateTime.fromISO(aDate)
.diff(DateTime.fromISO(bDate))
.as('milliseconds');
if (differenceInMs === 0) {
return SORT_EQUAL;
} else if (
sortDirection === 'AscNullsFirst' ||
sortDirection === 'AscNullsLast'
) {
return differenceInMs > 0 ? SORT_AFTER : SORT_BEFORE;
} else if (
sortDirection === 'DescNullsFirst' ||
sortDirection === 'DescNullsLast'
) {
return differenceInMs > 0 ? SORT_BEFORE : SORT_AFTER;
}
throw new Error(`Invalid sortDirection: ${sortDirection}`);
};