mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-22 21:50:43 +03:00
Feat/put target object identifier on use activities (#4682)
When writing to the normalized cache (record), it's crucial to use _refs
for relationships to avoid many problems. Essentially, we only deal with
level 0 and generate all fields to be comfortable with their defaults.
When writing in queries (which should be very rare, the only cases are
prefetch and the case of activities due to the nested query; I've
reduced this to a single file for activities
usePrepareFindManyActivitiesQuery 🙂), it's important to use queryFields
to avoid bugs. I've implemented them on the side of query generation and
record generation.
When doing an updateOne / createOne, etc., it's necessary to distinguish
between optimistic writing (which we actually want to do with _refs) and
the server response without refs. This allows for a clean write in the
optimistic cache without worrying about nesting (as the first point).
To simplify the whole activities part, write to the normalized cache
first. Then, base queries on it in an idempotent manner. This way,
there's no need to worry about the current page or action. The
normalized cache is up-to-date, so I update the queries. Same idea as
for optimisticEffects, actually.
Finally, I've triggered optimisticEffects rather than the manual update
of many queries.
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
parent
4e109c9a38
commit
02673a82af
1
packages/twenty-front/.gitignore
vendored
1
packages/twenty-front/.gitignore
vendored
@ -41,3 +41,4 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
.vite/
|
||||
.nyc_output/
|
@ -18,9 +18,9 @@ export default {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 70,
|
||||
lines: 70,
|
||||
functions: 60,
|
||||
statements: 65,
|
||||
lines: 65,
|
||||
functions: 55,
|
||||
},
|
||||
},
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
||||
|
@ -14,8 +14,8 @@ const modulesCoverage = {
|
||||
};
|
||||
|
||||
const pagesCoverage = {
|
||||
statements: 60,
|
||||
lines: 60,
|
||||
statements: 55,
|
||||
lines: 55,
|
||||
functions: 45,
|
||||
exclude: ['src/generated/**/*', 'src/modules/**/*', 'src/**/*.ts'],
|
||||
};
|
||||
|
@ -13,6 +13,12 @@ const meta: Meta<typeof Calendar> = {
|
||||
container: { width: 728 },
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
args: {
|
||||
targetableObject: {
|
||||
id: '1',
|
||||
targetObjectNameSingular: 'Person',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ClipboardEvent, useCallback, useMemo } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useCreateBlockNote } from '@blocknote/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isArray, isNonEmptyString } from '@sniptt/guards';
|
||||
@ -16,7 +17,7 @@ import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
@ -47,7 +48,7 @@ export const ActivityBodyEditor = ({
|
||||
fillTitleFromBody,
|
||||
}: ActivityBodyEditorProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
const activity = activityInStore as Activity | null;
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
@ -67,9 +68,6 @@ export const ActivityBodyEditor = ({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const modifyActivityFromCache = useModifyRecordFromCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
@ -172,10 +170,15 @@ export const ActivityBodyEditor = ({
|
||||
};
|
||||
});
|
||||
|
||||
modifyActivityFromCache(activityId, {
|
||||
modifyRecordFromCache({
|
||||
recordId: activityId,
|
||||
fieldModifiers: {
|
||||
body: () => {
|
||||
return newStringifiedBody;
|
||||
},
|
||||
},
|
||||
cache,
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const activityTitleHasBeenSet = snapshot
|
||||
@ -198,16 +201,27 @@ export const ActivityBodyEditor = ({
|
||||
};
|
||||
});
|
||||
|
||||
modifyActivityFromCache(activityId, {
|
||||
modifyRecordFromCache({
|
||||
recordId: activityId,
|
||||
fieldModifiers: {
|
||||
title: () => {
|
||||
return newTitleFromBody;
|
||||
},
|
||||
},
|
||||
cache,
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
}
|
||||
|
||||
handlePersistBody(newStringifiedBody);
|
||||
},
|
||||
[activityId, fillTitleFromBody, modifyActivityFromCache, handlePersistBody],
|
||||
[
|
||||
activityId,
|
||||
cache,
|
||||
objectMetadataItemActivity,
|
||||
fillTitleFromBody,
|
||||
handlePersistBody,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
@ -8,6 +7,8 @@ import { canCreateActivityState } from '@/activities/states/canCreateActivitySta
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
@ -23,7 +24,9 @@ export const ActivityEditorEffect = ({
|
||||
);
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
const { deleteActivityFromCache } = useDeleteActivityFromCache();
|
||||
const deleteRecordFromCache = useDeleteRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const upsertActivityCallback = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
@ -68,7 +71,7 @@ export const ActivityEditorEffect = ({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
deleteActivityFromCache(activity);
|
||||
deleteRecordFromCache(activity);
|
||||
}
|
||||
|
||||
set(isActivityInCreateModeState, false);
|
||||
@ -87,7 +90,7 @@ export const ActivityEditorEffect = ({
|
||||
}
|
||||
}
|
||||
},
|
||||
[activityId, deleteActivityFromCache, upsertActivity],
|
||||
[activityId, deleteRecordFromCache, upsertActivity],
|
||||
);
|
||||
|
||||
useRegisterClickOutsideListenerCallback({
|
||||
|
@ -1,10 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import {
|
||||
RecordUpdateHook,
|
||||
@ -26,9 +28,17 @@ export const ActivityEditorFields = ({
|
||||
}) => {
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const [activityFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
const { objectMetadataItem } = useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const activityFromCache = getRecordFromCache<Activity>(activityId);
|
||||
|
||||
const activityFromStore = useRecoilValue(recordStoreFamilyState(activityId));
|
||||
|
||||
const activity = activityFromStore as Activity;
|
||||
|
||||
@ -88,9 +98,9 @@ export const ActivityEditorFields = ({
|
||||
</AssigneeFieldContextProvider>
|
||||
</>
|
||||
)}
|
||||
{ActivityTargetsContextProvider && (
|
||||
{ActivityTargetsContextProvider && isDefined(activityFromCache) && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell activity={activity} />
|
||||
<ActivityTargetsInlineCell activity={activityFromCache} />
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
</StyledPropertyBox>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useRef } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState } from 'recoil';
|
||||
@ -13,7 +14,7 @@ import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import {
|
||||
Checkbox,
|
||||
@ -64,6 +65,8 @@ export const ActivityTitle = ({ activityId }: ActivityTitleProps) => {
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
|
||||
const [activityTitle, setActivityTitle] = useRecoilState(
|
||||
activityTitleFamilyState({ activityId }),
|
||||
);
|
||||
@ -112,10 +115,6 @@ export const ActivityTitle = ({ activityId }: ActivityTitleProps) => {
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const modifyActivityFromCache = useModifyRecordFromCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const persistTitleDebounced = useDebouncedCallback((newTitle: string) => {
|
||||
upsertActivity({
|
||||
activity,
|
||||
@ -142,10 +141,15 @@ export const ActivityTitle = ({ activityId }: ActivityTitleProps) => {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
modifyActivityFromCache(activity.id, {
|
||||
modifyRecordFromCache({
|
||||
recordId: activity.id,
|
||||
fieldModifiers: {
|
||||
title: () => {
|
||||
return newTitle;
|
||||
},
|
||||
},
|
||||
cache: cache,
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
|
@ -40,7 +40,6 @@ export const useRightDrawerEmailThread = () => {
|
||||
receivedAt: 'AscNullsLast',
|
||||
},
|
||||
skip: !viewableEmailThreadId,
|
||||
useRecordsWithoutConnection: true,
|
||||
});
|
||||
|
||||
const fetchMoreMessages = useCallback(() => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Event } from '@/activities/events/types/Event';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Attachment } from '@/activities/files/types/Attachment';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
@ -10,7 +10,7 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => {
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const { records: attachments } = useFindManyRecords({
|
||||
const { records: attachments } = useFindManyRecords<Attachment>({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
filter: {
|
||||
[targetableObjectFieldIdName]: {
|
||||
@ -23,6 +23,6 @@ export const useAttachments = (targetableObject: ActivityTargetableObject) => {
|
||||
});
|
||||
|
||||
return {
|
||||
attachments: attachments as Attachment[],
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { Attachment } from '@/attachments/types/Attachment';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
|
@ -21,7 +21,6 @@ const mockActivityTarget = {
|
||||
|
||||
const mockActivity = {
|
||||
__typename: 'Activity',
|
||||
activityTargets: [],
|
||||
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||
createdAt: '2021-08-03T19:20:06.000Z',
|
||||
completedAt: '2021-08-03T19:20:06.000Z',
|
||||
@ -29,7 +28,6 @@ const mockActivity = {
|
||||
title: 'title',
|
||||
authorId: '1',
|
||||
body: 'body',
|
||||
comments: [],
|
||||
dueAt: '2021-08-03T19:20:06.000Z',
|
||||
type: 'type',
|
||||
assigneeId: '1',
|
||||
@ -66,9 +64,7 @@ const mocks: MockedResponse[] = [
|
||||
__typename
|
||||
updatedAt
|
||||
createdAt
|
||||
personId
|
||||
activityId
|
||||
companyId
|
||||
id
|
||||
}
|
||||
cursor
|
||||
|
@ -1,80 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import gql from 'graphql-tag';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useActivityById } from '@/activities/hooks/useActivityById';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
query FindOneActivity($objectRecordId: UUID!) {
|
||||
activity(filter: { id: { eq: $objectRecordId } }) {
|
||||
__typename
|
||||
createdAt
|
||||
reminderAt
|
||||
authorId
|
||||
title
|
||||
completedAt
|
||||
updatedAt
|
||||
body
|
||||
dueAt
|
||||
type
|
||||
id
|
||||
assigneeId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { objectRecordId: '1234' },
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
activity: mockedActivities[0],
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useActivityById', () => {
|
||||
it('works as expected', async () => {
|
||||
const { result } = renderHook(
|
||||
() => useActivityById({ activityId: '1234' }),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
await waitFor(() => !result.current.loading);
|
||||
|
||||
expect(result.current.activity).toEqual({
|
||||
__typename: 'Activity',
|
||||
assigneeId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
|
||||
authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
|
||||
body: '',
|
||||
comments: [],
|
||||
completedAt: null,
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
activityTargets: [],
|
||||
dueAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
id: '3ecaa1be-aac7-463a-a38e-64078dd451d5',
|
||||
reminderAt: null,
|
||||
title: 'My very first note',
|
||||
type: 'Note',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
});
|
||||
});
|
||||
});
|
@ -1,111 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { Comment } from '@/activities/types/Comment';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||
|
||||
const mockActivityWithConnectionRelation = {
|
||||
activityTargets: {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ActivityTargetEdge',
|
||||
node: {
|
||||
id: '20202020-1029-4661-9e91-83bad932bdff',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
},
|
||||
comments: {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'CommentEdge',
|
||||
node: {
|
||||
id: '20202020-1029-4661-9e91-83bad932bdee',
|
||||
},
|
||||
},
|
||||
] as ObjectRecordEdge<Comment>[],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockActivityWithArrayRelation = {
|
||||
activityTargets: [
|
||||
{
|
||||
id: '20202020-1029-4661-9e91-83bad932bdff',
|
||||
},
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: '20202020-1029-4661-9e91-83bad932bdee',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('useActivityConnectionUtils', () => {
|
||||
it('Should turn activity with connection relation in activity with array relation', async () => {
|
||||
const { result } = renderHook(() => useActivityConnectionUtils(), {
|
||||
wrapper: ({ children }) => (
|
||||
<RecoilRoot
|
||||
initializeState={(snapshot) => {
|
||||
snapshot.set(
|
||||
objectMetadataItemsState,
|
||||
getObjectMetadataItemsMock(),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecoilRoot>
|
||||
),
|
||||
});
|
||||
|
||||
const { makeActivityWithoutConnection } = result.current;
|
||||
|
||||
const { activity: activityWithArrayRelation } =
|
||||
makeActivityWithoutConnection(mockActivityWithConnectionRelation as any);
|
||||
|
||||
expect(activityWithArrayRelation).toBeDefined();
|
||||
|
||||
expect(activityWithArrayRelation.activityTargets[0].id).toEqual(
|
||||
mockActivityWithArrayRelation.activityTargets[0].id,
|
||||
);
|
||||
});
|
||||
|
||||
it('Should turn activity with connection relation in activity with array relation', async () => {
|
||||
const { result } = renderHook(() => useActivityConnectionUtils(), {
|
||||
wrapper: ({ children }) => (
|
||||
<RecoilRoot
|
||||
initializeState={(snapshot) => {
|
||||
snapshot.set(
|
||||
objectMetadataItemsState,
|
||||
getObjectMetadataItemsMock(),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RecoilRoot>
|
||||
),
|
||||
});
|
||||
|
||||
const { makeActivityWithConnection } = result.current;
|
||||
|
||||
const { activityWithConnection } = makeActivityWithConnection(
|
||||
mockActivityWithArrayRelation as any,
|
||||
);
|
||||
|
||||
expect(activityWithConnection).toBeDefined();
|
||||
|
||||
expect(activityWithConnection.activityTargets.edges[0].node.id).toEqual(
|
||||
mockActivityWithConnectionRelation.activityTargets.edges[0].node.id,
|
||||
);
|
||||
});
|
||||
});
|
@ -1,167 +1,119 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import gql from 'graphql-tag';
|
||||
import { gql, InMemoryCache } from '@apollo/client';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
|
||||
const defaultResponseData = {
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const mockActivityTarget = {
|
||||
__typename: 'ActivityTarget',
|
||||
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||
createdAt: '2021-08-03T19:20:06.000Z',
|
||||
personId: '1',
|
||||
activityId: '234',
|
||||
companyId: '1',
|
||||
id: '123',
|
||||
person: { ...mockedPeopleData[0], __typename: 'Person', updatedAt: '' },
|
||||
company: { ...mockedCompaniesData[0], __typename: 'Company', updatedAt: '' },
|
||||
activity: mockedActivities[0],
|
||||
};
|
||||
|
||||
const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
query FindManyActivityTargets(
|
||||
$filter: ActivityTargetFilterInput
|
||||
$orderBy: ActivityTargetOrderByInput
|
||||
$lastCursor: String
|
||||
$limit: Float
|
||||
) {
|
||||
activityTargets(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
updatedAt
|
||||
createdAt
|
||||
company {
|
||||
__typename
|
||||
xLink {
|
||||
label
|
||||
url
|
||||
}
|
||||
linkedinLink {
|
||||
label
|
||||
url
|
||||
}
|
||||
domainName
|
||||
annualRecurringRevenue {
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
createdAt
|
||||
address
|
||||
updatedAt
|
||||
name
|
||||
accountOwnerId
|
||||
employees
|
||||
id
|
||||
idealCustomerProfile
|
||||
}
|
||||
personId
|
||||
activityId
|
||||
companyId
|
||||
id
|
||||
activity {
|
||||
__typename
|
||||
createdAt
|
||||
reminderAt
|
||||
authorId
|
||||
title
|
||||
completedAt
|
||||
updatedAt
|
||||
body
|
||||
dueAt
|
||||
type
|
||||
id
|
||||
assigneeId
|
||||
}
|
||||
person {
|
||||
__typename
|
||||
xLink {
|
||||
label
|
||||
url
|
||||
}
|
||||
id
|
||||
createdAt
|
||||
city
|
||||
email
|
||||
jobTitle
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
phone
|
||||
linkedinLink {
|
||||
label
|
||||
url
|
||||
}
|
||||
updatedAt
|
||||
avatarUrl
|
||||
companyId
|
||||
}
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
filter: { activityId: { eq: '1234' } },
|
||||
limit: undefined,
|
||||
orderBy: undefined,
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
activityTargets: {
|
||||
...defaultResponseData,
|
||||
edges: [
|
||||
{
|
||||
node: mockActivityTarget,
|
||||
cursor: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
|
||||
const cache = new InMemoryCache();
|
||||
|
||||
const activityNode = {
|
||||
id: '3ecaa1be-aac7-463a-a38e-64078dd451d5',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
reminderAt: null,
|
||||
title: 'My very first note',
|
||||
type: 'Note',
|
||||
body: '',
|
||||
dueAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
completedAt: null,
|
||||
author: null,
|
||||
assignee: null,
|
||||
assigneeId: null,
|
||||
authorId: null,
|
||||
comments: {
|
||||
edges: [],
|
||||
},
|
||||
activityTargets: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
personId: null,
|
||||
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||
company: {
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||
name: 'Airbnb',
|
||||
domainName: 'airbnb.com',
|
||||
},
|
||||
person: null,
|
||||
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||
activity: {
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
},
|
||||
__typename: 'ActivityTarget',
|
||||
},
|
||||
__typename: 'ActivityTargetEdge',
|
||||
},
|
||||
],
|
||||
__typename: 'ActivityTargetConnection',
|
||||
},
|
||||
__typename: 'Activity' as const,
|
||||
};
|
||||
|
||||
cache.writeFragment({
|
||||
fragment: gql`
|
||||
fragment CreateOneActivityInCache on Activity {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
reminderAt
|
||||
title
|
||||
body
|
||||
dueAt
|
||||
completedAt
|
||||
author
|
||||
assignee
|
||||
assigneeId
|
||||
authorId
|
||||
activityTargets {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
targetObjectNameSingular
|
||||
personId
|
||||
companyId
|
||||
company {
|
||||
id
|
||||
name
|
||||
domainName
|
||||
}
|
||||
person
|
||||
activityId
|
||||
activity {
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
id: activityNode.id,
|
||||
data: activityNode,
|
||||
});
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<MockedProvider cache={cache}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
@ -170,19 +122,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
);
|
||||
|
||||
describe('useActivityTargetObjectRecords', () => {
|
||||
it('returns default response', () => {
|
||||
const { result } = renderHook(
|
||||
() => useActivityTargetObjectRecords({ activityId: '1234' }),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({
|
||||
activityTargetObjectRecords: [],
|
||||
loadingActivityTargets: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches records', async () => {
|
||||
it('return targetObjects', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
@ -192,11 +132,12 @@ describe('useActivityTargetObjectRecords', () => {
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
|
||||
const { activityTargetObjectRecords, loadingActivityTargets } =
|
||||
useActivityTargetObjectRecords({ activityId: '1234' });
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
|
||||
getRecordFromRecordNode({ recordNode: activityNode as any }),
|
||||
);
|
||||
|
||||
return {
|
||||
activityTargetObjectRecords,
|
||||
loadingActivityTargets,
|
||||
setCurrentWorkspaceMember,
|
||||
setObjectMetadataItems,
|
||||
};
|
||||
@ -208,16 +149,18 @@ describe('useActivityTargetObjectRecords', () => {
|
||||
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
||||
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
||||
});
|
||||
const activityTargetObjectRecords =
|
||||
result.current.activityTargetObjectRecords;
|
||||
|
||||
expect(result.current.loadingActivityTargets).toBe(true);
|
||||
|
||||
// Wait for activityTargets to complete fetching
|
||||
await waitFor(() => !result.current.loadingActivityTargets);
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
expect(result.current.activityTargetObjectRecords).toHaveLength(1);
|
||||
expect(activityTargetObjectRecords).toHaveLength(1);
|
||||
expect(activityTargetObjectRecords[0].activityTarget).toEqual(
|
||||
activityNode.activityTargets.edges[0].node,
|
||||
);
|
||||
expect(activityTargetObjectRecords[0].targetObject).toEqual(
|
||||
activityNode.activityTargets.edges[0].node.company,
|
||||
);
|
||||
expect(
|
||||
result.current.activityTargetObjectRecords[0].targetObjectNameSingular,
|
||||
).toBe('person');
|
||||
activityTargetObjectRecords[0].targetObjectMetadataItem.nameSingular,
|
||||
).toEqual('company');
|
||||
});
|
||||
});
|
||||
|
@ -1,76 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
|
||||
const mocks: MockedResponse[] = [];
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
|
||||
describe('useAttachRelationInBothDirections', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setObjectMetadataItems = useSetRecoilState(
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
|
||||
const res = useAttachRelationInBothDirections();
|
||||
return {
|
||||
...res,
|
||||
setCurrentWorkspaceMember,
|
||||
setObjectMetadataItems,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
||||
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
||||
});
|
||||
const targetRecords = [
|
||||
{ id: '5678', person: { id: '1234' } },
|
||||
{ id: '91011', person: { id: '1234' } },
|
||||
];
|
||||
|
||||
const forEachSpy = jest.spyOn(targetRecords, 'forEach');
|
||||
|
||||
act(() => {
|
||||
result.current.attachRelationInBothDirections({
|
||||
sourceRecord: {
|
||||
id: '1234',
|
||||
company: { id: '5678' },
|
||||
},
|
||||
targetRecords,
|
||||
sourceObjectNameSingular: 'person',
|
||||
targetObjectNameSingular: 'company',
|
||||
fieldNameOnSourceRecord: 'company',
|
||||
fieldNameOnTargetRecord: 'person',
|
||||
});
|
||||
});
|
||||
|
||||
// expect forEach to have been called on targetRecords
|
||||
expect(forEachSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -1,53 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import pick from 'lodash.pick';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const triggerDeleteRecordsOptimisticEffectMock = jest.fn();
|
||||
|
||||
// mock the triggerDeleteRecordsOptimisticEffect function
|
||||
jest.mock(
|
||||
'@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect',
|
||||
() => ({
|
||||
triggerDeleteRecordsOptimisticEffect: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
(triggerDeleteRecordsOptimisticEffect as jest.Mock).mockImplementation(
|
||||
triggerDeleteRecordsOptimisticEffectMock,
|
||||
);
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useDeleteActivityFromCache', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(() => useDeleteActivityFromCache(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.deleteActivityFromCache(
|
||||
pick(mockedActivities[0], [
|
||||
'id',
|
||||
'title',
|
||||
'body',
|
||||
'type',
|
||||
'completedAt',
|
||||
'dueAt',
|
||||
'updatedAt',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(triggerDeleteRecordsOptimisticEffectMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,60 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const upsertFindManyRecordsQueryInCacheMock = jest.fn();
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache',
|
||||
() => ({
|
||||
useUpsertFindManyRecordsQueryInCache: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({
|
||||
upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock,
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useInjectIntoActivitiesQueries', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(() => useInjectIntoActivitiesQueries(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.injectActivitiesQueries({
|
||||
activityToInject: mockedActivities[0],
|
||||
activityTargetsToInject: [],
|
||||
targetableObjects: [{ id: '123', targetObjectNameSingular: 'person' }],
|
||||
});
|
||||
|
||||
expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.injectActivitiesQueries({
|
||||
activityToInject: mockedActivities[0],
|
||||
activityTargetsToInject: [],
|
||||
targetableObjects: [],
|
||||
});
|
||||
|
||||
expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,64 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useInjectIntoActivityTargetsQueries } from '@/activities/hooks/useInjectIntoActivityTargetsQueries';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const upsertFindManyRecordsQueryInCacheMock = jest.fn();
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache',
|
||||
() => ({
|
||||
useUpsertFindManyRecordsQueryInCache: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({
|
||||
upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock,
|
||||
}));
|
||||
|
||||
const mockActivityTarget = {
|
||||
__typename: 'ActivityTarget',
|
||||
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||
createdAt: '2021-08-03T19:20:06.000Z',
|
||||
personId: '1',
|
||||
activityId: '234',
|
||||
companyId: '1',
|
||||
id: '123',
|
||||
activity: mockedActivities[0],
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useInjectIntoActivityTargetsQueries', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(() => useInjectIntoActivityTargetsQueries(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.injectActivityTargetsQueries({
|
||||
activityTargetsToInject: [mockActivityTarget],
|
||||
targetableObjects: [{ id: '123', targetObjectNameSingular: 'person' }],
|
||||
});
|
||||
|
||||
expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.injectActivityTargetsQueries({
|
||||
activityTargetsToInject: [mockActivityTarget],
|
||||
targetableObjects: [],
|
||||
});
|
||||
|
||||
expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useModifyActivityOnActivityTargetsCache } from '@/activities/hooks/useModifyActivityOnActivityTargetCache';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const useModifyRecordFromCacheMock = jest.fn();
|
||||
|
||||
jest.mock('@/object-record/cache/hooks/useModifyRecordFromCache', () => ({
|
||||
useModifyRecordFromCache: jest.fn(),
|
||||
}));
|
||||
|
||||
(useModifyRecordFromCache as jest.Mock).mockImplementation(
|
||||
() => useModifyRecordFromCacheMock,
|
||||
);
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useModifyActivityOnActivityTargetsCache', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(
|
||||
() => useModifyActivityOnActivityTargetsCache(),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.modifyActivityOnActivityTargetsCache({
|
||||
activity: mockedActivities[0],
|
||||
activityTargetIds: ['123', '456'],
|
||||
});
|
||||
});
|
||||
|
||||
expect(useModifyRecordFromCacheMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -1,43 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
|
||||
const useModifyRecordFromCacheMock = jest.fn();
|
||||
|
||||
jest.mock('@/object-record/cache/hooks/useModifyRecordFromCache', () => ({
|
||||
useModifyRecordFromCache: jest.fn(),
|
||||
}));
|
||||
|
||||
(useModifyRecordFromCache as jest.Mock).mockImplementation(
|
||||
() => useModifyRecordFromCacheMock,
|
||||
);
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useModifyActivityTargetsOnActivityCache', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(
|
||||
() => useModifyActivityTargetsOnActivityCache(),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.modifyActivityTargetsOnActivityCache({
|
||||
activityId: '1234',
|
||||
activityTargets: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(useModifyRecordFromCacheMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -1,110 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
|
||||
|
||||
const useOpenCreateActivityDrawerMock = jest.fn();
|
||||
jest.mock('@/activities/hooks/useOpenCreateActivityDrawer', () => ({
|
||||
useOpenCreateActivityDrawer: jest.fn(),
|
||||
}));
|
||||
|
||||
(useOpenCreateActivityDrawer as jest.Mock).mockImplementation(
|
||||
() => useOpenCreateActivityDrawerMock,
|
||||
);
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
const recordTableId = 'recordTableId';
|
||||
const tableRowIds = ['123', '456'];
|
||||
const recordObject = {
|
||||
id: '789',
|
||||
};
|
||||
|
||||
describe('useOpenCreateActivityDrawerForSelectedRowIds', () => {
|
||||
it('works as expected', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const openCreateActivityDrawerForSelectedRowIds =
|
||||
useOpenCreateActivityDrawerForSelectedRowIds(recordTableId);
|
||||
const viewableActivityId = useRecoilValue(viewableActivityIdState);
|
||||
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
|
||||
const setObjectMetadataItems = useSetRecoilState(
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
const scopeId = `${recordTableId}-scope`;
|
||||
const setTableRowIds = useSetRecoilState(
|
||||
tableRowIdsComponentState({ scopeId }),
|
||||
);
|
||||
const setIsRowSelectedComponentFamilyState = useSetRecoilState(
|
||||
isRowSelectedComponentFamilyState({
|
||||
scopeId,
|
||||
familyKey: tableRowIds[0],
|
||||
}),
|
||||
);
|
||||
const setRecordStoreFamilyState = useSetRecoilState(
|
||||
recordStoreFamilyState(tableRowIds[0]),
|
||||
);
|
||||
return {
|
||||
openCreateActivityDrawerForSelectedRowIds,
|
||||
activityIdInDrawer,
|
||||
viewableActivityId,
|
||||
setObjectMetadataItems,
|
||||
setTableRowIds,
|
||||
setIsRowSelectedComponentFamilyState,
|
||||
setRecordStoreFamilyState,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setTableRowIds(tableRowIds);
|
||||
result.current.setRecordStoreFamilyState(recordObject);
|
||||
result.current.setIsRowSelectedComponentFamilyState(true);
|
||||
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
||||
});
|
||||
|
||||
expect(result.current.activityIdInDrawer).toBeNull();
|
||||
expect(result.current.viewableActivityId).toBeNull();
|
||||
await act(async () => {
|
||||
result.current.openCreateActivityDrawerForSelectedRowIds(
|
||||
'Note',
|
||||
'person',
|
||||
[{ id: '176', targetObjectNameSingular: 'person' }],
|
||||
);
|
||||
});
|
||||
|
||||
expect(useOpenCreateActivityDrawerMock).toHaveBeenCalledWith({
|
||||
type: 'Note',
|
||||
targetableObjects: [
|
||||
{
|
||||
type: 'Custom',
|
||||
targetObjectNameSingular: 'person',
|
||||
id: '123',
|
||||
targetObjectRecord: { id: '789' },
|
||||
},
|
||||
{
|
||||
id: '176',
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -1,63 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
|
||||
const upsertFindManyRecordsQueryInCacheMock = jest.fn();
|
||||
const useReadFindManyRecordsQueryInCacheMock = jest.fn(() => [
|
||||
{ activityId: '981' },
|
||||
{ activityId: '345' },
|
||||
]);
|
||||
jest.mock(
|
||||
'@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache',
|
||||
() => ({
|
||||
useReadFindManyRecordsQueryInCache: jest.fn(),
|
||||
}),
|
||||
);
|
||||
jest.mock(
|
||||
'@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache',
|
||||
() => ({
|
||||
useUpsertFindManyRecordsQueryInCache: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
(useReadFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({
|
||||
readFindManyRecordsQueryInCache: useReadFindManyRecordsQueryInCacheMock,
|
||||
}));
|
||||
|
||||
(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({
|
||||
upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock,
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useRemoveFromActivitiesQueries', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(() => useRemoveFromActivitiesQueries(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.removeFromActivitiesQueries({
|
||||
activityIdToRemove: '123',
|
||||
targetableObjects: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledWith({
|
||||
objectRecordsToOverwrite: [{ activityId: '981' }, { activityId: '345' }],
|
||||
queryVariables: {
|
||||
filter: { id: { in: ['345', '981'] } },
|
||||
orderBy: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -1,72 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useRemoveFromActivityTargetsQueries } from '@/activities/hooks/useRemoveFromActivityTargetsQueries';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const upsertFindManyRecordsQueryInCacheMock = jest.fn();
|
||||
const useReadFindManyRecordsQueryInCacheMock = jest.fn(() => [
|
||||
{ id: '981' },
|
||||
{ id: '345' },
|
||||
]);
|
||||
jest.mock(
|
||||
'@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache',
|
||||
() => ({
|
||||
useReadFindManyRecordsQueryInCache: jest.fn(),
|
||||
}),
|
||||
);
|
||||
jest.mock(
|
||||
'@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache',
|
||||
() => ({
|
||||
useUpsertFindManyRecordsQueryInCache: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
(useReadFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({
|
||||
readFindManyRecordsQueryInCache: useReadFindManyRecordsQueryInCacheMock,
|
||||
}));
|
||||
(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({
|
||||
upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock,
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
const mockActivityTarget = {
|
||||
__typename: 'ActivityTarget',
|
||||
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||
createdAt: '2021-08-03T19:20:06.000Z',
|
||||
personId: '1',
|
||||
activityId: '234',
|
||||
companyId: '1',
|
||||
id: '123',
|
||||
activity: mockedActivities[0],
|
||||
};
|
||||
|
||||
describe('useRemoveFromActivityTargetsQueries', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(() => useRemoveFromActivityTargetsQueries(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.removeFromActivityTargetsQueries({
|
||||
activityTargetsToRemove: [mockActivityTarget],
|
||||
targetableObjects: [],
|
||||
});
|
||||
});
|
||||
|
||||
expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledWith({
|
||||
objectRecordsToOverwrite: [{ id: '981' }, { id: '345' }],
|
||||
queryVariables: { filter: {} },
|
||||
depth: 2,
|
||||
});
|
||||
});
|
||||
});
|
@ -1,187 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import gql from 'graphql-tag';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
|
||||
import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const newId = 'new-id';
|
||||
const activity = mockedActivities[0];
|
||||
const input: Partial<Activity> = { id: newId };
|
||||
|
||||
const mockedDate = '2024-03-15T12:00:00.000Z';
|
||||
const toISOStringMock = jest.fn(() => mockedDate);
|
||||
global.Date.prototype.toISOString = toISOStringMock;
|
||||
|
||||
const useCreateActivityInDBMock = jest.fn();
|
||||
|
||||
jest.mock('@/activities/hooks/useCreateActivityInDB', () => ({
|
||||
useCreateActivityInDB: jest.fn(),
|
||||
}));
|
||||
(useCreateActivityInDB as jest.Mock).mockImplementation(() => ({
|
||||
createActivityInDB: useCreateActivityInDBMock,
|
||||
}));
|
||||
|
||||
const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
mutation UpdateOneActivity(
|
||||
$idToUpdate: ID!
|
||||
$input: ActivityUpdateInput!
|
||||
) {
|
||||
updateActivity(id: $idToUpdate, data: $input) {
|
||||
__typename
|
||||
createdAt
|
||||
reminderAt
|
||||
authorId
|
||||
title
|
||||
completedAt
|
||||
updatedAt
|
||||
body
|
||||
dueAt
|
||||
type
|
||||
id
|
||||
assigneeId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
idToUpdate: activity.id,
|
||||
input: { id: 'new-id' },
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updateActivity: { ...activity, ...input },
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const getWrapper =
|
||||
(initialIndex: 0 | 1) =>
|
||||
({ children }: { children: ReactNode }) => (
|
||||
<MemoryRouter
|
||||
initialEntries={['/tasks', '/object', { pathname: '/three' }]}
|
||||
initialIndex={initialIndex}
|
||||
>
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
{children}
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('useUpsertActivity', () => {
|
||||
it('updates an activity', async () => {
|
||||
const { result } = renderHook(() => useUpsertActivity(), {
|
||||
wrapper: getWrapper(0),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upsertActivity({
|
||||
activity,
|
||||
input,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates an activity on tasks page', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const res = useUpsertActivity();
|
||||
const setIsActivityInCreateMode = useSetRecoilState(
|
||||
isActivityInCreateModeState,
|
||||
);
|
||||
|
||||
return { ...res, setIsActivityInCreateMode };
|
||||
},
|
||||
{
|
||||
wrapper: getWrapper(0),
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setIsActivityInCreateMode(true);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upsertActivity({
|
||||
activity,
|
||||
input: {},
|
||||
});
|
||||
});
|
||||
|
||||
expect(useCreateActivityInDBMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('creates an activity on objects page', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const res = useUpsertActivity();
|
||||
const setIsActivityInCreateMode = useSetRecoilState(
|
||||
isActivityInCreateModeState,
|
||||
);
|
||||
const setObjectShowPageTargetableObject = useSetRecoilState(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
const setCurrentCompletedTaskQueryVariables = useSetRecoilState(
|
||||
currentCompletedTaskQueryVariablesState,
|
||||
);
|
||||
const setCurrentIncompleteTaskQueryVariables = useSetRecoilState(
|
||||
currentIncompleteTaskQueryVariablesState,
|
||||
);
|
||||
|
||||
const setCurrentNotesQueryVariables = useSetRecoilState(
|
||||
currentNotesQueryVariablesState,
|
||||
);
|
||||
|
||||
return {
|
||||
...res,
|
||||
setIsActivityInCreateMode,
|
||||
setObjectShowPageTargetableObject,
|
||||
setCurrentCompletedTaskQueryVariables,
|
||||
setCurrentIncompleteTaskQueryVariables,
|
||||
setCurrentNotesQueryVariables,
|
||||
};
|
||||
},
|
||||
{
|
||||
wrapper: getWrapper(1),
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setIsActivityInCreateMode(true);
|
||||
result.current.setObjectShowPageTargetableObject({
|
||||
id: '123',
|
||||
targetObjectNameSingular: 'people',
|
||||
});
|
||||
result.current.setCurrentCompletedTaskQueryVariables({});
|
||||
result.current.setCurrentIncompleteTaskQueryVariables({});
|
||||
result.current.setCurrentNotesQueryVariables({});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.upsertActivity({
|
||||
activity,
|
||||
input: {},
|
||||
});
|
||||
});
|
||||
|
||||
expect(useCreateActivityInDBMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
@ -2,13 +2,12 @@ import { useEffect, useState } from 'react';
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects';
|
||||
import { FIND_MANY_ACTIVITIES_QUERY_KEY } from '@/activities/query-keys/FindManyActivitiesQueryKey';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
@ -29,7 +28,7 @@ export const useActivities = ({
|
||||
}) => {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const {
|
||||
activityTargets,
|
||||
@ -40,13 +39,17 @@ export const useActivities = ({
|
||||
skip: skipActivityTargets || skip,
|
||||
});
|
||||
|
||||
const activityIds = activityTargets
|
||||
const activityIds = [
|
||||
...new Set(
|
||||
activityTargets
|
||||
? [
|
||||
...activityTargets
|
||||
.map((activityTarget) => activityTarget.activityId)
|
||||
.filter(isNonEmptyString),
|
||||
].sort(sortByAscString)
|
||||
: [];
|
||||
: [],
|
||||
),
|
||||
];
|
||||
|
||||
const activityTargetsFound =
|
||||
initializedActivityTargets && isNonEmptyArray(activityTargets);
|
||||
@ -65,24 +68,22 @@ export const useActivities = ({
|
||||
(!skipActivityTargets &&
|
||||
(!initializedActivityTargets || !activityTargetsFound));
|
||||
|
||||
const { records: activitiesWithConnection, loading: loadingActivities } =
|
||||
const { records: activities, loading: loadingActivities } =
|
||||
useFindManyRecords<Activity>({
|
||||
skip: skipActivities,
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
depth: 1,
|
||||
objectNameSingular: FIND_MANY_ACTIVITIES_QUERY_KEY.objectNameSingular,
|
||||
depth: FIND_MANY_ACTIVITIES_QUERY_KEY.depth,
|
||||
queryFields:
|
||||
FIND_MANY_ACTIVITIES_QUERY_KEY.fieldsFactory?.(objectMetadataItems),
|
||||
filter,
|
||||
orderBy: activitiesOrderByVariables,
|
||||
onCompleted: useRecoilCallback(
|
||||
({ set }) =>
|
||||
(data) => {
|
||||
(activities) => {
|
||||
if (!initialized) {
|
||||
setInitialized(true);
|
||||
}
|
||||
|
||||
const activities = getRecordsFromRecordConnection({
|
||||
recordConnection: data,
|
||||
});
|
||||
|
||||
for (const activity of activities) {
|
||||
set(recordStoreFamilyState(activity.id), activity);
|
||||
}
|
||||
@ -93,11 +94,6 @@ export const useActivities = ({
|
||||
|
||||
const loading = loadingActivities || loadingActivityTargets;
|
||||
|
||||
// TODO: fix connection in relation => automatically change to an array
|
||||
const activities: Activity[] = activitiesWithConnection
|
||||
?.map(makeActivityWithoutConnection as any)
|
||||
.map(({ activity }: any) => activity);
|
||||
|
||||
const noActivities =
|
||||
(!activityTargetsFound && !skipActivityTargets && initialized) ||
|
||||
(initialized && !loading && !isNonEmptyArray(activities));
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
|
||||
const QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS = 3;
|
||||
|
||||
export const useActivityById = ({ activityId }: { activityId: string }) => {
|
||||
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
|
||||
|
||||
// TODO: fix connection in relation => automatically change to an array
|
||||
const { record: activityWithConnections, loading } = useFindOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
skip: !activityId,
|
||||
depth: QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS,
|
||||
});
|
||||
|
||||
const { activity } = activityWithConnections
|
||||
? makeActivityWithoutConnection(activityWithConnections as any)
|
||||
: { activity: null };
|
||||
|
||||
return {
|
||||
activity,
|
||||
loading,
|
||||
};
|
||||
};
|
@ -1,112 +0,0 @@
|
||||
import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { Comment } from '@/activities/types/Comment';
|
||||
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
|
||||
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useActivityConnectionUtils = () => {
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const makeActivityWithoutConnection = (
|
||||
activityWithConnections: Activity & {
|
||||
activityTargets: ObjectRecordConnection<ActivityTarget>;
|
||||
comments: ObjectRecordConnection<Comment>;
|
||||
},
|
||||
) => {
|
||||
if (!isDefined(activityWithConnections)) {
|
||||
throw new Error('Activity with connections is not defined');
|
||||
}
|
||||
|
||||
const hasActivityTargetsConnection = isObjectRecordConnection(
|
||||
CoreObjectNameSingular.ActivityTarget,
|
||||
activityWithConnections?.activityTargets,
|
||||
);
|
||||
|
||||
const activityTargets: ActivityTarget[] = [];
|
||||
|
||||
if (hasActivityTargetsConnection) {
|
||||
const newActivityTargets = mapConnectionToRecords({
|
||||
objectRecordConnection: activityWithConnections?.activityTargets,
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
depth: 5,
|
||||
}) as ActivityTarget[];
|
||||
|
||||
activityTargets.push(...newActivityTargets);
|
||||
}
|
||||
|
||||
const hasCommentsConnection = isObjectRecordConnection(
|
||||
CoreObjectNameSingular.Comment,
|
||||
activityWithConnections?.comments,
|
||||
);
|
||||
|
||||
const comments: Comment[] = [];
|
||||
|
||||
if (hasCommentsConnection) {
|
||||
const newComments = mapConnectionToRecords({
|
||||
objectRecordConnection: activityWithConnections?.comments,
|
||||
objectNameSingular: CoreObjectNameSingular.Comment,
|
||||
depth: 5,
|
||||
}) as Comment[];
|
||||
|
||||
comments.push(...newComments);
|
||||
}
|
||||
|
||||
const activity: Activity = {
|
||||
...activityWithConnections,
|
||||
activityTargets,
|
||||
comments,
|
||||
};
|
||||
|
||||
return { activity };
|
||||
};
|
||||
|
||||
const makeActivityWithConnection = (activity: Activity) => {
|
||||
const activityTargetEdges = isNonEmptyArray(activity?.activityTargets)
|
||||
? activity.activityTargets.map((activityTarget) => ({
|
||||
node: activityTarget,
|
||||
cursor: '',
|
||||
}))
|
||||
: [];
|
||||
|
||||
const commentEdges = isNonEmptyArray(activity?.comments)
|
||||
? activity.comments.map((comment) => ({
|
||||
node: comment,
|
||||
cursor: '',
|
||||
}))
|
||||
: [];
|
||||
|
||||
const activityTargets = {
|
||||
__typename: 'ActivityTargetConnection',
|
||||
edges: activityTargetEdges,
|
||||
pageInfo: getEmptyPageInfo(),
|
||||
} as ObjectRecordConnection<ActivityTarget>;
|
||||
|
||||
const comments = {
|
||||
__typename: 'CommentConnection',
|
||||
edges: commentEdges,
|
||||
pageInfo: getEmptyPageInfo(),
|
||||
} as ObjectRecordConnection<Comment>;
|
||||
|
||||
const activityWithConnection = {
|
||||
...activity,
|
||||
activityTargets,
|
||||
comments,
|
||||
} as Activity & {
|
||||
activityTargets: ObjectRecordConnection<ActivityTarget>;
|
||||
comments: ObjectRecordConnection<Comment>;
|
||||
};
|
||||
|
||||
return { activityWithConnection };
|
||||
};
|
||||
|
||||
return {
|
||||
makeActivityWithoutConnection,
|
||||
makeActivityWithConnection,
|
||||
};
|
||||
};
|
@ -1,56 +1,73 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { Nullable } from '~/types/Nullable';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useActivityTargetObjectRecords = ({
|
||||
activityId,
|
||||
}: {
|
||||
activityId: string;
|
||||
}) => {
|
||||
export const useActivityTargetObjectRecords = (activity: Activity) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const { records: activityTargets, loading: loadingActivityTargets } =
|
||||
useFindManyRecords<ActivityTarget>({
|
||||
const activityTargets = activity.activityTargets ?? [];
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
skip: !isNonEmptyString(activityId),
|
||||
filter: {
|
||||
activityId: {
|
||||
eq: activityId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const activityTargetObjectRecords = activityTargets
|
||||
.map<Nullable<ActivityTargetWithTargetRecord>>((activityTarget) => {
|
||||
const activityTargetFromCache = getRecordFromCache<ActivityTarget>(
|
||||
activityTarget.id,
|
||||
apolloClient.cache,
|
||||
);
|
||||
|
||||
if (!isDefined(activityTargetFromCache)) {
|
||||
throw new Error(
|
||||
`Cannot find activity target ${activityTarget.id} in cache, this shouldn't happen.`,
|
||||
);
|
||||
}
|
||||
|
||||
const correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
|
||||
isDefined(activityTargetFromCache[objectMetadataItem.nameSingular]) &&
|
||||
!objectMetadataItem.isSystem,
|
||||
);
|
||||
|
||||
if (!correspondingObjectMetadataItem) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const targetObjectRecord =
|
||||
activityTargetFromCache[correspondingObjectMetadataItem.nameSingular];
|
||||
|
||||
if (!targetObjectRecord) {
|
||||
throw new Error(
|
||||
`Cannot find target object record of type ${correspondingObjectMetadataItem.nameSingular}, make sure the request for activities eagerly loads for the target objects on activity target relation.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
activityTarget: activityTarget,
|
||||
targetObject:
|
||||
activityTarget[correspondingObjectMetadataItem.nameSingular],
|
||||
activityTarget: activityTargetFromCache ?? activityTarget,
|
||||
targetObject: targetObjectRecord ?? undefined,
|
||||
targetObjectMetadataItem: correspondingObjectMetadataItem,
|
||||
targetObjectNameSingular: correspondingObjectMetadataItem.nameSingular,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
activityTargetObjectRecords,
|
||||
loadingActivityTargets,
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
@ -26,7 +26,7 @@ export const useActivityTargetsForTargetableObject = ({
|
||||
// If we are on a show page and we remove the current show page object corresponding activity target
|
||||
// See also if we need to update useTimelineActivities
|
||||
const { records: activityTargets, loading: loadingActivityTargets } =
|
||||
useFindManyRecords({
|
||||
useFindManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
skip: skipRequest,
|
||||
filter: {
|
||||
@ -42,7 +42,7 @@ export const useActivityTargetsForTargetableObject = ({
|
||||
});
|
||||
|
||||
return {
|
||||
activityTargets: activityTargets as ActivityTarget[],
|
||||
activityTargets,
|
||||
loadingActivityTargets,
|
||||
initialized,
|
||||
};
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { FIND_MANY_ACTIVITY_TARGETS_QUERY_KEY } from '@/activities/query-keys/FindManyActivityTargetsQueryKey';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
export const useActivityTargetsForTargetableObjects = ({
|
||||
@ -20,16 +22,23 @@ export const useActivityTargetsForTargetableObjects = ({
|
||||
targetableObjects: targetableObjects,
|
||||
});
|
||||
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
// TODO: We want to optimistically remove from this request
|
||||
// If we are on a show page and we remove the current show page object corresponding activity target
|
||||
// See also if we need to update useTimelineActivities
|
||||
const { records: activityTargets, loading: loadingActivityTargets } =
|
||||
useFindManyRecords({
|
||||
useFindManyRecords<ActivityTarget>({
|
||||
skip,
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
objectNameSingular:
|
||||
FIND_MANY_ACTIVITY_TARGETS_QUERY_KEY.objectNameSingular,
|
||||
filter: activityTargetsFilter,
|
||||
queryFields:
|
||||
FIND_MANY_ACTIVITY_TARGETS_QUERY_KEY.fieldsFactory?.(
|
||||
objectMetadataItems,
|
||||
),
|
||||
onCompleted: () => {
|
||||
if (!initialized) {
|
||||
setInitialized(true);
|
||||
@ -38,7 +47,7 @@ export const useActivityTargetsForTargetableObjects = ({
|
||||
});
|
||||
|
||||
return {
|
||||
activityTargets: activityTargets as ActivityTarget[],
|
||||
activityTargets,
|
||||
loadingActivityTargets,
|
||||
initialized,
|
||||
};
|
||||
|
@ -1,91 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { StringKeyOf } from 'type-fest';
|
||||
|
||||
import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition';
|
||||
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useAttachRelationInBothDirections = () => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const attachRelationInBothDirections = <
|
||||
Source extends ObjectRecord = ObjectRecord,
|
||||
Target extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
sourceRecord,
|
||||
targetRecords,
|
||||
sourceObjectNameSingular,
|
||||
targetObjectNameSingular,
|
||||
fieldNameOnSourceRecord,
|
||||
fieldNameOnTargetRecord,
|
||||
}: {
|
||||
sourceRecord: Source;
|
||||
targetRecords: Target[];
|
||||
sourceObjectNameSingular: string;
|
||||
targetObjectNameSingular: string;
|
||||
fieldNameOnSourceRecord: StringKeyOf<Source>;
|
||||
fieldNameOnTargetRecord: StringKeyOf<Target>;
|
||||
}) => {
|
||||
const sourceObjectMetadataItem = getObjectMetadataItemByNameSingular({
|
||||
objectMetadataItems,
|
||||
objectNameSingular: sourceObjectNameSingular,
|
||||
});
|
||||
|
||||
const targetObjectMetadataItem = getObjectMetadataItemByNameSingular({
|
||||
objectMetadataItems,
|
||||
objectNameSingular: targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const fieldMetadataItemOnSourceRecord =
|
||||
sourceObjectMetadataItem.fields.find(
|
||||
(field) => field.name === fieldNameOnSourceRecord,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataItemOnSourceRecord)) {
|
||||
throw new Error(
|
||||
`Field ${fieldNameOnSourceRecord} not found on object ${sourceObjectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationDefinition = getRelationDefinition({
|
||||
fieldMetadataItemOnSourceRecord: fieldMetadataItemOnSourceRecord,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
if (!isDefined(relationDefinition)) {
|
||||
throw new Error(
|
||||
`Relation metadata not found for field ${fieldNameOnSourceRecord} on object ${sourceObjectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: could we use triggerUpdateRelationsOptimisticEffect here?
|
||||
targetRecords.forEach((relationTargetRecord) => {
|
||||
triggerAttachRelationOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: sourceRecord.id,
|
||||
fieldNameOnTargetRecord: fieldNameOnTargetRecord,
|
||||
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
|
||||
targetRecordId: relationTargetRecord.id,
|
||||
});
|
||||
|
||||
triggerAttachRelationOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
sourceObjectNameSingular: targetObjectMetadataItem.nameSingular,
|
||||
sourceRecordId: relationTargetRecord.id,
|
||||
fieldNameOnTargetRecord: fieldNameOnSourceRecord,
|
||||
targetObjectNameSingular: sourceObjectMetadataItem.nameSingular,
|
||||
targetRecordId: sourceRecord.id,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
attachRelationInBothDirections,
|
||||
};
|
||||
};
|
@ -1,20 +1,24 @@
|
||||
import { Reference, useApolloClient } from '@apollo/client';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections';
|
||||
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
|
||||
import { Activity, ActivityType } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { makeActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache';
|
||||
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const useCreateActivityInCache = () => {
|
||||
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
|
||||
@ -22,11 +26,9 @@ export const useCreateActivityInCache = () => {
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const { createOneRecordInCache: createOneActivityInCache } =
|
||||
useCreateOneRecordInCache<Activity>({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
const cache = useApolloClient().cache;
|
||||
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { record: currentWorkspaceMemberRecord } = useFindOneRecord({
|
||||
@ -35,27 +37,37 @@ export const useCreateActivityInCache = () => {
|
||||
depth: 0,
|
||||
});
|
||||
|
||||
const { injectIntoActivityTargetInlineCellCache } =
|
||||
useInjectIntoActivityTargetInlineCellCache();
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const { attachRelationInBothDirections } =
|
||||
useAttachRelationInBothDirections();
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const createOneActivityInCache = useCreateOneRecordInCache<Activity>({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const createActivityInCache = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
type,
|
||||
targetableObjects,
|
||||
targetObject,
|
||||
customAssignee,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
targetObject?: ActivityTargetableObject;
|
||||
customAssignee?: WorkspaceMember;
|
||||
}) => {
|
||||
const activityId = v4();
|
||||
|
||||
const createdActivityInCache = createOneActivityInCache({
|
||||
id: activityId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: currentWorkspaceMemberRecord,
|
||||
authorId: currentWorkspaceMemberRecord?.id,
|
||||
assignee: customAssignee ?? currentWorkspaceMemberRecord,
|
||||
@ -63,42 +75,89 @@ export const useCreateActivityInCache = () => {
|
||||
type,
|
||||
});
|
||||
|
||||
const targetObjectRecords = targetableObjects
|
||||
.map((targetableObject) => {
|
||||
const targetObject = snapshot
|
||||
.getLoadable(recordStoreFamilyState(targetableObject.id))
|
||||
if (isUndefinedOrNull(createdActivityInCache)) {
|
||||
throw new Error('Failed to create activity in cache');
|
||||
}
|
||||
|
||||
if (isUndefinedOrNull(targetObject)) {
|
||||
set(recordStoreFamilyState(activityId), {
|
||||
...createdActivityInCache,
|
||||
activityTargets: [],
|
||||
comments: [],
|
||||
});
|
||||
|
||||
return {
|
||||
createdActivityInCache: {
|
||||
...createdActivityInCache,
|
||||
activityTargets: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const targetObjectRecord = snapshot
|
||||
.getLoadable(recordStoreFamilyState(targetObject.id))
|
||||
.getValue();
|
||||
|
||||
return targetObject;
|
||||
})
|
||||
.filter(isDefined);
|
||||
if (isUndefinedOrNull(targetObjectRecord)) {
|
||||
throw new Error('Failed to find target object record');
|
||||
}
|
||||
|
||||
const activityTargetsToCreate =
|
||||
makeActivityTargetsToCreateFromTargetableObjects({
|
||||
activityId,
|
||||
targetableObjects,
|
||||
targetObjectRecords,
|
||||
activity: createdActivityInCache,
|
||||
targetableObjects: [targetObject],
|
||||
targetObjectRecords: [targetObjectRecord],
|
||||
});
|
||||
|
||||
const createdActivityTargetsInCache = createManyActivityTargetsInCache(
|
||||
activityTargetsToCreate,
|
||||
);
|
||||
|
||||
injectIntoActivityTargetInlineCellCache({
|
||||
activityId,
|
||||
activityTargetsToInject: createdActivityTargetsInCache,
|
||||
const activityTargetsConnection = getRecordConnectionFromRecords({
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
records: createdActivityTargetsInCache,
|
||||
withPageInfo: false,
|
||||
computeReferences: true,
|
||||
isRootLevel: false,
|
||||
});
|
||||
|
||||
attachRelationInBothDirections({
|
||||
sourceRecord: createdActivityInCache,
|
||||
fieldNameOnSourceRecord: 'activityTargets',
|
||||
sourceObjectNameSingular: CoreObjectNameSingular.Activity,
|
||||
fieldNameOnTargetRecord: 'activity',
|
||||
targetObjectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
targetRecords: createdActivityTargetsInCache,
|
||||
modifyRecordFromCache({
|
||||
recordId: createdActivityInCache.id,
|
||||
cache,
|
||||
fieldModifiers: {
|
||||
activityTargets: () => activityTargetsConnection,
|
||||
},
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
// TODO: should refactor when refactoring make activity connection utils
|
||||
const targetObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === targetObject.targetObjectNameSingular,
|
||||
);
|
||||
|
||||
if (isDefined(targetObjectMetadataItem)) {
|
||||
modifyRecordFromCache({
|
||||
cache,
|
||||
objectMetadataItem: targetObjectMetadataItem,
|
||||
recordId: targetObject.id,
|
||||
fieldModifiers: {
|
||||
activityTargets: (activityTargetsRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
activityTargetsRef,
|
||||
);
|
||||
|
||||
if (!edges) return activityTargetsRef;
|
||||
|
||||
return {
|
||||
...activityTargetsRef,
|
||||
edges: [...edges, ...activityTargetsConnection.edges],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
set(recordStoreFamilyState(activityId), {
|
||||
...createdActivityInCache,
|
||||
activityTargets: createdActivityTargetsInCache,
|
||||
@ -110,15 +169,16 @@ export const useCreateActivityInCache = () => {
|
||||
...createdActivityInCache,
|
||||
activityTargets: createdActivityTargetsInCache,
|
||||
},
|
||||
createdActivityTargetsInCache,
|
||||
};
|
||||
},
|
||||
[
|
||||
attachRelationInBothDirections,
|
||||
createManyActivityTargetsInCache,
|
||||
createOneActivityInCache,
|
||||
currentWorkspaceMemberRecord,
|
||||
injectIntoActivityTargetInlineCellCache,
|
||||
createManyActivityTargetsInCache,
|
||||
objectMetadataItems,
|
||||
objectMetadataItemActivityTarget,
|
||||
cache,
|
||||
objectMetadataItemActivity,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { CREATE_ONE_ACTIVITY_QUERY_KEY } from '@/activities/query-keys/CreateOneActivityQueryKey';
|
||||
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
@ -9,37 +9,27 @@ import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
|
||||
export const useCreateActivityInDB = () => {
|
||||
const { createOneRecord: createOneActivity } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectNameSingular: CREATE_ONE_ACTIVITY_QUERY_KEY.objectNameSingular,
|
||||
queryFields: CREATE_ONE_ACTIVITY_QUERY_KEY.fields,
|
||||
depth: CREATE_ONE_ACTIVITY_QUERY_KEY.depth,
|
||||
});
|
||||
|
||||
const { createManyRecords: createManyActivityTargets } =
|
||||
useCreateManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
skipPostOptmisticEffect: true,
|
||||
});
|
||||
|
||||
const { makeActivityWithConnection } = useActivityConnectionUtils();
|
||||
|
||||
const createActivityInDB = async (activityToCreate: ActivityForEditor) => {
|
||||
const { activityWithConnection } = makeActivityWithConnection(
|
||||
activityToCreate as any, // TODO: fix type
|
||||
);
|
||||
|
||||
await createOneActivity?.(
|
||||
{
|
||||
...activityWithConnection,
|
||||
await createOneActivity?.({
|
||||
...activityToCreate,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
skipOptimisticEffect: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const activityTargetsToCreate = activityToCreate.activityTargets ?? [];
|
||||
|
||||
if (isNonEmptyArray(activityTargetsToCreate)) {
|
||||
await createManyActivityTargets(activityTargetsToCreate, {
|
||||
skipOptimisticEffect: true,
|
||||
});
|
||||
await createManyActivityTargets(activityTargetsToCreate);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,39 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
|
||||
// TODO: this should be useDeleteRecordFromCache
|
||||
export const useDeleteActivityFromCache = () => {
|
||||
const { makeActivityWithConnection } = useActivityConnectionUtils();
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const deleteActivityFromCache = (activityToDelete: ActivityForEditor) => {
|
||||
const { activityWithConnection } = makeActivityWithConnection(
|
||||
activityToDelete as any, // TODO: fix type
|
||||
);
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
objectMetadataItems,
|
||||
recordsToDelete: [activityWithConnection],
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
deleteActivityFromCache,
|
||||
};
|
||||
};
|
@ -1,175 +0,0 @@
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
|
||||
// TODO: create a generic hook from this
|
||||
export const useInjectIntoActivitiesQueries = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const {
|
||||
upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache,
|
||||
} = useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache,
|
||||
} = useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache,
|
||||
} = useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const injectActivitiesQueries = ({
|
||||
activityToInject,
|
||||
activityTargetsToInject,
|
||||
targetableObjects,
|
||||
activitiesFilters,
|
||||
activitiesOrderByVariables,
|
||||
injectOnlyInIdFilter,
|
||||
}: {
|
||||
activityToInject: Activity;
|
||||
activityTargetsToInject: ActivityTarget[];
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
activitiesFilters?: ObjectRecordQueryFilter;
|
||||
activitiesOrderByVariables?: OrderByField;
|
||||
injectOnlyInIdFilter?: boolean;
|
||||
}) => {
|
||||
const hasActivityTargets = isNonEmptyArray(targetableObjects);
|
||||
|
||||
if (hasActivityTargets) {
|
||||
const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({
|
||||
targetableObjects,
|
||||
});
|
||||
|
||||
const findManyActivitiyTargetsQueryVariables = {
|
||||
filter: findManyActivitiyTargetsQueryFilter,
|
||||
};
|
||||
|
||||
const existingActivityTargetsWithMaybeDuplicates =
|
||||
readFindManyActivityTargetsQueryInCache({
|
||||
queryVariables: findManyActivitiyTargetsQueryVariables,
|
||||
});
|
||||
|
||||
const existingActivityTargetsWithoutDuplicates: ObjectRecord[] =
|
||||
existingActivityTargetsWithMaybeDuplicates.filter(
|
||||
(existingActivityTarget) =>
|
||||
!activityTargetsToInject.some(
|
||||
(activityTargetToInject) =>
|
||||
activityTargetToInject.id === existingActivityTarget.id,
|
||||
),
|
||||
);
|
||||
|
||||
const existingActivityIdsFromTargets =
|
||||
existingActivityTargetsWithoutDuplicates
|
||||
?.map((activityTarget) => activityTarget.activityId)
|
||||
.filter(isNonEmptyString);
|
||||
|
||||
const currentFindManyActivitiesQueryVariables = {
|
||||
filter: {
|
||||
id: {
|
||||
in: [...existingActivityIdsFromTargets].sort(sortByAscString),
|
||||
},
|
||||
...activitiesFilters,
|
||||
},
|
||||
orderBy: activitiesOrderByVariables,
|
||||
};
|
||||
|
||||
const existingActivities = readFindManyActivitiesQueryInCache({
|
||||
queryVariables: currentFindManyActivitiesQueryVariables,
|
||||
});
|
||||
|
||||
const nextActivityIds = [
|
||||
...existingActivityIdsFromTargets,
|
||||
activityToInject.id,
|
||||
];
|
||||
|
||||
const nextFindManyActivitiesQueryVariables = {
|
||||
filter: {
|
||||
id: {
|
||||
in: [...nextActivityIds].sort(sortByAscString),
|
||||
},
|
||||
...activitiesFilters,
|
||||
},
|
||||
orderBy: activitiesOrderByVariables,
|
||||
};
|
||||
|
||||
const newActivities = [...existingActivities];
|
||||
|
||||
if (!injectOnlyInIdFilter) {
|
||||
const newActivity = {
|
||||
...activityToInject,
|
||||
__typename: 'Activity',
|
||||
};
|
||||
|
||||
newActivities.unshift(newActivity);
|
||||
}
|
||||
|
||||
overwriteFindManyActivitiesInCache({
|
||||
objectRecordsToOverwrite: newActivities,
|
||||
queryVariables: nextFindManyActivitiesQueryVariables,
|
||||
});
|
||||
} else {
|
||||
const currentFindManyActivitiesQueryVariables = {
|
||||
filter: {
|
||||
...activitiesFilters,
|
||||
},
|
||||
orderBy: activitiesOrderByVariables,
|
||||
};
|
||||
|
||||
const existingActivities = readFindManyActivitiesQueryInCache({
|
||||
queryVariables: currentFindManyActivitiesQueryVariables,
|
||||
});
|
||||
|
||||
const nextFindManyActivitiesQueryVariables = {
|
||||
filter: {
|
||||
...activitiesFilters,
|
||||
},
|
||||
orderBy: activitiesOrderByVariables,
|
||||
};
|
||||
|
||||
const newActivities = [...existingActivities];
|
||||
|
||||
if (!injectOnlyInIdFilter) {
|
||||
const newActivity = {
|
||||
...activityToInject,
|
||||
__typename: 'Activity',
|
||||
};
|
||||
|
||||
newActivities.unshift(newActivity);
|
||||
}
|
||||
|
||||
overwriteFindManyActivitiesInCache({
|
||||
objectRecordsToOverwrite: newActivities,
|
||||
queryVariables: nextFindManyActivitiesQueryVariables,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
injectActivitiesQueries,
|
||||
};
|
||||
};
|
@ -1,82 +0,0 @@
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
// TODO: create a generic hook from this
|
||||
export const useInjectIntoActivityTargetsQueries = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache,
|
||||
} = useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
upsertFindManyRecordsQueryInCache:
|
||||
overwriteFindManyActivityTargetsQueryInCache,
|
||||
} = useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const injectActivityTargetsQueries = ({
|
||||
activityTargetsToInject,
|
||||
targetableObjects,
|
||||
}: {
|
||||
activityTargetsToInject: ActivityTarget[];
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
}) => {
|
||||
const hasActivityTargets = isNonEmptyArray(targetableObjects);
|
||||
|
||||
if (!hasActivityTargets) {
|
||||
return;
|
||||
}
|
||||
|
||||
const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({
|
||||
targetableObjects,
|
||||
});
|
||||
|
||||
const findManyActivitiyTargetsQueryVariables = {
|
||||
filter: findManyActivitiyTargetsQueryFilter,
|
||||
};
|
||||
|
||||
const existingActivityTargetsWithMaybeDuplicates =
|
||||
readFindManyActivityTargetsQueryInCache({
|
||||
queryVariables: findManyActivitiyTargetsQueryVariables,
|
||||
});
|
||||
|
||||
const existingActivityTargetsWithoutDuplicates: ObjectRecord[] =
|
||||
existingActivityTargetsWithMaybeDuplicates.filter(
|
||||
(existingActivityTarget) =>
|
||||
!activityTargetsToInject.some(
|
||||
(activityTargetToInject) =>
|
||||
activityTargetToInject.id === existingActivityTarget.id,
|
||||
),
|
||||
);
|
||||
|
||||
const newActivityTargets = [
|
||||
...existingActivityTargetsWithoutDuplicates,
|
||||
...activityTargetsToInject,
|
||||
];
|
||||
|
||||
overwriteFindManyActivityTargetsQueryInCache({
|
||||
objectRecordsToOverwrite: newActivityTargets,
|
||||
queryVariables: findManyActivitiyTargetsQueryVariables,
|
||||
depth: 2,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
injectActivityTargetsQueries,
|
||||
};
|
||||
};
|
@ -1,46 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { getCacheReferenceFromRecord } from '@/object-record/cache/utils/getCacheReferenceFromRecord';
|
||||
|
||||
export const useModifyActivityOnActivityTargetsCache = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const modifyActivityTargetFromCache = useModifyRecordFromCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const modifyActivityOnActivityTargetsCache = ({
|
||||
activityTargetIds,
|
||||
activity,
|
||||
}: {
|
||||
activityTargetIds: string[];
|
||||
activity: Activity;
|
||||
}) => {
|
||||
for (const activityTargetId of activityTargetIds) {
|
||||
modifyActivityTargetFromCache(activityTargetId, {
|
||||
activity: () => {
|
||||
const newActivityReference = getCacheReferenceFromRecord({
|
||||
apolloClient,
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
record: activity,
|
||||
});
|
||||
|
||||
return newActivityReference;
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
modifyActivityOnActivityTargetsCache,
|
||||
};
|
||||
};
|
@ -1,51 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { CachedObjectRecordConnection } from '@/apollo/types/CachedObjectRecordConnection';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { getCachedRecordEdgesFromRecords } from '@/object-record/cache/utils/getCachedRecordEdgesFromRecords';
|
||||
|
||||
export const useModifyActivityTargetsOnActivityCache = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const modifyActivityFromCache = useModifyRecordFromCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const modifyActivityTargetsOnActivityCache = ({
|
||||
activityId,
|
||||
activityTargets,
|
||||
}: {
|
||||
activityId: string;
|
||||
activityTargets: ActivityTarget[];
|
||||
}) => {
|
||||
modifyActivityFromCache(activityId, {
|
||||
activityTargets: (
|
||||
activityTargetsCachedConnection: CachedObjectRecordConnection,
|
||||
) => {
|
||||
const newActivityTargetsCachedRecordEdges =
|
||||
getCachedRecordEdgesFromRecords({
|
||||
apolloClient,
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
records: activityTargets,
|
||||
});
|
||||
|
||||
return {
|
||||
...activityTargetsCachedConnection,
|
||||
edges: newActivityTargetsCachedRecordEdges,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
modifyActivityTargetsOnActivityCache,
|
||||
};
|
||||
};
|
@ -51,7 +51,7 @@ export const useOpenCreateActivityDrawer = () => {
|
||||
}) => {
|
||||
const { createdActivityInCache } = createActivityInCache({
|
||||
type,
|
||||
targetableObjects,
|
||||
targetObject: targetableObjects[0],
|
||||
customAssignee,
|
||||
});
|
||||
|
||||
|
@ -1,64 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityType } from '@/activities/types/Activity';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||
|
||||
export const useOpenCreateActivityDrawerForSelectedRowIds = (
|
||||
recordTableId: string,
|
||||
) => {
|
||||
const openCreateActivityDrawer = useOpenCreateActivityDrawer();
|
||||
|
||||
const { selectedRowIdsSelector } = useRecordTableStates(recordTableId);
|
||||
|
||||
return useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(
|
||||
type: ActivityType,
|
||||
objectNameSingular: string,
|
||||
relatedEntities?: ActivityTargetableObject[],
|
||||
) => {
|
||||
const selectedRowIds = getSnapshotValue(
|
||||
snapshot,
|
||||
selectedRowIdsSelector(),
|
||||
);
|
||||
|
||||
let activityTargetableObjectArray: ActivityTargetableObject[] =
|
||||
selectedRowIds
|
||||
.map((recordId: string) => {
|
||||
const targetObjectRecord = getSnapshotValue(
|
||||
snapshot,
|
||||
recordStoreFamilyState(recordId),
|
||||
);
|
||||
|
||||
if (!targetObjectRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Custom',
|
||||
targetObjectNameSingular: objectNameSingular,
|
||||
id: recordId,
|
||||
targetObjectRecord,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (isDefined(relatedEntities)) {
|
||||
activityTargetableObjectArray =
|
||||
activityTargetableObjectArray.concat(relatedEntities);
|
||||
}
|
||||
|
||||
openCreateActivityDrawer({
|
||||
type,
|
||||
targetableObjects: activityTargetableObjectArray,
|
||||
});
|
||||
},
|
||||
[selectedRowIdsSelector, openCreateActivityDrawer],
|
||||
);
|
||||
};
|
@ -0,0 +1,123 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { FIND_MANY_ACTIVITIES_QUERY_KEY } from '@/activities/query-keys/FindManyActivitiesQueryKey';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const usePrepareFindManyActivitiesQuery = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const getActivityFromCache = useGetRecordFromCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } =
|
||||
useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const prepareFindManyActivitiesQuery = ({
|
||||
targetableObject,
|
||||
additionalFilter,
|
||||
shouldActivityBeExcluded,
|
||||
}: {
|
||||
additionalFilter?: Record<string, unknown>;
|
||||
targetableObject: ActivityTargetableObject;
|
||||
shouldActivityBeExcluded?: (activityTarget: Activity) => boolean;
|
||||
}) => {
|
||||
const targetableObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular ===
|
||||
targetableObject.targetObjectNameSingular,
|
||||
);
|
||||
|
||||
if (!targetableObjectMetadataItem) {
|
||||
throw new Error(
|
||||
`Cannot find object metadata item for targetable object ${targetableObject.targetObjectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetableObjectRecord = getRecordFromCache<ObjectRecord>({
|
||||
recordId: targetableObject.id,
|
||||
objectMetadataItem: targetableObjectMetadataItem,
|
||||
objectMetadataItems,
|
||||
cache,
|
||||
});
|
||||
|
||||
const activityTargets: ActivityTarget[] =
|
||||
targetableObjectRecord?.activityTargets ?? [];
|
||||
|
||||
const activityTargetIds = [
|
||||
...new Set(
|
||||
activityTargets
|
||||
.map((activityTarget) => activityTarget.id)
|
||||
.filter(isDefined),
|
||||
),
|
||||
];
|
||||
|
||||
const activities: Activity[] = activityTargetIds
|
||||
.map((activityTargetId) => {
|
||||
const activityTarget = activityTargets.find(
|
||||
(activityTarget) => activityTarget.id === activityTargetId,
|
||||
);
|
||||
|
||||
if (!activityTarget) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getActivityFromCache<Activity>(activityTarget.activityId);
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const activityIds = [...new Set(activities.map((activity) => activity.id))];
|
||||
|
||||
const nextFindManyActivitiesQueryFilter = {
|
||||
filter: {
|
||||
id: {
|
||||
in: [...activityIds].sort(sortByAscString),
|
||||
},
|
||||
...additionalFilter,
|
||||
},
|
||||
};
|
||||
|
||||
const filteredActivities = [
|
||||
...activities.filter(
|
||||
(activity) => !shouldActivityBeExcluded?.(activity) ?? true,
|
||||
),
|
||||
].sort((a, b) => {
|
||||
return a.createdAt > b.createdAt ? -1 : 1;
|
||||
});
|
||||
|
||||
upsertFindManyActivitiesInCache({
|
||||
objectRecordsToOverwrite: filteredActivities,
|
||||
queryVariables: {
|
||||
...nextFindManyActivitiesQueryFilter,
|
||||
orderBy: { createdAt: 'DescNullsFirst' },
|
||||
},
|
||||
depth: FIND_MANY_ACTIVITIES_QUERY_KEY.depth,
|
||||
queryFields:
|
||||
FIND_MANY_ACTIVITIES_QUERY_KEY.fieldsFactory?.(objectMetadataItems),
|
||||
computeReferences: true,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
prepareFindManyActivitiesQuery,
|
||||
};
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { usePrepareFindManyActivitiesQuery } from '@/activities/hooks/usePrepareFindManyActivitiesQuery';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// This hook should only be executed if the normalized cache is up-to-date
|
||||
// It will take a targetableObject and prepare the queries for the activities
|
||||
// based on the activityTargets of the targetableObject
|
||||
export const useRefreshShowPageFindManyActivitiesQueries = () => {
|
||||
const objectShowPageTargetableObject = useRecoilValue(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
const { prepareFindManyActivitiesQuery } =
|
||||
usePrepareFindManyActivitiesQuery();
|
||||
|
||||
const refreshShowPageFindManyActivitiesQueries = () => {
|
||||
if (isDefined(objectShowPageTargetableObject)) {
|
||||
prepareFindManyActivitiesQuery({
|
||||
targetableObject: objectShowPageTargetableObject,
|
||||
});
|
||||
prepareFindManyActivitiesQuery({
|
||||
targetableObject: objectShowPageTargetableObject,
|
||||
additionalFilter: {
|
||||
completedAt: { is: 'NULL' },
|
||||
type: { eq: 'Task' },
|
||||
},
|
||||
shouldActivityBeExcluded: (activity: Activity) => {
|
||||
return activity.type !== 'Task';
|
||||
},
|
||||
});
|
||||
prepareFindManyActivitiesQuery({
|
||||
targetableObject: objectShowPageTargetableObject,
|
||||
additionalFilter: {
|
||||
type: { eq: 'Note' },
|
||||
},
|
||||
shouldActivityBeExcluded: (activity: Activity) => {
|
||||
return activity.type !== 'Note';
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
refreshShowPageFindManyActivitiesQueries,
|
||||
};
|
||||
};
|
@ -1,117 +0,0 @@
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
|
||||
// TODO: improve, no bug if query to inject doesn't exist
|
||||
export const useRemoveFromActivitiesQueries = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const {
|
||||
upsertFindManyRecordsQueryInCache: overwriteFindManyActivitiesInCache,
|
||||
} = useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache,
|
||||
} = useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
readFindManyRecordsQueryInCache: readFindManyActivitiesQueryInCache,
|
||||
} = useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const removeFromActivitiesQueries = ({
|
||||
activityIdToRemove,
|
||||
targetableObjects,
|
||||
activitiesFilters,
|
||||
activitiesOrderByVariables,
|
||||
}: {
|
||||
activityIdToRemove: string;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
activitiesFilters?: ObjectRecordQueryFilter;
|
||||
activitiesOrderByVariables?: OrderByField;
|
||||
}) => {
|
||||
const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({
|
||||
targetableObjects,
|
||||
});
|
||||
|
||||
const findManyActivityTargetsQueryVariables = {
|
||||
filter: findManyActivitiyTargetsQueryFilter,
|
||||
} as ObjectRecordQueryVariables;
|
||||
|
||||
const existingActivityTargetsForTargetableObject =
|
||||
readFindManyActivityTargetsQueryInCache({
|
||||
queryVariables: findManyActivityTargetsQueryVariables,
|
||||
});
|
||||
|
||||
const existingActivityIds = existingActivityTargetsForTargetableObject
|
||||
?.map((activityTarget) => activityTarget.activityId)
|
||||
.filter(isNonEmptyString);
|
||||
|
||||
const currentFindManyActivitiesQueryVariables = {
|
||||
filter: {
|
||||
id: {
|
||||
in: [...existingActivityIds].sort(sortByAscString),
|
||||
},
|
||||
...activitiesFilters,
|
||||
},
|
||||
orderBy: activitiesOrderByVariables,
|
||||
};
|
||||
|
||||
const existingActivities = readFindManyActivitiesQueryInCache({
|
||||
queryVariables: currentFindManyActivitiesQueryVariables,
|
||||
});
|
||||
|
||||
if (!isNonEmptyArray(existingActivities)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activityIdsAfterRemoval = existingActivityIds.filter(
|
||||
(existingActivityId) => existingActivityId !== activityIdToRemove,
|
||||
);
|
||||
|
||||
const nextFindManyActivitiesQueryVariables = {
|
||||
filter: {
|
||||
id: {
|
||||
in: [...activityIdsAfterRemoval].sort(sortByAscString),
|
||||
},
|
||||
...activitiesFilters,
|
||||
},
|
||||
orderBy: activitiesOrderByVariables,
|
||||
};
|
||||
|
||||
const newActivities = existingActivities.filter(
|
||||
(existingActivity) => existingActivity.id !== activityIdToRemove,
|
||||
);
|
||||
|
||||
overwriteFindManyActivitiesInCache({
|
||||
objectRecordsToOverwrite: newActivities,
|
||||
queryVariables: nextFindManyActivitiesQueryVariables,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
removeFromActivitiesQueries,
|
||||
};
|
||||
};
|
@ -1,73 +0,0 @@
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
|
||||
export const useRemoveFromActivityTargetsQueries = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
readFindManyRecordsQueryInCache: readFindManyActivityTargetsQueryInCache,
|
||||
} = useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
upsertFindManyRecordsQueryInCache:
|
||||
overwriteFindManyActivityTargetsQueryInCache,
|
||||
} = useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const removeFromActivityTargetsQueries = ({
|
||||
activityTargetsToRemove,
|
||||
targetableObjects,
|
||||
}: {
|
||||
activityTargetsToRemove: ActivityTarget[];
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
}) => {
|
||||
const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({
|
||||
targetableObjects,
|
||||
});
|
||||
|
||||
const findManyActivityTargetsQueryVariables = {
|
||||
filter: findManyActivitiyTargetsQueryFilter,
|
||||
} as ObjectRecordQueryVariables;
|
||||
|
||||
const existingActivityTargetsForTargetableObject =
|
||||
readFindManyActivityTargetsQueryInCache({
|
||||
queryVariables: findManyActivityTargetsQueryVariables,
|
||||
});
|
||||
|
||||
const newActivityTargetsForTargetableObject = isNonEmptyArray(
|
||||
activityTargetsToRemove,
|
||||
)
|
||||
? existingActivityTargetsForTargetableObject.filter(
|
||||
(existingActivityTarget) =>
|
||||
activityTargetsToRemove.some(
|
||||
(activityTargetToRemove) =>
|
||||
activityTargetToRemove.id !== existingActivityTarget.id,
|
||||
),
|
||||
)
|
||||
: existingActivityTargetsForTargetableObject;
|
||||
|
||||
overwriteFindManyActivityTargetsQueryInCache({
|
||||
objectRecordsToOverwrite: newActivityTargetsForTargetableObject,
|
||||
queryVariables: findManyActivityTargetsQueryVariables,
|
||||
depth: 2,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
removeFromActivityTargetsQueries,
|
||||
};
|
||||
};
|
@ -1,24 +1,16 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
|
||||
import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries';
|
||||
import { useInjectIntoActivityTargetsQueries } from '@/activities/hooks/useInjectIntoActivityTargetsQueries';
|
||||
import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState';
|
||||
import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
|
||||
import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState';
|
||||
import { useInjectIntoTimelineActivitiesQueries } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// TODO: create a generic way to have records only in cache for create mode and delete them afterwards ?
|
||||
export const useUpsertActivity = () => {
|
||||
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
|
||||
isActivityInCreateModeState,
|
||||
@ -40,31 +32,8 @@ export const useUpsertActivity = () => {
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
const { injectActivitiesQueries } = useInjectIntoActivitiesQueries();
|
||||
const { injectActivityTargetsQueries } =
|
||||
useInjectIntoActivityTargetsQueries();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const weAreOnObjectShowPage = pathname.startsWith('/object');
|
||||
const weAreOnTaskPage = pathname.startsWith('/tasks');
|
||||
|
||||
const { injectIntoTimelineActivitiesQueries } =
|
||||
useInjectIntoTimelineActivitiesQueries();
|
||||
|
||||
const { makeActivityWithConnection } = useActivityConnectionUtils();
|
||||
|
||||
const currentCompletedTaskQueryVariables = useRecoilValue(
|
||||
currentCompletedTaskQueryVariablesState,
|
||||
);
|
||||
|
||||
const currentIncompleteTaskQueryVariables = useRecoilValue(
|
||||
currentIncompleteTaskQueryVariablesState,
|
||||
);
|
||||
|
||||
const currentNotesQueryVariables = useRecoilValue(
|
||||
currentNotesQueryVariablesState,
|
||||
);
|
||||
const { refreshShowPageFindManyActivitiesQueries } =
|
||||
useRefreshShowPageFindManyActivitiesQueries();
|
||||
|
||||
const upsertActivity = async ({
|
||||
activity,
|
||||
@ -74,103 +43,19 @@ export const useUpsertActivity = () => {
|
||||
input: Partial<Activity>;
|
||||
}) => {
|
||||
setIsUpsertingActivityInDB(true);
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
const activityToCreate: Activity = {
|
||||
...activity,
|
||||
...input,
|
||||
};
|
||||
|
||||
const { activityWithConnection } =
|
||||
makeActivityWithConnection(activityToCreate);
|
||||
|
||||
if (weAreOnTaskPage) {
|
||||
if (isDefined(activityWithConnection.completedAt)) {
|
||||
injectActivitiesQueries({
|
||||
activitiesFilters: currentCompletedTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentCompletedTaskQueryVariables?.orderBy,
|
||||
activityTargetsToInject: activityToCreate.activityTargets,
|
||||
activityToInject: activityWithConnection,
|
||||
targetableObjects: [],
|
||||
});
|
||||
} else {
|
||||
injectActivitiesQueries({
|
||||
activitiesFilters: currentIncompleteTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentIncompleteTaskQueryVariables?.orderBy,
|
||||
activityTargetsToInject: activityToCreate.activityTargets,
|
||||
activityToInject: activityWithConnection,
|
||||
targetableObjects: [],
|
||||
});
|
||||
}
|
||||
|
||||
injectActivityTargetsQueries({
|
||||
activityTargetsToInject: activityToCreate.activityTargets,
|
||||
targetableObjects: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Call optimistic effects
|
||||
if (weAreOnObjectShowPage && isDefined(objectShowPageTargetableObject)) {
|
||||
injectIntoTimelineActivitiesQueries({
|
||||
timelineTargetableObject: objectShowPageTargetableObject,
|
||||
activityToInject: activityWithConnection,
|
||||
activityTargetsToInject: activityToCreate.activityTargets,
|
||||
});
|
||||
|
||||
const injectOnlyInIdFilterForTaskQueries =
|
||||
activityWithConnection.type !== 'Task';
|
||||
|
||||
const injectOnlyInIdFilterForNotesQueries =
|
||||
activityWithConnection.type !== 'Note';
|
||||
|
||||
if (isDefined(currentCompletedTaskQueryVariables)) {
|
||||
injectActivitiesQueries({
|
||||
activitiesFilters: currentCompletedTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentCompletedTaskQueryVariables?.orderBy,
|
||||
activityTargetsToInject: activityToCreate.activityTargets,
|
||||
activityToInject: activityWithConnection,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
injectOnlyInIdFilter: injectOnlyInIdFilterForTaskQueries,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(currentIncompleteTaskQueryVariables)) {
|
||||
injectActivitiesQueries({
|
||||
activitiesFilters:
|
||||
currentIncompleteTaskQueryVariables?.filter ?? {},
|
||||
activitiesOrderByVariables:
|
||||
currentIncompleteTaskQueryVariables?.orderBy ?? {},
|
||||
activityTargetsToInject: activityToCreate.activityTargets,
|
||||
activityToInject: activityWithConnection,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
injectOnlyInIdFilter: injectOnlyInIdFilterForTaskQueries,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(currentNotesQueryVariables)) {
|
||||
injectActivitiesQueries({
|
||||
activitiesFilters: currentNotesQueryVariables?.filter,
|
||||
activitiesOrderByVariables: currentNotesQueryVariables?.orderBy,
|
||||
activityTargetsToInject: activityToCreate.activityTargets,
|
||||
activityToInject: activityWithConnection,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
injectOnlyInIdFilter: injectOnlyInIdFilterForNotesQueries,
|
||||
});
|
||||
}
|
||||
|
||||
injectActivityTargetsQueries({
|
||||
activityTargetsToInject: activityToCreate.activityTargets,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
});
|
||||
if (isDefined(objectShowPageTargetableObject)) {
|
||||
refreshShowPageFindManyActivitiesQueries();
|
||||
}
|
||||
|
||||
await createActivityInDB(activityToCreate);
|
||||
|
||||
setActivityIdInDrawer(activityToCreate.id);
|
||||
|
||||
setIsActivityInCreateMode(false);
|
||||
} else {
|
||||
await updateOneActivity?.({
|
||||
|
@ -1,23 +1,25 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isNonEmptyArray, isNull } from '@sniptt/guards';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { getActivityTargetObjectFieldName } from '@/activities/utils/getActivityTargetObjectFieldName';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGenerateObjectRecordOptimisticResponse } from '@/object-record/cache/hooks/useGenerateObjectRecordOptimisticResponse';
|
||||
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { MultipleObjectRecordSelect } from '@/object-record/relation-picker/components/MultipleObjectRecordSelect';
|
||||
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
|
||||
const StyledSelectContainer = styled.div`
|
||||
left: 0px;
|
||||
@ -38,7 +40,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
|
||||
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
|
||||
(activityTarget) => ({
|
||||
objectNameSingular: activityTarget.targetObjectNameSingular,
|
||||
objectNameSingular: activityTarget.targetObjectMetadataItem.nameSingular,
|
||||
id: activityTarget.targetObject.id,
|
||||
}),
|
||||
);
|
||||
@ -63,12 +65,13 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const { injectIntoActivityTargetInlineCellCache } =
|
||||
useInjectIntoActivityTargetInlineCellCache();
|
||||
const setActivityFromStore = useSetRecoilState(
|
||||
recordStoreFamilyState(activity.id),
|
||||
);
|
||||
|
||||
const { generateObjectRecordOptimisticResponse } =
|
||||
useGenerateObjectRecordOptimisticResponse({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
|
||||
useCreateManyRecordsInCache<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
|
||||
@ -100,16 +103,21 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
|
||||
const activityTargetsToCreate = selectedTargetObjectsToCreate.map(
|
||||
(selectedRecord) => {
|
||||
const emptyActivityTarget =
|
||||
generateObjectRecordOptimisticResponse<ActivityTarget>({
|
||||
const emptyActivityTarget = prefillRecord<ActivityTarget>({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
input: {
|
||||
id: v4(),
|
||||
activityId: activity.id,
|
||||
activity,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
[getActivityTargetObjectFieldName({
|
||||
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
|
||||
})]: selectedRecord.record,
|
||||
[getActivityTargetObjectFieldIdName({
|
||||
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
|
||||
})]: selectedRecord.recordIdentifier.id,
|
||||
},
|
||||
});
|
||||
|
||||
return emptyActivityTarget;
|
||||
@ -128,12 +136,8 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
);
|
||||
}
|
||||
|
||||
injectIntoActivityTargetInlineCellCache({
|
||||
activityId: activity.id,
|
||||
activityTargetsToInject: activityTargetsAfterUpdate,
|
||||
});
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
createManyActivityTargetsInCache(activityTargetsToCreate);
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
@ -142,9 +146,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
});
|
||||
} else {
|
||||
if (activityTargetsToCreate.length > 0) {
|
||||
await createManyActivityTargets(activityTargetsToCreate, {
|
||||
skipOptimisticEffect: true,
|
||||
});
|
||||
await createManyActivityTargets(activityTargetsToCreate);
|
||||
}
|
||||
|
||||
if (activityTargetsToDelete.length > 0) {
|
||||
@ -153,12 +155,20 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.activityTarget.id,
|
||||
),
|
||||
{
|
||||
skipOptimisticEffect: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setActivityFromStore((currentActivity) => {
|
||||
if (isNull(currentActivity)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentActivity,
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
|
@ -18,9 +18,8 @@ type ActivityTargetsInlineCellProps = {
|
||||
export const ActivityTargetsInlineCell = ({
|
||||
activity,
|
||||
}: ActivityTargetsInlineCellProps) => {
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords({
|
||||
activityId: activity?.id ?? '',
|
||||
});
|
||||
const { activityTargetObjectRecords } =
|
||||
useActivityTargetObjectRecords(activity);
|
||||
const { closeInlineCell } = useInlineCell();
|
||||
|
||||
useScopedHotkeys(
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
|
||||
jest.mock('@/object-metadata/hooks/useObjectMetadataItemOnly', () => ({
|
||||
useObjectMetadataItemOnly: jest.fn(() => ({
|
||||
objectMetadataItem: { exampleMetadataItem: 'example' },
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache',
|
||||
() => ({
|
||||
useUpsertFindManyRecordsQueryInCache: jest.fn(() => ({
|
||||
upsertFindManyRecordsQueryInCache: jest.fn(),
|
||||
})),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('useInjectIntoActivityTargetInlineCellCache', () => {
|
||||
it('should inject into activity target inline cell cache as expected', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useInjectIntoActivityTargetInlineCellCache(),
|
||||
);
|
||||
|
||||
const { injectIntoActivityTargetInlineCellCache } = result.current;
|
||||
|
||||
const mockActivityId = 'mockId';
|
||||
const mockActivityTargetsToInject = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Example Activity Target',
|
||||
createdAt: '2022-01-01',
|
||||
updatedAt: '2022-01-01',
|
||||
activity: {
|
||||
id: '1',
|
||||
createdAt: '2022-01-01',
|
||||
updatedAt: '2022-01-01',
|
||||
} as Pick<Activity, 'id' | 'createdAt' | 'updatedAt'>,
|
||||
},
|
||||
];
|
||||
injectIntoActivityTargetInlineCellCache({
|
||||
activityId: mockActivityId,
|
||||
activityTargetsToInject: mockActivityTargetsToInject,
|
||||
});
|
||||
|
||||
expect(
|
||||
result.current.injectIntoActivityTargetInlineCellCache,
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
|
||||
export const useInjectIntoActivityTargetInlineCellCache = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const {
|
||||
upsertFindManyRecordsQueryInCache:
|
||||
overwriteFindManyActivityTargetsQueryInCache,
|
||||
} = useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const injectIntoActivityTargetInlineCellCache = ({
|
||||
activityId,
|
||||
activityTargetsToInject,
|
||||
}: {
|
||||
activityId: string;
|
||||
activityTargetsToInject: ActivityTarget[];
|
||||
}) => {
|
||||
const activityTargetInlineCellQueryVariables = {
|
||||
filter: {
|
||||
activityId: {
|
||||
eq: activityId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
overwriteFindManyActivityTargetsQueryInCache({
|
||||
queryVariables: activityTargetInlineCellQueryVariables,
|
||||
objectRecordsToOverwrite: activityTargetsToInject,
|
||||
depth: 2,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
injectIntoActivityTargetInlineCellCache,
|
||||
};
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { QueryKey } from '@/object-record/query-keys/types/QueryKey';
|
||||
|
||||
export const CREATE_ONE_ACTIVITY_QUERY_KEY: QueryKey = {
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
variables: {},
|
||||
fields: {
|
||||
id: true,
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
author: {
|
||||
id: true,
|
||||
name: true,
|
||||
__typename: true,
|
||||
},
|
||||
authorId: true,
|
||||
assigneeId: true,
|
||||
assignee: {
|
||||
id: true,
|
||||
name: true,
|
||||
__typename: true,
|
||||
},
|
||||
comments: true,
|
||||
attachments: true,
|
||||
body: true,
|
||||
title: true,
|
||||
completedAt: true,
|
||||
dueAt: true,
|
||||
reminderAt: true,
|
||||
type: true,
|
||||
},
|
||||
depth: 1,
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { QueryKey } from '@/object-record/query-keys/types/QueryKey';
|
||||
|
||||
export const FIND_MANY_ACTIVITIES_QUERY_KEY: QueryKey = {
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
variables: {},
|
||||
fieldsFactory: (_objectMetadataItems: ObjectMetadataItem[]) => {
|
||||
return {
|
||||
id: true,
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
author: {
|
||||
id: true,
|
||||
name: true,
|
||||
__typename: true,
|
||||
},
|
||||
authorId: true,
|
||||
assigneeId: true,
|
||||
assignee: {
|
||||
id: true,
|
||||
name: true,
|
||||
__typename: true,
|
||||
},
|
||||
comments: true,
|
||||
attachments: true,
|
||||
body: true,
|
||||
title: true,
|
||||
completedAt: true,
|
||||
dueAt: true,
|
||||
reminderAt: true,
|
||||
type: true,
|
||||
activityTargets: true,
|
||||
};
|
||||
},
|
||||
depth: 2,
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import { generateActivityTargetMorphFieldKeys } from '@/activities/utils/generateActivityTargetMorphFieldKeys';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { QueryKey } from '@/object-record/query-keys/types/QueryKey';
|
||||
|
||||
export const FIND_MANY_ACTIVITY_TARGETS_QUERY_KEY: QueryKey = {
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
variables: {},
|
||||
fieldsFactory: (objectMetadataItems: ObjectMetadataItem[]) => {
|
||||
return {
|
||||
id: true,
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
activity: true,
|
||||
activityId: true,
|
||||
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
|
||||
};
|
||||
},
|
||||
depth: 1,
|
||||
};
|
@ -1,25 +1,20 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries';
|
||||
import { useRemoveFromActivityTargetsQueries } from '@/activities/hooks/useRemoveFromActivityTargetsQueries';
|
||||
import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState';
|
||||
import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
|
||||
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
|
||||
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
|
||||
import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
@ -56,7 +51,12 @@ export const ActivityActionBar = () => {
|
||||
const [temporaryActivityForEditor, setTemporaryActivityForEditor] =
|
||||
useRecoilState(temporaryActivityForEditorState);
|
||||
|
||||
const { deleteActivityFromCache } = useDeleteActivityFromCache();
|
||||
const deleteActivityFromCache = useDeleteRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
const deleteActivityTargetFromCache = useDeleteRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
const [isUpsertingActivityInDB] = useRecoilState(
|
||||
@ -67,28 +67,11 @@ export const ActivityActionBar = () => {
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
const { refreshShowPageFindManyActivitiesQueries } =
|
||||
useRefreshShowPageFindManyActivitiesQueries();
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
const currentCompletedTaskQueryVariables = useRecoilValue(
|
||||
currentCompletedTaskQueryVariablesState,
|
||||
);
|
||||
|
||||
const currentIncompleteTaskQueryVariables = useRecoilValue(
|
||||
currentIncompleteTaskQueryVariablesState,
|
||||
);
|
||||
|
||||
const currentNotesQueryVariables = useRecoilValue(
|
||||
currentNotesQueryVariablesState,
|
||||
);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const { removeFromActivitiesQueries } = useRemoveFromActivitiesQueries();
|
||||
const { removeFromActivityTargetsQueries } =
|
||||
useRemoveFromActivityTargetsQueries();
|
||||
|
||||
const weAreOnObjectShowPage = pathname.startsWith('/object');
|
||||
const weAreOnTaskPage = pathname.startsWith('/tasks');
|
||||
|
||||
const deleteActivity = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
@ -108,78 +91,26 @@ export const ActivityActionBar = () => {
|
||||
|
||||
setIsRightDrawerOpen(false);
|
||||
|
||||
if (isNonEmptyString(viewableActivityId)) {
|
||||
if (!isNonEmptyString(viewableActivityId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) {
|
||||
deleteActivityFromCache(temporaryActivityForEditor);
|
||||
setTemporaryActivityForEditor(null);
|
||||
} else if (isNonEmptyString(activityIdInDrawer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNonEmptyString(activityIdInDrawer)) {
|
||||
const activityTargetIdsToDelete: string[] =
|
||||
activityTargets.map(mapToRecordId) ?? [];
|
||||
|
||||
if (weAreOnTaskPage) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [],
|
||||
activitiesFilters: currentCompletedTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentCompletedTaskQueryVariables?.orderBy,
|
||||
deleteActivityFromCache(activity);
|
||||
activityTargets.forEach((activityTarget: ActivityTarget) => {
|
||||
deleteActivityTargetFromCache(activityTarget);
|
||||
});
|
||||
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [],
|
||||
activitiesFilters: currentIncompleteTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentIncompleteTaskQueryVariables?.orderBy,
|
||||
});
|
||||
} else if (
|
||||
weAreOnObjectShowPage &&
|
||||
isDefined(objectShowPageTargetableObject)
|
||||
) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables:
|
||||
FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
});
|
||||
|
||||
if (isDefined(currentCompletedTaskQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: currentCompletedTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentCompletedTaskQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(currentIncompleteTaskQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters:
|
||||
currentIncompleteTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentIncompleteTaskQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(currentNotesQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: currentNotesQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentNotesQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
removeFromActivityTargetsQueries({
|
||||
activityTargetsToRemove: activity?.activityTargets ?? [],
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
});
|
||||
}
|
||||
refreshShowPageFindManyActivitiesQueries();
|
||||
|
||||
if (isNonEmptyArray(activityTargetIdsToDelete)) {
|
||||
await deleteManyActivityTargets(activityTargetIdsToDelete);
|
||||
@ -187,26 +118,19 @@ export const ActivityActionBar = () => {
|
||||
|
||||
await deleteOneActivity?.(viewableActivityId);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
activityIdInDrawer,
|
||||
currentCompletedTaskQueryVariables,
|
||||
currentIncompleteTaskQueryVariables,
|
||||
currentNotesQueryVariables,
|
||||
deleteActivityFromCache,
|
||||
deleteManyActivityTargets,
|
||||
deleteOneActivity,
|
||||
isActivityInCreateMode,
|
||||
objectShowPageTargetableObject,
|
||||
removeFromActivitiesQueries,
|
||||
removeFromActivityTargetsQueries,
|
||||
setTemporaryActivityForEditor,
|
||||
temporaryActivityForEditor,
|
||||
viewableActivityId,
|
||||
weAreOnObjectShowPage,
|
||||
weAreOnTaskPage,
|
||||
setIsRightDrawerOpen,
|
||||
viewableActivityId,
|
||||
isActivityInCreateMode,
|
||||
temporaryActivityForEditor,
|
||||
deleteActivityFromCache,
|
||||
setTemporaryActivityForEditor,
|
||||
refreshShowPageFindManyActivitiesQueries,
|
||||
deleteOneActivity,
|
||||
deleteActivityTargetFromCache,
|
||||
deleteManyActivityTargets,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { DateTime } from 'luxon';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentUserDueTaskCountState } from '@/activities/tasks/states/currentUserTaskCountState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
@ -15,7 +16,7 @@ export const CurrentUserDueTaskCountEffect = () => {
|
||||
currentUserDueTaskCountState,
|
||||
);
|
||||
|
||||
const { records: tasks } = useFindManyRecords({
|
||||
const { records: tasks } = useFindManyRecords<Activity>({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
depth: 0,
|
||||
filter: {
|
||||
|
@ -78,9 +78,7 @@ export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
const body = getActivitySummary(task.body);
|
||||
const { completeTask } = useCompleteTask(task);
|
||||
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords({
|
||||
activityId: task.id,
|
||||
});
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(task);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
|
@ -1,54 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useInjectIntoTimelineActivitiesQueries } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const upsertFindManyRecordsQueryInCacheMock = jest.fn();
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache',
|
||||
() => ({
|
||||
useUpsertFindManyRecordsQueryInCache: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
(useUpsertFindManyRecordsQueryInCache as jest.Mock).mockImplementation(() => ({
|
||||
upsertFindManyRecordsQueryInCache: upsertFindManyRecordsQueryInCacheMock,
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useInjectIntoTimelineActivitiesQueries', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(
|
||||
() => useInjectIntoTimelineActivitiesQueries(),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.injectIntoTimelineActivitiesQueries({
|
||||
activityToInject: mockedActivities[0],
|
||||
activityTargetsToInject: [],
|
||||
timelineTargetableObject: {
|
||||
id: '123',
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(upsertFindManyRecordsQueryInCacheMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
|
||||
export const useInjectIntoTimelineActivitiesQueries = () => {
|
||||
const { injectActivitiesQueries } = useInjectIntoActivitiesQueries();
|
||||
|
||||
const injectIntoTimelineActivitiesQueries = ({
|
||||
activityToInject,
|
||||
activityTargetsToInject,
|
||||
timelineTargetableObject,
|
||||
}: {
|
||||
activityToInject: Activity;
|
||||
activityTargetsToInject: ActivityTarget[];
|
||||
timelineTargetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
injectActivitiesQueries({
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables: {
|
||||
createdAt: 'DescNullsFirst',
|
||||
},
|
||||
activityTargetsToInject,
|
||||
activityToInject,
|
||||
targetableObjects: [timelineTargetableObject],
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
injectIntoTimelineActivitiesQueries,
|
||||
};
|
||||
};
|
@ -2,14 +2,12 @@ import { useEffect, useState } from 'react';
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
@ -20,8 +18,6 @@ export const useTimelineActivities = ({
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
|
||||
|
||||
const [, setObjectShowPageTargetableObject] = useRecoilState(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
@ -60,7 +56,7 @@ export const useTimelineActivities = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { records: activitiesWithConnection, loading: loadingActivities } =
|
||||
const { records: activities, loading: loadingActivities } =
|
||||
useFindManyRecords<Activity>({
|
||||
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
@ -68,15 +64,11 @@ export const useTimelineActivities = ({
|
||||
orderBy: timelineActivitiesQueryVariables.orderBy,
|
||||
onCompleted: useRecoilCallback(
|
||||
({ set }) =>
|
||||
(data) => {
|
||||
(activities) => {
|
||||
if (!initialized) {
|
||||
setInitialized(true);
|
||||
}
|
||||
|
||||
const activities = getRecordsFromRecordConnection({
|
||||
recordConnection: data,
|
||||
});
|
||||
|
||||
for (const activity of activities) {
|
||||
set(recordStoreFamilyState(activity.id), activity);
|
||||
}
|
||||
@ -97,11 +89,6 @@ export const useTimelineActivities = ({
|
||||
|
||||
const loading = loadingActivities || loadingActivityTargets;
|
||||
|
||||
const activities = activitiesWithConnection
|
||||
?.map(makeActivityWithoutConnection as any)
|
||||
.map(({ activity }: any) => activity as any)
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
activities,
|
||||
loading,
|
||||
|
@ -6,5 +6,4 @@ export type ActivityTargetWithTargetRecord = {
|
||||
targetObjectMetadataItem: ObjectMetadataItem;
|
||||
activityTarget: ActivityTarget;
|
||||
targetObject: ObjectRecord;
|
||||
targetObjectNameSingular: string;
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
export type ActivityTargetableObject = {
|
||||
id: string;
|
||||
targetObjectNameSingular: string;
|
||||
relatedTargetableObjects?: ActivityTargetableObject[];
|
||||
};
|
||||
|
@ -1,48 +0,0 @@
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
|
||||
|
||||
describe('getTargetableEntitiesWithParents', () => {
|
||||
it('should return the correct value', () => {
|
||||
const entities: ActivityTargetableObject[] = [
|
||||
{
|
||||
id: '1',
|
||||
targetObjectNameSingular: 'person',
|
||||
relatedTargetableObjects: [
|
||||
{
|
||||
id: '2',
|
||||
targetObjectNameSingular: 'company',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
targetObjectNameSingular: 'car',
|
||||
relatedTargetableObjects: [
|
||||
{
|
||||
id: '6',
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
targetObjectNameSingular: 'company',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const res =
|
||||
flattenTargetableObjectsAndTheirRelatedTargetableObjects(entities);
|
||||
|
||||
expect(res).toHaveLength(6);
|
||||
expect(res[0].id).toBe('1');
|
||||
expect(res[1].id).toBe('2');
|
||||
expect(res[2].id).toBe('4');
|
||||
expect(res[3].id).toBe('3');
|
||||
expect(res[4].id).toBe('6');
|
||||
expect(res[5].id).toBe('5');
|
||||
});
|
||||
});
|
@ -1,23 +0,0 @@
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||
|
||||
export const flattenTargetableObjectsAndTheirRelatedTargetableObjects = (
|
||||
targetableObjectsWithRelatedTargetableObjects: ActivityTargetableObject[],
|
||||
): ActivityTargetableObject[] => {
|
||||
const flattenedTargetableObjects: ActivityTargetableObject[] = [];
|
||||
|
||||
for (const targetableObject of targetableObjectsWithRelatedTargetableObjects ??
|
||||
[]) {
|
||||
flattenedTargetableObjects.push(targetableObject);
|
||||
|
||||
if (isDefined(targetableObject.relatedTargetableObjects)) {
|
||||
for (const relatedEntity of targetableObject.relatedTargetableObjects ??
|
||||
[]) {
|
||||
flattenedTargetableObjects.push(relatedEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return flattenedTargetableObjects;
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const generateActivityTargetMorphFieldKeys = (
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
) => {
|
||||
const targetableObjects = Object.fromEntries(
|
||||
objectMetadataItems
|
||||
.filter(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
|
||||
)
|
||||
.map((objectMetadataItem) => [objectMetadataItem.nameSingular, true]),
|
||||
);
|
||||
|
||||
const targetableObjectIds = Object.fromEntries(
|
||||
objectMetadataItems
|
||||
.filter(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
|
||||
)
|
||||
.map((objectMetadataItem) => [
|
||||
`${objectMetadataItem.nameSingular}Id`,
|
||||
true,
|
||||
]),
|
||||
);
|
||||
|
||||
return {
|
||||
...targetableObjects,
|
||||
...targetableObjectIds,
|
||||
};
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
export const getActivityTargetObjectFieldName = ({
|
||||
nameSingular,
|
||||
}: {
|
||||
nameSingular: string;
|
||||
}) => {
|
||||
return `${nameSingular}`;
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
|
||||
export const getActivityTargetsFilter = ({
|
||||
targetableObjects,
|
||||
|
@ -1,28 +1,21 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const makeActivityTargetsToCreateFromTargetableObjects = ({
|
||||
targetableObjects,
|
||||
activityId,
|
||||
activity,
|
||||
targetObjectRecords,
|
||||
}: {
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
activityId: string;
|
||||
activity: Activity;
|
||||
targetObjectRecords: ObjectRecord[];
|
||||
}): Partial<ActivityTarget>[] => {
|
||||
const activityTargetableObjects = targetableObjects
|
||||
? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
|
||||
targetableObjects,
|
||||
)
|
||||
: [];
|
||||
|
||||
const activityTargetsToCreate = activityTargetableObjects.map(
|
||||
(targetableObject) => {
|
||||
const activityTargetsToCreate = targetableObjects.map((targetableObject) => {
|
||||
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
@ -34,15 +27,15 @@ export const makeActivityTargetsToCreateFromTargetableObjects = ({
|
||||
const activityTarget = {
|
||||
[targetableObject.targetObjectNameSingular]: relatedObjectRecord,
|
||||
[targetableObjectFieldIdName]: targetableObject.id,
|
||||
activityId,
|
||||
activity,
|
||||
activityId: activity.id,
|
||||
id: v4(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
} as Partial<ActivityTarget>;
|
||||
|
||||
return activityTarget;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return activityTargetsToCreate;
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
@ -32,8 +32,8 @@ export const triggerAttachRelationOptimisticEffect = ({
|
||||
id: targetRecordCacheId,
|
||||
fields: {
|
||||
[fieldNameOnTargetRecord]: (targetRecordFieldValue, { toReference }) => {
|
||||
const fieldValueIsCachedObjectRecordConnection =
|
||||
isCachedObjectRecordConnection(
|
||||
const fieldValueisObjectRecordConnectionWithRefs =
|
||||
isObjectRecordConnectionWithRefs(
|
||||
sourceObjectNameSingular,
|
||||
targetRecordFieldValue,
|
||||
);
|
||||
@ -47,7 +47,7 @@ export const triggerAttachRelationOptimisticEffect = ({
|
||||
return targetRecordFieldValue;
|
||||
}
|
||||
|
||||
if (fieldValueIsCachedObjectRecordConnection) {
|
||||
if (fieldValueisObjectRecordConnectionWithRefs) {
|
||||
const nextEdges: CachedObjectRecordEdge[] = [
|
||||
...targetRecordFieldValue.edges,
|
||||
{
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
|
||||
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
|
||||
|
||||
/*
|
||||
TODO: for now new records are added to all cached record lists, no matter what the variables (filters, orderBy, etc.) are.
|
||||
@ -24,10 +24,6 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
||||
recordsToCreate: CachedObjectRecord[];
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
}) => {
|
||||
const objectEdgeTypeName = getEdgeTypename({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
recordsToCreate.forEach((record) =>
|
||||
triggerUpdateRelationsOptimisticEffect({
|
||||
cache,
|
||||
@ -49,7 +45,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
||||
toReference,
|
||||
},
|
||||
) => {
|
||||
const shouldSkip = !isCachedObjectRecordConnection(
|
||||
const shouldSkip = !isObjectRecordConnectionWithRefs(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
@ -97,7 +93,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
|
||||
|
||||
if (recordToCreateReference && !recordAlreadyInCache) {
|
||||
nextRootQueryCachedRecordEdges.unshift({
|
||||
__typename: objectEdgeTypeName,
|
||||
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
|
||||
node: recordToCreateReference,
|
||||
cursor: '',
|
||||
});
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
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 { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
|
||||
@ -27,7 +27,7 @@ export const triggerDeleteRecordsOptimisticEffect = ({
|
||||
{ DELETE, readField, storeFieldName },
|
||||
) => {
|
||||
const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
|
||||
!isCachedObjectRecordConnection(
|
||||
!isObjectRecordConnectionWithRefs(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const triggerDetachRelationOptimisticEffect = ({
|
||||
@ -32,7 +32,7 @@ export const triggerDetachRelationOptimisticEffect = ({
|
||||
targetRecordFieldValue,
|
||||
{ isReference, readField },
|
||||
) => {
|
||||
const isRecordConnection = isCachedObjectRecordConnection(
|
||||
const isRecordConnection = isObjectRecordConnectionWithRefs(
|
||||
sourceObjectNameSingular,
|
||||
targetRecordFieldValue,
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { isCachedObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isCachedObjectRecordConnection';
|
||||
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
@ -8,6 +7,7 @@ import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
|
||||
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
@ -27,10 +27,6 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
updatedRecord: CachedObjectRecord;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
}) => {
|
||||
const objectEdgeTypeName = getEdgeTypename({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
triggerUpdateRelationsOptimisticEffect({
|
||||
cache,
|
||||
sourceObjectMetadataItem: objectMetadataItem,
|
||||
@ -45,7 +41,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
rootQueryCachedResponse,
|
||||
{ DELETE, readField, storeFieldName, toReference },
|
||||
) => {
|
||||
const shouldSkip = !isCachedObjectRecordConnection(
|
||||
const shouldSkip = !isObjectRecordConnectionWithRefs(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
@ -103,7 +99,7 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
|
||||
if (isDefined(updatedRecordNodeReference)) {
|
||||
rootQueryNextEdges.push({
|
||||
__typename: objectEdgeTypeName,
|
||||
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
|
||||
node: updatedRecordNodeReference,
|
||||
cursor: '',
|
||||
});
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { ApolloCache } from '@apollo/client';
|
||||
|
||||
import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition';
|
||||
import { isObjectRecordConnection } from '@/apollo/optimistic-effect/utils/isObjectRecordConnection';
|
||||
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||
@ -9,6 +8,7 @@ import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
|
||||
import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isObjectRecordConnection } from '@/object-record/cache/utils/isObjectRecordConnection';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
@ -65,6 +65,27 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
}
|
||||
defaultValue
|
||||
options
|
||||
relationDefinition {
|
||||
direction
|
||||
sourceObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
sourceFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
targetObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
targetFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
|
@ -1,21 +1,16 @@
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
ApolloClient,
|
||||
NormalizedCacheObject,
|
||||
useApolloClient,
|
||||
} from '@apollo/client';
|
||||
|
||||
import { ApolloMetadataClientContext } from '@/object-metadata/context/ApolloClientMetadataContext';
|
||||
import { mockedMetadataApolloClient } from '~/testing/mockedMetadataApolloClient';
|
||||
|
||||
export const TestApolloMetadataClientProvider = ({
|
||||
export const ApolloMetadataClientMockedProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const client = useApolloClient() as ApolloClient<NormalizedCacheObject>;
|
||||
return (
|
||||
<ApolloMetadataClientContext.Provider value={client}>
|
||||
{client ? children : ''}
|
||||
<ApolloMetadataClientContext.Provider value={mockedMetadataApolloClient}>
|
||||
{mockedMetadataApolloClient ? children : ''}
|
||||
</ApolloMetadataClientContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -32,7 +32,6 @@ describe('useObjectMetadataItem', () => {
|
||||
labelIdentifierFieldMetadata,
|
||||
getRecordFromCache,
|
||||
findManyRecordsQuery,
|
||||
modifyRecordFromCache,
|
||||
findOneRecordQuery,
|
||||
createOneRecordMutation,
|
||||
updateOneRecordMutation,
|
||||
@ -48,7 +47,6 @@ describe('useObjectMetadataItem', () => {
|
||||
expect(basePathToShowPage).toBe('/object/opportunity/');
|
||||
expect(objectMetadataItem.id).toBe('20202020-cae9-4ff4-9579-f7d9fe44c937');
|
||||
expect(typeof getRecordFromCache).toBe('function');
|
||||
expect(typeof modifyRecordFromCache).toBe('function');
|
||||
expect(typeof mapToObjectRecordIdentifier).toBe('function');
|
||||
expect(typeof getObjectOrderByField).toBe('function');
|
||||
expect(findManyRecordsQuery).toHaveProperty('kind', 'Document');
|
||||
|
@ -11,7 +11,6 @@ import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShow
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||
import { useGenerateDeleteManyRecordMutation } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
|
||||
@ -40,7 +39,8 @@ export const EMPTY_MUTATION = gql`
|
||||
export const useObjectMetadataItem = (
|
||||
{ objectNameSingular }: ObjectMetadataItemIdentifier,
|
||||
depth?: number,
|
||||
eagerLoadedRelations?: Record<string, any>,
|
||||
queryFields?: Record<string, any>,
|
||||
computeReferences = false,
|
||||
) => {
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
@ -83,15 +83,11 @@ export const useObjectMetadataItem = (
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const modifyRecordFromCache = useModifyRecordFromCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery();
|
||||
const findManyRecordsQuery = generateFindManyRecordsQuery({
|
||||
objectMetadataItem,
|
||||
depth,
|
||||
eagerLoadedRelations,
|
||||
queryFields,
|
||||
});
|
||||
|
||||
const generateFindDuplicateRecordsQuery =
|
||||
@ -109,14 +105,18 @@ export const useObjectMetadataItem = (
|
||||
|
||||
const createOneRecordMutation = useGenerateCreateOneRecordMutation({
|
||||
objectMetadataItem,
|
||||
depth,
|
||||
});
|
||||
|
||||
const createManyRecordsMutation = useGenerateCreateManyRecordMutation({
|
||||
objectMetadataItem,
|
||||
depth,
|
||||
});
|
||||
|
||||
const updateOneRecordMutation = useGenerateUpdateOneRecordMutation({
|
||||
objectMetadataItem,
|
||||
depth,
|
||||
computeReferences,
|
||||
});
|
||||
|
||||
const deleteOneRecordMutation = generateDeleteOneRecordMutation({
|
||||
@ -144,7 +144,6 @@ export const useObjectMetadataItem = (
|
||||
basePathToShowPage,
|
||||
objectMetadataItem,
|
||||
getRecordFromCache,
|
||||
modifyRecordFromCache,
|
||||
findManyRecordsQuery,
|
||||
findDuplicateRecordsQuery,
|
||||
findOneRecordQuery,
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { ThemeColor } from '@/ui/theme/constants/MainColorNames';
|
||||
import { Field, Relation } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
Field,
|
||||
Object as MetadataObject,
|
||||
Relation,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export type FieldMetadataItemOption = {
|
||||
color: ThemeColor;
|
||||
@ -16,6 +21,7 @@ export type FieldMetadataItem = Omit<
|
||||
| 'toRelationMetadata'
|
||||
| 'defaultValue'
|
||||
| 'options'
|
||||
| 'relationDefinition'
|
||||
> & {
|
||||
__typename?: string;
|
||||
fromRelationMetadata?:
|
||||
@ -36,4 +42,17 @@ export type FieldMetadataItem = Omit<
|
||||
| null;
|
||||
defaultValue?: any;
|
||||
options?: FieldMetadataItemOption[];
|
||||
relationDefinition?: {
|
||||
direction: RelationDefinitionType;
|
||||
sourceFieldMetadata: Pick<Field, 'id' | 'name'>;
|
||||
sourceObjectMetadata: Pick<
|
||||
MetadataObject,
|
||||
'id' | 'nameSingular' | 'namePlural'
|
||||
>;
|
||||
targetFieldMetadata: Pick<Field, 'id' | 'name'>;
|
||||
targetObjectMetadata: Pick<
|
||||
MetadataObject,
|
||||
'id' | 'nameSingular' | 'namePlural'
|
||||
>;
|
||||
} | null;
|
||||
};
|
||||
|
@ -40,7 +40,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
|
||||
it('should not return relation if depth is < 1', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
relationFieldDepth: 0,
|
||||
depth: 0,
|
||||
field: personObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'company',
|
||||
)!,
|
||||
@ -51,7 +51,7 @@ describe('mapFieldMetadataToGraphQLQuery', () => {
|
||||
it('should return relation if it matches depth', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
relationFieldDepth: 1,
|
||||
depth: 1,
|
||||
field: personObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'company',
|
||||
)!,
|
||||
@ -88,7 +88,7 @@ idealCustomerProfile
|
||||
it('should return relation with all sub relations if it matches depth', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
relationFieldDepth: 2,
|
||||
depth: 2,
|
||||
field: personObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'company',
|
||||
)!,
|
||||
@ -239,11 +239,26 @@ idealCustomerProfile
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should return eagerLoaded relations', async () => {
|
||||
it('should return GraphQL fields based on queryFields', async () => {
|
||||
const res = mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
relationFieldDepth: 2,
|
||||
relationFieldEagerLoad: { accountOwner: true, people: true },
|
||||
depth: 2,
|
||||
queryFields: {
|
||||
accountOwner: true,
|
||||
people: true,
|
||||
xLink: true,
|
||||
linkedinLink: true,
|
||||
domainName: true,
|
||||
annualRecurringRevenue: true,
|
||||
createdAt: true,
|
||||
address: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
accountOwnerId: true,
|
||||
employees: true,
|
||||
id: true,
|
||||
idealCustomerProfile: true,
|
||||
},
|
||||
field: personObjectMetadataItem.fields.find(
|
||||
(field) => field.name === 'company',
|
||||
)!,
|
||||
|
@ -213,11 +213,25 @@ companyId
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should eager load only specified relations', async () => {
|
||||
it('should query only specified queryFields', async () => {
|
||||
const res = mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
eagerLoadedRelations: { company: true },
|
||||
queryFields: {
|
||||
company: true,
|
||||
xLink: true,
|
||||
id: true,
|
||||
createdAt: true,
|
||||
city: true,
|
||||
email: true,
|
||||
jobTitle: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
linkedinLink: true,
|
||||
updatedAt: true,
|
||||
avatarUrl: true,
|
||||
companyId: true,
|
||||
},
|
||||
depth: 1,
|
||||
});
|
||||
expect(formatGQLString(res)).toEqual(`{
|
||||
@ -274,6 +288,52 @@ linkedinLink
|
||||
updatedAt
|
||||
avatarUrl
|
||||
companyId
|
||||
}`);
|
||||
});
|
||||
|
||||
it('should load only specified query fields', async () => {
|
||||
const res = mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: mockObjectMetadataItems,
|
||||
objectMetadataItem: personObjectMetadataItem,
|
||||
queryFields: { company: true, id: true, name: true },
|
||||
depth: 1,
|
||||
});
|
||||
expect(formatGQLString(res)).toEqual(`{
|
||||
__typename
|
||||
id
|
||||
company
|
||||
{
|
||||
__typename
|
||||
xLink
|
||||
{
|
||||
label
|
||||
url
|
||||
}
|
||||
linkedinLink
|
||||
{
|
||||
label
|
||||
url
|
||||
}
|
||||
domainName
|
||||
annualRecurringRevenue
|
||||
{
|
||||
amountMicros
|
||||
currencyCode
|
||||
}
|
||||
createdAt
|
||||
address
|
||||
updatedAt
|
||||
name
|
||||
accountOwnerId
|
||||
employees
|
||||
id
|
||||
idealCustomerProfile
|
||||
}
|
||||
name
|
||||
{
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}`);
|
||||
});
|
||||
});
|
||||
|
@ -34,10 +34,10 @@ describe('shouldFieldBeQueried', () => {
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should not depends on eagerLoadedRelation', () => {
|
||||
it('should not depends on queryFields', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 0,
|
||||
eagerLoadedRelations: {
|
||||
queryFields: {
|
||||
fieldName: true,
|
||||
},
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Boolean },
|
||||
@ -47,14 +47,14 @@ describe('shouldFieldBeQueried', () => {
|
||||
});
|
||||
|
||||
describe('if field is relation', () => {
|
||||
it('should be queried if eagerLoadedRelation and depth are undefined', () => {
|
||||
it('should be queried if queryFields and depth are undefined', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should be queried if eagerLoadedRelation is undefined and depth = 1', () => {
|
||||
it('should be queried if queryFields is undefined and depth = 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 1,
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
@ -62,7 +62,7 @@ describe('shouldFieldBeQueried', () => {
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should be queried if eagerLoadedRelation is undefined and depth > 1', () => {
|
||||
it('should be queried if queryFields is undefined and depth > 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 2,
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
@ -70,7 +70,7 @@ describe('shouldFieldBeQueried', () => {
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT be queried if eagerLoadedRelation is undefined and depth < 1', () => {
|
||||
it('should NOT be queried if queryFields is undefined and depth < 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 0,
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
@ -78,37 +78,37 @@ describe('shouldFieldBeQueried', () => {
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should be queried if eagerLoadedRelation is matching and depth > 1', () => {
|
||||
it('should be queried if queryFields is matching and depth > 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 1,
|
||||
eagerLoadedRelations: { fieldName: true },
|
||||
queryFields: { fieldName: true },
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT be queried if eagerLoadedRelation is matching and depth < 1', () => {
|
||||
it('should NOT be queried if queryFields is matching and depth < 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 0,
|
||||
eagerLoadedRelations: { fieldName: true },
|
||||
queryFields: { fieldName: true },
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT be queried if eagerLoadedRelation is not matching (falsy) and depth < 1', () => {
|
||||
it('should NOT be queried if queryFields is not matching (falsy) and depth < 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 1,
|
||||
eagerLoadedRelations: { fieldName: false },
|
||||
queryFields: { fieldName: false },
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT be queried if eagerLoadedRelation is not matching and depth < 1', () => {
|
||||
it('should NOT be queried if queryFields is not matching and depth < 1', () => {
|
||||
const res = shouldFieldBeQueried({
|
||||
depth: 0,
|
||||
eagerLoadedRelations: { anotherFieldName: true },
|
||||
queryFields: { anotherFieldName: true },
|
||||
field: { name: 'fieldName', type: FieldMetadataType.Relation },
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
|
@ -0,0 +1,38 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { RelationDirections } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const getFieldRelationDirections = (
|
||||
field: Pick<FieldMetadataItem, 'type' | 'relationDefinition'> | undefined,
|
||||
): RelationDirections => {
|
||||
if (!field || field.type !== FieldMetadataType.Relation) {
|
||||
throw new Error(`Field is not a relation field.`);
|
||||
}
|
||||
|
||||
switch (field.relationDefinition?.direction) {
|
||||
case RelationDefinitionType.ManyToMany:
|
||||
throw new Error(`Many to many relations are not supported.`);
|
||||
case RelationDefinitionType.OneToMany:
|
||||
return {
|
||||
from: 'FROM_ONE_OBJECT',
|
||||
to: 'TO_MANY_OBJECTS',
|
||||
};
|
||||
case RelationDefinitionType.ManyToOne:
|
||||
return {
|
||||
from: 'FROM_MANY_OBJECTS',
|
||||
to: 'TO_ONE_OBJECT',
|
||||
};
|
||||
case RelationDefinitionType.OneToOne:
|
||||
return {
|
||||
from: 'FROM_ONE_OBJECT',
|
||||
to: 'TO_ONE_OBJECT',
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid relation definition type direction : ${field.relationDefinition?.direction}`,
|
||||
);
|
||||
}
|
||||
};
|
@ -6,19 +6,22 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
// TODO: change ObjectMetadataItems mock before refactoring with relationDefinition computed field
|
||||
export const mapFieldMetadataToGraphQLQuery = ({
|
||||
objectMetadataItems,
|
||||
field,
|
||||
relationFieldDepth = 0,
|
||||
relationFieldEagerLoad,
|
||||
depth = 0,
|
||||
queryFields,
|
||||
computeReferences = false,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
field: Pick<
|
||||
FieldMetadataItem,
|
||||
'name' | 'type' | 'toRelationMetadata' | 'fromRelationMetadata'
|
||||
>;
|
||||
relationFieldDepth?: number;
|
||||
relationFieldEagerLoad?: Record<string, any>;
|
||||
depth?: number;
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
}): any => {
|
||||
const fieldType = field.type;
|
||||
|
||||
@ -43,7 +46,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
} else if (
|
||||
fieldType === 'RELATION' &&
|
||||
field.toRelationMetadata?.relationType === 'ONE_TO_MANY' &&
|
||||
relationFieldDepth > 0
|
||||
depth > 0
|
||||
) {
|
||||
const relationMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
@ -59,13 +62,15 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: relationMetadataItem,
|
||||
eagerLoadedRelations: relationFieldEagerLoad,
|
||||
depth: relationFieldDepth - 1,
|
||||
depth: depth - 1,
|
||||
queryFields,
|
||||
computeReferences: computeReferences,
|
||||
isRootLevel: false,
|
||||
})}`;
|
||||
} else if (
|
||||
fieldType === 'RELATION' &&
|
||||
field.fromRelationMetadata?.relationType === 'ONE_TO_MANY' &&
|
||||
relationFieldDepth > 0
|
||||
depth > 0
|
||||
) {
|
||||
const relationMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
@ -83,8 +88,10 @@ ${mapObjectMetadataToGraphQLQuery({
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem: relationMetadataItem,
|
||||
eagerLoadedRelations: relationFieldEagerLoad,
|
||||
depth: relationFieldDepth - 1,
|
||||
depth: depth - 1,
|
||||
queryFields,
|
||||
computeReferences,
|
||||
isRootLevel: false,
|
||||
})}
|
||||
}
|
||||
}`;
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
|
||||
import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried';
|
||||
@ -8,28 +6,47 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
depth = 1,
|
||||
eagerLoadedRelations,
|
||||
queryFields,
|
||||
computeReferences = false,
|
||||
isRootLevel = true,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: Pick<ObjectMetadataItem, 'nameSingular' | 'fields'>;
|
||||
depth?: number;
|
||||
eagerLoadedRelations?: Record<string, any>;
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
isRootLevel?: boolean;
|
||||
}): any => {
|
||||
return `{
|
||||
__typename
|
||||
${(objectMetadataItem?.fields ?? [])
|
||||
const fieldsThatShouldBeQueried =
|
||||
objectMetadataItem?.fields
|
||||
.filter((field) => field.isActive)
|
||||
.filter((field) =>
|
||||
shouldFieldBeQueried({ field, depth, eagerLoadedRelations }),
|
||||
)
|
||||
shouldFieldBeQueried({
|
||||
field,
|
||||
depth,
|
||||
queryFields,
|
||||
}),
|
||||
) ?? [];
|
||||
|
||||
if (!isRootLevel && computeReferences) {
|
||||
return `{
|
||||
__ref
|
||||
}`;
|
||||
}
|
||||
|
||||
return `{
|
||||
__typename
|
||||
${fieldsThatShouldBeQueried
|
||||
.map((field) =>
|
||||
mapFieldMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
field,
|
||||
relationFieldDepth: depth,
|
||||
relationFieldEagerLoad: isUndefined(eagerLoadedRelations)
|
||||
depth,
|
||||
queryFields:
|
||||
typeof queryFields?.[field.name] === 'boolean'
|
||||
? undefined
|
||||
: eagerLoadedRelations[field.name] ?? undefined,
|
||||
: queryFields?.[field.name],
|
||||
computeReferences,
|
||||
}),
|
||||
)
|
||||
.join('\n')}
|
||||
|
@ -1,17 +1,20 @@
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
export const shouldFieldBeQueried = ({
|
||||
field,
|
||||
depth,
|
||||
eagerLoadedRelations,
|
||||
queryFields,
|
||||
}: {
|
||||
field: Pick<FieldMetadataItem, 'name' | 'type'>;
|
||||
depth?: number;
|
||||
eagerLoadedRelations?: Record<string, boolean>;
|
||||
objectRecord?: ObjectRecord;
|
||||
queryFields?: Record<string, any>;
|
||||
}): any => {
|
||||
if (!isUndefined(depth) && depth < 0) {
|
||||
return false;
|
||||
@ -25,12 +28,7 @@ export const shouldFieldBeQueried = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === FieldMetadataType.Relation &&
|
||||
!isUndefined(eagerLoadedRelations) &&
|
||||
(isUndefined(eagerLoadedRelations[field.name]) ||
|
||||
!eagerLoadedRelations[field.name])
|
||||
) {
|
||||
if (isDefined(queryFields) && !queryFields[field.name]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1,64 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import gql from 'graphql-tag';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { useInjectIntoFindOneRecordQueryCache } from '@/object-record/cache/hooks/useInjectIntoFindOneRecordQueryCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useAddRecordInCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { injectIntoFindOneRecordQueryCache } =
|
||||
useInjectIntoFindOneRecordQueryCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
(record: ObjectRecord) => {
|
||||
const fragment = gql`
|
||||
fragment Create${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}InCache on ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)} ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
})}
|
||||
`;
|
||||
|
||||
const cachedObjectRecord = {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...record,
|
||||
};
|
||||
|
||||
apolloClient.writeFragment({
|
||||
id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`,
|
||||
fragment,
|
||||
data: cachedObjectRecord,
|
||||
});
|
||||
|
||||
// TODO: should we keep this here ? Or should the caller of createOneRecordInCache/createManyRecordsInCache be responsible for this ?
|
||||
injectIntoFindOneRecordQueryCache(cachedObjectRecord);
|
||||
|
||||
// TODO: remove this once we get rid of entityFieldsFamilyState
|
||||
set(recordStoreFamilyState(record.id), record);
|
||||
},
|
||||
[
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
apolloClient,
|
||||
injectIntoFindOneRecordQueryCache,
|
||||
],
|
||||
);
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
|
||||
export const useAppendToFindManyRecordsQueryInCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { readFindManyRecordsQueryInCache } =
|
||||
useReadFindManyRecordsQueryInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const {
|
||||
upsertFindManyRecordsQueryInCache: overwriteFindManyRecordsQueryInCache,
|
||||
} = useUpsertFindManyRecordsQueryInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const appendToFindManyRecordsQueryInCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
queryVariables,
|
||||
objectRecordsToAppend,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
objectRecordsToAppend: T[];
|
||||
}) => {
|
||||
const existingObjectRecords = readFindManyRecordsQueryInCache({
|
||||
queryVariables,
|
||||
});
|
||||
|
||||
const newObjectRecordList = [
|
||||
...existingObjectRecords,
|
||||
...objectRecordsToAppend,
|
||||
];
|
||||
|
||||
overwriteFindManyRecordsQueryInCache({
|
||||
objectRecordsToOverwrite: newObjectRecordList,
|
||||
queryVariables,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
appendToFindManyRecordsQueryInCache,
|
||||
};
|
||||
};
|
42
packages/twenty-front/src/modules/object-record/cache/hooks/useCreateManyRecordsInCache.ts
vendored
Normal file
42
packages/twenty-front/src/modules/object-record/cache/hooks/useCreateManyRecordsInCache.ts
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const createOneRecordInCache = useCreateOneRecordInCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const createManyRecordsInCache = (recordsToCreate: Partial<T>[]) => {
|
||||
const recordsWithId = recordsToCreate
|
||||
.map((record) => {
|
||||
return prefillRecord<T>({
|
||||
input: record,
|
||||
objectMetadataItem,
|
||||
});
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const createdRecordsInCache = [] as T[];
|
||||
|
||||
for (const record of recordsWithId) {
|
||||
if (isDefined(record)) {
|
||||
createOneRecordInCache(record);
|
||||
createdRecordsInCache.push(record);
|
||||
}
|
||||
}
|
||||
|
||||
return createdRecordsInCache;
|
||||
};
|
||||
|
||||
return { createManyRecordsInCache };
|
||||
};
|
62
packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts
vendored
Normal file
62
packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import gql from 'graphql-tag';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useCreateOneRecordInCache = <T extends ObjectRecord>({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectMetadataItem,
|
||||
});
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
return (record: ObjectRecord) => {
|
||||
const fragment = gql`
|
||||
fragment Create${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}InCache on ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)} ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
computeReferences: true,
|
||||
})}
|
||||
`;
|
||||
|
||||
const prefilledRecord = prefillRecord({
|
||||
objectMetadataItem,
|
||||
input: record,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
const recordToCreateWithNestedConnections = getRecordNodeFromRecord({
|
||||
record: prefilledRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
const cachedObjectRecord = {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...recordToCreateWithNestedConnections,
|
||||
};
|
||||
|
||||
apolloClient.writeFragment({
|
||||
id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`,
|
||||
fragment,
|
||||
data: cachedObjectRecord,
|
||||
});
|
||||
return getRecordFromCache(record.id) as T;
|
||||
};
|
||||
};
|
29
packages/twenty-front/src/modules/object-record/cache/hooks/useDeleteRecordFromCache.ts
vendored
Normal file
29
packages/twenty-front/src/modules/object-record/cache/hooks/useDeleteRecordFromCache.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const useDeleteRecordFromCache = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItemOnly({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
return (recordToDelete: ObjectRecord) => {
|
||||
deleteRecordFromCache({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordToDelete,
|
||||
cache: apolloClient.cache,
|
||||
});
|
||||
};
|
||||
};
|
@ -1,79 +0,0 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
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 useGenerateObjectRecordOptimisticResponse = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const getRelationMetadata = useGetRelationMetadata();
|
||||
|
||||
const generateObjectRecordOptimisticResponse = <
|
||||
GeneratedObjectRecord extends ObjectRecord,
|
||||
>(
|
||||
input: Record<string, unknown>,
|
||||
) => {
|
||||
const recordSchema = z.object(
|
||||
Object.fromEntries(
|
||||
objectMetadataItem.fields.map((fieldMetadataItem) => [
|
||||
fieldMetadataItem.name,
|
||||
z.unknown().default(generateEmptyFieldValue(fieldMetadataItem)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const inputWithRelationFields = objectMetadataItem.fields.reduce(
|
||||
(result, fieldMetadataItem) => {
|
||||
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||
|
||||
if (!(relationIdFieldName in input)) return result;
|
||||
|
||||
const relationMetadata = getRelationMetadata({ fieldMetadataItem });
|
||||
|
||||
if (!relationMetadata) return result;
|
||||
|
||||
const relationRecordTypeName = capitalize(
|
||||
relationMetadata.relationObjectMetadataItem.nameSingular,
|
||||
);
|
||||
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,
|
||||
};
|
||||
},
|
||||
input,
|
||||
);
|
||||
|
||||
return {
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
...recordSchema.parse({
|
||||
id: v4(),
|
||||
createdAt: new Date().toISOString(),
|
||||
...inputWithRelationFields,
|
||||
}),
|
||||
} as GeneratedObjectRecord & { __typename: string };
|
||||
};
|
||||
|
||||
return {
|
||||
generateObjectRecordOptimisticResponse,
|
||||
};
|
||||
};
|
@ -1,13 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
import { gql, useApolloClient } from '@apollo/client';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useGetRecordFromCache = ({
|
||||
objectMetadataItem,
|
||||
@ -23,29 +21,11 @@ export const useGetRecordFromCache = ({
|
||||
recordId: string,
|
||||
cache = apolloClient.cache,
|
||||
) => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
|
||||
const cacheReadFragment = gql`
|
||||
fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} ${mapObjectMetadataToGraphQLQuery(
|
||||
{
|
||||
return getRecordFromCache<CachedObjectRecord>({
|
||||
cache,
|
||||
recordId,
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
},
|
||||
)}
|
||||
`;
|
||||
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
return cache.readFragment<CachedObjectRecord & { __typename: string }>({
|
||||
id: cachedRecordId,
|
||||
fragment: cacheReadFragment,
|
||||
});
|
||||
},
|
||||
[objectMetadataItem, objectMetadataItems, apolloClient],
|
||||
|
@ -1,44 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useInjectIntoFindOneRecordQueryCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const generateFindOneRecordQuery = useGenerateFindOneRecordQuery();
|
||||
|
||||
const injectIntoFindOneRecordQueryCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>(
|
||||
record: T,
|
||||
) => {
|
||||
const findOneRecordQueryForCacheInjection = generateFindOneRecordQuery({
|
||||
objectMetadataItem,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
apolloClient.writeQuery({
|
||||
query: findOneRecordQueryForCacheInjection,
|
||||
variables: {
|
||||
objectRecordId: record.id,
|
||||
},
|
||||
data: {
|
||||
[objectMetadataItem.nameSingular]: {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
|
||||
...record,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
injectIntoFindOneRecordQueryCache,
|
||||
};
|
||||
};
|
@ -1,32 +0,0 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { Modifiers } from '@apollo/client/cache';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useModifyRecordFromCache = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { cache } = useApolloClient();
|
||||
|
||||
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
|
||||
recordId: string,
|
||||
fieldModifiers: Modifiers<CachedObjectRecord>,
|
||||
) => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) return;
|
||||
|
||||
const cachedRecordId = cache.identify({
|
||||
__typename: capitalize(objectMetadataItem.nameSingular),
|
||||
id: recordId,
|
||||
});
|
||||
|
||||
cache.modify<CachedObjectRecord>({
|
||||
id: cachedRecordId,
|
||||
fields: fieldModifiers,
|
||||
});
|
||||
};
|
||||
};
|
@ -21,11 +21,17 @@ export const useReadFindManyRecordsQueryInCache = ({
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
>({
|
||||
queryVariables,
|
||||
queryFields,
|
||||
depth,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
queryFields?: Record<string, any>;
|
||||
depth?: number;
|
||||
}) => {
|
||||
const findManyRecordsQueryForCacheRead = generateFindManyRecordsQuery({
|
||||
objectMetadataItem,
|
||||
queryFields,
|
||||
depth,
|
||||
});
|
||||
|
||||
const existingRecordsQueryResult = apolloClient.readQuery<
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
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';
|
||||
@ -18,6 +20,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const generateFindManyRecordsQuery = useGenerateFindManyRecordsQuery();
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const upsertFindManyRecordsQueryInCache = <
|
||||
T extends ObjectRecord = ObjectRecord,
|
||||
@ -25,19 +28,28 @@ export const useUpsertFindManyRecordsQueryInCache = ({
|
||||
queryVariables,
|
||||
depth = MAX_QUERY_DEPTH_FOR_CACHE_INJECTION,
|
||||
objectRecordsToOverwrite,
|
||||
queryFields,
|
||||
computeReferences = false,
|
||||
}: {
|
||||
queryVariables: ObjectRecordQueryVariables;
|
||||
depth?: number;
|
||||
objectRecordsToOverwrite: T[];
|
||||
queryFields?: Record<string, any>;
|
||||
computeReferences?: boolean;
|
||||
}) => {
|
||||
const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({
|
||||
objectMetadataItem,
|
||||
depth, // TODO: fix this
|
||||
depth,
|
||||
queryFields,
|
||||
computeReferences,
|
||||
});
|
||||
|
||||
const newObjectRecordConnection = getRecordConnectionFromRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
records: objectRecordsToOverwrite,
|
||||
queryFields,
|
||||
computeReferences,
|
||||
});
|
||||
|
||||
apolloClient.writeQuery({
|
||||
|
30
packages/twenty-front/src/modules/object-record/cache/utils/deleteRecordFromCache.ts
vendored
Normal file
30
packages/twenty-front/src/modules/object-record/cache/utils/deleteRecordFromCache.ts
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
import { ApolloCache } from '@apollo/client';
|
||||
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const deleteRecordFromCache = ({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordToDelete,
|
||||
cache,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
recordToDelete: ObjectRecord;
|
||||
cache: ApolloCache<object>;
|
||||
}) => {
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordsToDelete: [
|
||||
{
|
||||
...recordToDelete,
|
||||
__typename: getObjectTypename(objectMetadataItem.nameSingular),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
import { ApolloClient, makeReference, Reference } from '@apollo/client';
|
||||
|
||||
import { getCachedRecordFromRecord } from '@/object-record/cache/utils/getCachedRecordFromRecord';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const getCacheReferenceFromRecord = <T extends ObjectRecord>({
|
||||
apolloClient,
|
||||
objectNameSingular,
|
||||
record,
|
||||
}: {
|
||||
apolloClient: ApolloClient<object>;
|
||||
objectNameSingular: string;
|
||||
record: T;
|
||||
}): Reference => {
|
||||
const cachedRecord = getCachedRecordFromRecord({
|
||||
objectNameSingular,
|
||||
record,
|
||||
});
|
||||
|
||||
const id = apolloClient.cache.identify(cachedRecord);
|
||||
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
`Could not identify record "${objectNameSingular}", id : "${record.id}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const recordReference = makeReference(id);
|
||||
|
||||
return recordReference;
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user