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:
Charles Bochet 2024-04-01 13:12:37 +02:00 committed by GitHub
parent 4e109c9a38
commit 02673a82af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
172 changed files with 2182 additions and 4915 deletions

View File

@ -41,3 +41,4 @@ dist-ssr
*.sw?
.vite/
.nyc_output/

View File

@ -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'],

View File

@ -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'],
};

View File

@ -13,6 +13,12 @@ const meta: Meta<typeof Calendar> = {
container: { width: 728 },
msw: graphqlMocks,
},
args: {
targetableObject: {
id: '1',
targetObjectNameSingular: 'Person',
},
},
};
export default meta;

View File

@ -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);

View File

@ -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({

View File

@ -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>

View File

@ -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);

View File

@ -40,7 +40,6 @@ export const useRightDrawerEmailThread = () => {
receivedAt: 'AscNullsLast',
},
skip: !viewableEmailThreadId,
useRecordsWithoutConnection: true,
});
const fetchMoreMessages = useCallback(() => {

View File

@ -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';

View File

@ -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,
};
};

View File

@ -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';

View File

@ -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

View File

@ -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',
});
});
});

View File

@ -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,
);
});
});

View File

@ -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');
});
});

View File

@ -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();
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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',
},
],
});
});
});

View File

@ -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,
},
});
});
});

View File

@ -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,
});
});
});

View File

@ -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);
});
});

View File

@ -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));

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};
};

View File

@ -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,
],
);

View File

@ -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);
}
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -51,7 +51,7 @@ export const useOpenCreateActivityDrawer = () => {
}) => {
const { createdActivityInCache } = createActivityInCache({
type,
targetableObjects,
targetObject: targetableObjects[0],
customAssignee,
});

View File

@ -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],
);
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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?.({

View File

@ -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 = () => {

View File

@ -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(

View File

@ -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();
});
});

View File

@ -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,
};
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
],
);

View File

@ -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: {

View File

@ -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

View File

@ -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);
});
});

View File

@ -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,
};
};

View File

@ -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,

View File

@ -6,5 +6,4 @@ export type ActivityTargetWithTargetRecord = {
targetObjectMetadataItem: ObjectMetadataItem;
activityTarget: ActivityTarget;
targetObject: ObjectRecord;
targetObjectNameSingular: string;
};

View File

@ -1,5 +1,4 @@
export type ActivityTargetableObject = {
id: string;
targetObjectNameSingular: string;
relatedTargetableObjects?: ActivityTargetableObject[];
};

View File

@ -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');
});
});

View File

@ -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;
};

View File

@ -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,
};
};

View File

@ -0,0 +1,7 @@
export const getActivityTargetObjectFieldName = ({
nameSingular,
}: {
nameSingular: string;
}) => {
return `${nameSingular}`;
};

View File

@ -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,

View File

@ -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;
};

View File

@ -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,
{

View File

@ -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: '',
});

View File

@ -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,
);

View File

@ -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,
);

View File

@ -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: '',
});

View File

@ -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';

View File

@ -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 {

View File

@ -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>
);
};

View File

@ -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');

View File

@ -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,

View File

@ -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;
};

View File

@ -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',
)!,

View File

@ -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
}
}`);
});
});

View File

@ -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);

View File

@ -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}`,
);
}
};

View File

@ -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,
})}
}
}`;

View File

@ -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')}

View File

@ -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;
}

View File

@ -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,
],
);
};

View File

@ -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,
};
};

View 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 };
};

View 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;
};
};

View 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,
});
};
};

View File

@ -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,
};
};

View File

@ -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],

View File

@ -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,
};
};

View File

@ -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,
});
};
};

View File

@ -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<

View File

@ -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({

View 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),
},
],
});
};

View File

@ -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