Activity injection into Apollo cache (#3665)

- Created addRecordInCache to inject a record in Apollo cache and inject single read query on this record
- Created createOneRecordInCache and createManyRecordsInCache that uses this addRecordInCache
- Created useOpenCreateActivityDrawerV2 hook to create an activity in cache and inject it into all other relevant requests in the app before opening activity drawer
- Refactored DEFAULT_SEARCH_REQUEST_LIMIT constant and hardcoded arbitrary request limits
- Added Apollo dev logs to see errors in the console when manipulating cache
This commit is contained in:
Lucas Bordeau
2024-01-29 16:12:52 +01:00
committed by GitHub
parent 64d0e15ada
commit 3b458d5207
57 changed files with 1160 additions and 190 deletions

View File

@ -6,6 +6,7 @@ import { RecoilRoot } from 'recoil';
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect';
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
@ -35,6 +36,7 @@ root.render(
<RecoilRoot>
<AppErrorBoundary>
<RecoilDebugObserverEffect />
<ApolloDevLogEffect />
<BrowserRouter>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>

View File

@ -1,4 +1,3 @@
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
@ -15,41 +14,40 @@ export const useActivityTargetObjectRecords = ({
}) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { records: activityTargets } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: {
activityId: {
eq: activityId,
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: {
activityId: {
eq: activityId,
},
},
},
});
});
const activityTargetObjectRecords = useMemo(() => {
return activityTargets
.map<Nullable<ActivityTargetObjectRecord>>((activityTarget) => {
const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
!objectMetadataItem.isSystem,
);
const activityTargetObjectRecords = activityTargets
.map<Nullable<ActivityTargetObjectRecord>>((activityTarget) => {
const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
!objectMetadataItem.isSystem,
);
if (!correspondingObjectMetadataItem) {
return null;
}
if (!correspondingObjectMetadataItem) {
return null;
}
return {
activityTargetRecord: activityTarget,
targetObjectRecord:
activityTarget[correspondingObjectMetadataItem.nameSingular],
targetObjectMetadataItem: correspondingObjectMetadataItem,
targetObjectNameSingular:
correspondingObjectMetadataItem.nameSingular,
};
})
.filter(isDefined);
}, [activityTargets, objectMetadataItems]);
return {
activityTargetRecord: activityTarget,
targetObjectRecord:
activityTarget[correspondingObjectMetadataItem.nameSingular],
targetObjectMetadataItem: correspondingObjectMetadataItem,
targetObjectNameSingular: correspondingObjectMetadataItem.nameSingular,
};
})
.filter(isDefined);
return {
activityTargetObjectRecords,
loadingActivityTargets,
};
};

View File

@ -1,3 +1,5 @@
import { useState } from 'react';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
@ -13,17 +15,26 @@ export const useActivityTargets = ({
nameSingular: targetableObject.targetObjectNameSingular,
});
const { records: activityTargets } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
const [initialized, setInitialized] = useState(false);
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
},
},
},
skip: !targetableObject.id,
});
onCompleted: () => {
if (!initialized) {
setInitialized(true);
}
},
});
return {
activityTargets: activityTargets as ActivityTarget[],
loadingActivityTargets,
initialized,
};
};

View File

@ -1,76 +0,0 @@
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
export const useHandleCheckableActivityTargetChange = ({
activityId,
currentActivityTargets,
}: {
activityId: string;
currentActivityTargets: any[];
}) => {
const { createOneRecord: createOneActivityTarget } =
useCreateOneRecord<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
return async (
entityValues: Record<string, boolean>,
entitiesToSelect: any,
selectedEntities: any,
) => {
if (!activityId) {
return;
}
const currentActivityTargetRecordIds = currentActivityTargets.map(
({ companyId, personId }) => companyId ?? personId ?? '',
);
const idsToAdd = Object.entries(entityValues)
.filter(
([recordId, value]) =>
value && !currentActivityTargetRecordIds.includes(recordId),
)
.map(([id, _]) => id);
const idsToDelete = Object.entries(entityValues)
.filter(([_, value]) => !value)
.map(([id, _]) => id);
if (idsToAdd.length) {
idsToAdd.forEach((id) => {
const entityFromToSelect = entitiesToSelect.filter(
(entity: any) => entity.id === id,
).length
? entitiesToSelect.filter((entity: any) => entity.id === id)[0]
: null;
const entityFromSelected = selectedEntities.filter(
(entity: any) => entity.id === id,
).length
? selectedEntities.filter((entity: any) => entity.id === id)[0]
: null;
const entity = entityFromToSelect ?? entityFromSelected;
createOneActivityTarget?.({
activityId: activityId,
companyId: entity.record.__typename === 'Company' ? entity.id : null,
personId: entity.record.__typename === 'Person' ? entity.id : null,
});
});
}
if (idsToDelete.length) {
idsToDelete.forEach((id) => {
const currentActivityTargetId = currentActivityTargets.filter(
({ companyId, personId }) => companyId === id || personId === id,
)[0].id;
deleteOneActivityTarget?.(currentActivityTargetId);
});
}
};
};

View File

@ -0,0 +1,46 @@
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

@ -0,0 +1,51 @@
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

@ -1,8 +1,10 @@
import { useRecoilCallback } from 'recoil';
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';
@ -27,21 +29,35 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
getSelectedRowIdsSelector(),
);
let activityTargetableEntityArray: ActivityTargetableObject[] =
selectedRowIds.map((id: string) => ({
type: 'Custom',
targetObjectNameSingular: objectNameSingular,
id,
}));
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 (relatedEntities) {
activityTargetableEntityArray =
activityTargetableEntityArray.concat(relatedEntities);
activityTargetableObjectArray =
activityTargetableObjectArray.concat(relatedEntities);
}
openCreateActivityDrawer({
type,
targetableObjects: activityTargetableEntityArray,
targetableObjects: activityTargetableObjectArray,
});
},
[openCreateActivityDrawer, getSelectedRowIdsSelector],

View File

@ -0,0 +1,163 @@
import { useCallback } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { useModifyActivityOnActivityTargetsCache } from '@/activities/hooks/useModifyActivityOnActivityTargetCache';
import { useModifyActivityTargetsOnActivityCache } from '@/activities/hooks/useModifyActivityTargetsOnActivityCache';
import { useWriteActivityTargetsInCache } from '@/activities/hooks/useWriteActivityTargetsInCache';
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
import { useInjectIntoTimelineActivitiesQuery } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQuery';
import { Activity, ActivityType } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { getActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache';
import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { mapToRecordId } from '@/object-record/utils/mapToObjectId';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState';
import { viewableActivityIdState } from '../states/viewableActivityIdState';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
export const useOpenCreateActivityDrawerV2 = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
const { openRightDrawer } = useRightDrawer();
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
useCreateManyRecordsInCache<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const { createOneRecordInCache: createOneActivityInCache } =
useCreateOneRecordInCache<Activity>({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { record: workspaceMemberRecord } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
objectRecordId: currentWorkspaceMember?.id,
});
const setHotkeyScope = useSetHotkeyScope();
const [, setActivityTargetableEntityArray] = useRecoilState(
activityTargetableEntityArrayState,
);
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
const { activityTargets } = useActivityTargets({
targetableObject,
});
const { injectIntoTimelineActivitiesNextQuery } =
useInjectIntoTimelineActivitiesQuery();
const { injectIntoActivityTargetInlineCellCache } =
useInjectIntoActivityTargetInlineCellCache();
const { injectIntoUseActivityTargets } = useWriteActivityTargetsInCache();
const { modifyActivityTargetsOnActivityCache } =
useModifyActivityTargetsOnActivityCache();
const { modifyActivityOnActivityTargetsCache } =
useModifyActivityOnActivityTargetsCache();
return useCallback(
async ({
type,
targetableObjects,
assigneeId,
}: {
type: ActivityType;
targetableObjects: ActivityTargetableObject[];
assigneeId?: string;
}) => {
const activityId = v4();
const createdActivityInCache = await createOneActivityInCache({
id: activityId,
author: workspaceMemberRecord,
authorId: workspaceMemberRecord?.id,
assignee: !assigneeId ? workspaceMemberRecord : undefined,
assigneeId:
assigneeId ?? isNonEmptyString(workspaceMemberRecord?.id)
? workspaceMemberRecord?.id
: undefined,
type: type,
});
if (!createdActivityInCache) {
return;
}
const activityTargetsToCreate =
getActivityTargetsToCreateFromTargetableObjects({
activityId,
targetableObjects,
});
const createdActivityTargetsInCache =
await createManyActivityTargetsInCache(activityTargetsToCreate);
injectIntoUseActivityTargets({
targetableObject,
activityTargetsToInject: createdActivityTargetsInCache,
});
injectIntoTimelineActivitiesNextQuery({
activityTargets,
activityToInject: createdActivityInCache,
});
injectIntoActivityTargetInlineCellCache({
activityId,
activityTargetsToInject: createdActivityTargetsInCache,
});
modifyActivityTargetsOnActivityCache({
activityId,
activityTargets: createdActivityTargetsInCache,
});
modifyActivityOnActivityTargetsCache({
activityTargetIds: createdActivityTargetsInCache.map(mapToRecordId),
activity: createdActivityInCache,
});
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(activityId);
setActivityTargetableEntityArray(targetableObjects ?? []);
openRightDrawer(RightDrawerPages.CreateActivity);
},
[
openRightDrawer,
setActivityTargetableEntityArray,
createManyActivityTargetsInCache,
setHotkeyScope,
setViewableActivityId,
createOneActivityInCache,
workspaceMemberRecord,
activityTargets,
targetableObject,
injectIntoTimelineActivitiesNextQuery,
injectIntoActivityTargetInlineCellCache,
injectIntoUseActivityTargets,
modifyActivityTargetsOnActivityCache,
modifyActivityOnActivityTargetsCache,
],
);
};

View File

@ -0,0 +1,84 @@
import { useApolloClient } from '@apollo/client';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export const useWriteActivityTargetsInCache = () => {
const apolloClient = useApolloClient();
const {
objectMetadataItem: objectMetadataItemActivityTarget,
findManyRecordsQuery: findManyActivityTargetsQuery,
} = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const injectIntoUseActivityTargets = ({
targetableObject,
activityTargetsToInject,
}: {
targetableObject: Pick<
ActivityTargetableObject,
'id' | 'targetObjectNameSingular'
>;
activityTargetsToInject: ActivityTarget[];
}) => {
const targetObjectFieldName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
const existingActivityTargetsForTargetableObjectQueryResult =
apolloClient.readQuery({
query: findManyActivityTargetsQuery,
variables: {
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
},
},
},
});
const existingActivityTargetsForTargetableObject =
getRecordsFromRecordConnection({
recordConnection: existingActivityTargetsForTargetableObjectQueryResult[
objectMetadataItemActivityTarget.namePlural
] as ObjectRecordConnection<ActivityTarget>,
});
const newActivityTargetsForTargetableObject = [
...existingActivityTargetsForTargetableObject,
...activityTargetsToInject,
];
const newActivityTargetsConnection = getRecordConnectionFromRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
records: newActivityTargetsForTargetableObject,
});
apolloClient.writeQuery({
query: findManyActivityTargetsQuery,
variables: {
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
},
},
},
data: {
[objectMetadataItemActivityTarget.namePlural]:
newActivityTargetsConnection,
},
});
};
return {
injectIntoUseActivityTargets,
};
};

View File

@ -0,0 +1,58 @@
import { useApolloClient } from '@apollo/client';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordConnectionFromEdges } from '@/object-record/cache/utils/getRecordConnectionFromEdges';
import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord';
export const useInjectIntoActivityTargetInlineCellCache = () => {
const apolloClient = useApolloClient();
const {
objectMetadataItem: objectMetadataItemActivityTarget,
findManyRecordsQuery: findManyActivityTargetsQuery,
} = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const injectIntoActivityTargetInlineCellCache = ({
activityId,
activityTargetsToInject,
}: {
activityId: string;
activityTargetsToInject: ActivityTarget[];
}) => {
const newActivityTargetEdgesForCache = activityTargetsToInject.map(
(activityTargetToInject) =>
getRecordEdgeFromRecord({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
record: activityTargetToInject,
}),
);
const newActivityTargetConnectionForCache = getRecordConnectionFromEdges({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
edges: newActivityTargetEdgesForCache,
});
apolloClient.writeQuery({
query: findManyActivityTargetsQuery,
variables: {
filter: {
activityId: {
eq: activityId,
},
},
},
data: {
[objectMetadataItemActivityTarget.namePlural]:
newActivityTargetConnectionForCache,
},
});
};
return {
injectIntoActivityTargetInlineCellCache,
};
};

View File

@ -14,6 +14,7 @@ export const PageAddTaskButton = ({
const { selectedFilter } = useFilterDropdown({
filterDropdownId: filterDropdownId,
});
const openCreateActivity = useOpenCreateActivityDrawer();
const handleClick = () => {

View File

@ -1,14 +1,9 @@
import React from 'react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton';
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { Activity } from '@/activities/types/Activity';
import { useTimelineActivities } from '@/activities/timeline/hooks/useTimelineActivities';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { TimelineItemsContainer } from './TimelineItemsContainer';
@ -55,26 +50,22 @@ export const Timeline = ({
}: {
targetableObject: ActivityTargetableObject;
}) => {
const { activityTargets } = useActivityTargets({ targetableObject });
const { records: activities } = useFindManyRecords({
skip: !activityTargets?.length,
objectNameSingular: CoreObjectNameSingular.Activity,
filter: {
id: {
in: activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString),
},
},
orderBy: {
createdAt: 'AscNullsFirst',
},
const { activities, initialized } = useTimelineActivities({
targetableObject,
});
const openCreateActivity = useOpenCreateActivityDrawer();
if (!activities.length) {
const showEmptyState = initialized && activities.length === 0;
const showLoadingState = !initialized;
if (showLoadingState) {
// TODO: Display a beautiful loading page
return <></>;
}
if (showEmptyState) {
return (
<StyledTimelineEmptyContainer>
<StyledEmptyTimelineTitle>No activity yet</StyledEmptyTimelineTitle>
@ -99,7 +90,7 @@ export const Timeline = ({
return (
<StyledMainContainer>
<TimelineItemsContainer activities={activities as Activity[]} />
<TimelineItemsContainer activities={activities} />
</StyledMainContainer>
);
};

View File

@ -0,0 +1,89 @@
import { useApolloClient } from '@apollo/client';
import { isNonEmptyString } from '@sniptt/guards';
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export const useInjectIntoTimelineActivitiesQuery = () => {
const apolloClient = useApolloClient();
const {
objectMetadataItem: objectMetadataItemActivity,
findManyRecordsQuery: findManyActivitiesQuery,
} = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const injectIntoTimelineActivitiesQuery = ({
activityTargets,
activityToInject,
}: {
activityTargets: ActivityTarget[];
activityToInject: Activity;
}) => {
const activityIds = activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString);
const timelineActivitiesQueryVariables =
makeTimelineActivitiesQueryVariables({
activityIds,
});
const exitistingActivitiesQueryResult = apolloClient.readQuery({
query: findManyActivitiesQuery,
variables: timelineActivitiesQueryVariables,
});
const extistingActivities = exitistingActivitiesQueryResult
? getRecordsFromRecordConnection({
recordConnection: exitistingActivitiesQueryResult[
objectMetadataItemActivity.namePlural
] as ObjectRecordConnection<Activity>,
})
: [];
const newActivity = {
...activityToInject,
__typename: 'Activity',
};
const newActivitiesSortedAsActivitiesQuery = [
newActivity,
...extistingActivities,
];
const newActivityIdsSortedAsActivityTargetsQuery = [
...extistingActivities,
newActivity,
].map((activity) => activity.id);
const newTimelineActivitiesQueryVariables =
makeTimelineActivitiesQueryVariables({
activityIds: newActivityIdsSortedAsActivityTargetsQuery,
});
const newActivityConnectionForCache = getRecordConnectionFromRecords({
objectNameSingular: CoreObjectNameSingular.Activity,
records: newActivitiesSortedAsActivitiesQuery,
});
apolloClient.writeQuery({
query: findManyActivitiesQuery,
variables: newTimelineActivitiesQueryVariables,
data: {
[objectMetadataItemActivity.namePlural]: newActivityConnectionForCache,
},
});
};
return {
injectIntoTimelineActivitiesNextQuery: injectIntoTimelineActivitiesQuery,
};
};

View File

@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { useActivityTargets } from '@/activities/hooks/useActivityTargets';
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 { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useTimelineActivities = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
const {
activityTargets,
loadingActivityTargets,
initialized: initializedActivityTargets,
} = useActivityTargets({
targetableObject,
});
const [initialized, setInitialized] = useState(false);
const [activities, setActivities] = useState<Activity[]>([]);
const activityIds = activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString);
const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables(
{
activityIds,
},
);
const { records: activitiesFromRequest, loading: loadingActivities } =
useFindManyRecords<Activity>({
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
objectNameSingular: CoreObjectNameSingular.Activity,
filter: timelineActivitiesQueryVariables.filter,
orderBy: timelineActivitiesQueryVariables.orderBy,
onCompleted: () => {
if (!initialized) {
setInitialized(true);
}
},
});
useEffect(() => {
if (!loadingActivities) {
setActivities(activitiesFromRequest);
}
}, [activitiesFromRequest, loadingActivities]);
const noActivityTargets =
initializedActivityTargets && !isNonEmptyArray(activityTargets);
useEffect(() => {
if (noActivityTargets) {
setInitialized(true);
}
}, [noActivityTargets]);
const loading = loadingActivities || loadingActivityTargets;
return {
activities,
loading,
initialized,
};
};

View File

@ -0,0 +1,18 @@
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
export const makeTimelineActivitiesQueryVariables = ({
activityIds,
}: {
activityIds: string[];
}): ObjectRecordQueryVariables => {
return {
filter: {
id: {
in: activityIds,
},
},
orderBy: {
createdAt: 'AscNullsFirst',
},
};
};

View File

@ -1,5 +1,8 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export type ActivityTargetableObject = {
id: string;
targetObjectNameSingular: string;
targetObjectRecord: ObjectRecord;
relatedTargetableObjects?: ActivityTargetableObject[];
};

View File

@ -0,0 +1,38 @@
import { v4 } from 'uuid';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
export const getActivityTargetsToCreateFromTargetableObjects = ({
targetableObjects,
activityId,
}: {
targetableObjects: ActivityTargetableObject[];
activityId: string;
}): Partial<ActivityTarget>[] => {
const activityTargetableObjects = targetableObjects
? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
targetableObjects,
)
: [];
const activityTargetsToCreate = activityTargetableObjects.map(
(targetableObject) => {
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
return {
[targetableObject.targetObjectNameSingular]:
targetableObject.targetObjectRecord,
[targetableObjectFieldIdName]: targetableObject.id,
activityId,
id: v4(),
};
},
);
return activityTargetsToCreate;
};

View File

@ -1,3 +1,5 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export type CachedObjectRecord = ObjectRecord & { __typename: string };
export type CachedObjectRecord<T extends ObjectRecord = ObjectRecord> = T & {
__typename: string;
};

View File

@ -0,0 +1,18 @@
import { useEffect } from 'react';
import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev';
import { useRecoilValue } from 'recoil';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
export const ApolloDevLogEffect = () => {
const isDebugMode = useRecoilValue(isDebugModeState);
useEffect(() => {
if (isDebugMode) {
loadDevMessages();
loadErrorMessages();
}
}, [isDebugMode]);
return null;
};

View File

@ -10,6 +10,8 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
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';
@ -17,8 +19,6 @@ import { useGenerateExecuteQuickActionOnOneRecordMutation } from '@/object-recor
import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery';
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
import { useGenerateUpdateOneRecordMutation } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
import { useGetRecordFromCache } from '@/object-record/hooks/useGetRecordFromCache';
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
import { generateDeleteOneRecordMutation } from '@/object-record/utils/generateDeleteOneRecordMutation';
import { isDefined } from '~/utils/isDefined';

View File

@ -0,0 +1,72 @@
import { useApolloClient } from '@apollo/client';
import gql from 'graphql-tag';
import { useRecoilCallback } from 'recoil';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
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 mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
const apolloClient = useApolloClient();
const generateFindOneRecordQuery = useGenerateFindOneRecordQuery();
const findOneRecordQuery = generateFindOneRecordQuery({
objectMetadataItem,
});
return useRecoilCallback(
({ set }) =>
(record: ObjectRecord) => {
apolloClient.writeFragment({
id: `${capitalize(objectMetadataItem.nameSingular)}:${record.id}`,
fragment: gql`
fragment Create${capitalize(
objectMetadataItem.nameSingular,
)}InCache on ${capitalize(objectMetadataItem.nameSingular)} {
__typename
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))
.join('\n')}
}
`,
data: {
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
...record,
},
});
// TODO: Turn into injectIntoFindOneRecordQueryCache
apolloClient.writeQuery({
query: findOneRecordQuery,
variables: {
objectRecordId: record.id,
},
data: {
[objectMetadataItem.nameSingular]: {
__typename: `${capitalize(objectMetadataItem.nameSingular)}`,
...record,
},
},
});
// TODO: remove this once we get rid of entityFieldsFamilyState
set(recordStoreFamilyState(record.id), record);
},
[
objectMetadataItem,
apolloClient,
mapFieldMetadataToGraphQLQuery,
findOneRecordQuery,
],
);
};

View File

@ -0,0 +1,31 @@
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;
};

View File

@ -0,0 +1,43 @@
import { ApolloClient, makeReference } from '@apollo/client';
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
import { getCachedRecordFromRecord } from '@/object-record/cache/utils/getCachedRecordFromRecord';
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const getCachedRecordEdgesFromRecords = <T extends ObjectRecord>({
apolloClient,
objectNameSingular,
records,
}: {
apolloClient: ApolloClient<object>;
objectNameSingular: string;
records: T[];
}): CachedObjectRecordEdge[] => {
const cachedRecordEdges = records.map((record) => {
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 reference = makeReference(id);
const cachedObjectRecordEdge: CachedObjectRecordEdge = {
cursor: '',
node: reference,
__typename: getEdgeTypename({ objectNameSingular }),
};
return cachedObjectRecordEdge;
});
return cachedRecordEdges;
};

View File

@ -0,0 +1,16 @@
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const getCachedRecordFromRecord = <T extends ObjectRecord>({
objectNameSingular,
record,
}: {
objectNameSingular: string;
record: T;
}): CachedObjectRecord<T> => {
return {
__typename: getNodeTypename({ objectNameSingular }),
...record,
};
};

View File

@ -0,0 +1,9 @@
import { capitalize } from '~/utils/string/capitalize';
export const getConnectionTypename = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return `${capitalize(objectNameSingular)}Connection`;
};

View File

@ -0,0 +1,9 @@
import { capitalize } from '~/utils/string/capitalize';
export const getEdgeTypename = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return `${capitalize(objectNameSingular)}Edge`;
};

View File

@ -0,0 +1,8 @@
export const getEmptyPageInfo = () => {
return {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
};
};

View File

@ -0,0 +1,9 @@
import { capitalize } from '~/utils/string/capitalize';
export const getNodeTypename = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return capitalize(objectNameSingular);
};

View File

@ -0,0 +1,19 @@
import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename';
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
export const getRecordConnectionFromEdges = <T extends ObjectRecord>({
objectNameSingular,
edges,
}: {
objectNameSingular: string;
edges: ObjectRecordEdge<T>[];
}) => {
return {
__typename: getConnectionTypename({ objectNameSingular }),
edges: edges,
pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection<T>;
};

View File

@ -0,0 +1,24 @@
import { getConnectionTypename } from '@/object-record/cache/utils/getConnectionTypename';
import { getEmptyPageInfo } from '@/object-record/cache/utils/getEmptyPageInfo';
import { getRecordEdgeFromRecord } from '@/object-record/cache/utils/getRecordEdgeFromRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export const getRecordConnectionFromRecords = <T extends ObjectRecord>({
objectNameSingular,
records,
}: {
objectNameSingular: string;
records: T[];
}) => {
return {
__typename: getConnectionTypename({ objectNameSingular }),
edges: records.map((record) => {
return getRecordEdgeFromRecord({
objectNameSingular,
record,
});
}),
pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection<T>;
};

View File

@ -0,0 +1,21 @@
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
export const getRecordEdgeFromRecord = <T extends ObjectRecord>({
objectNameSingular,
record,
}: {
objectNameSingular: string;
record: T;
}) => {
return {
__typename: getEdgeTypename({ objectNameSingular }),
node: {
__typename: getNodeTypename({ objectNameSingular }),
...record,
},
cursor: '',
} as ObjectRecordEdge<T>;
};

View File

@ -0,0 +1,10 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
export const getRecordsFromRecordConnection = <T extends ObjectRecord>({
recordConnection,
}: {
recordConnection: ObjectRecordConnection<T>;
}): T[] => {
return recordConnection.edges.map((edge) => edge.node);
};

View File

@ -0,0 +1 @@
export const DEFAULT_SEARCH_REQUEST_LIMIT = 60;

View File

@ -5,7 +5,7 @@ export const query = gql`
$filter: PersonFilterInput
$orderBy: PersonOrderByInput
$lastCursor: String
$limit: Float = 30
$limit: Float
) {
people(
filter: $filter

View File

@ -5,7 +5,7 @@ import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider addTypename={false}>

View File

@ -3,7 +3,7 @@ import { useApolloClient } from '@apollo/client';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useGenerateCachedObjectRecord } from '@/object-record/hooks/useGenerateCachedObjectRecord';
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';

View File

@ -0,0 +1,48 @@
import { v4 } from 'uuid';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const useCreateManyRecordsInCache = <T extends ObjectRecord>({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
objectMetadataItem,
});
const addRecordInCache = useAddRecordInCache({
objectMetadataItem,
});
const createManyRecordsInCache = async (data: Partial<T>[]) => {
const recordsWithId = data.map((record) => ({
...record,
id: (record.id as string) ?? v4(),
}));
const createdRecordsInCache = [] as T[];
for (const record of recordsWithId) {
const generatedCachedObjectRecord = generateCachedObjectRecord<T>({
...record,
});
if (generatedCachedObjectRecord) {
addRecordInCache(generatedCachedObjectRecord);
createdRecordsInCache.push(generatedCachedObjectRecord);
}
}
return createdRecordsInCache;
};
return { createManyRecordsInCache };
};

View File

@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGenerateCachedObjectRecord } from '@/object-record/hooks/useGenerateCachedObjectRecord';
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord';
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';

View File

@ -0,0 +1,39 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useAddRecordInCache } from '@/object-record/cache/hooks/useAddRecordInCache';
import { useGenerateCachedObjectRecord } from '@/object-record/cache/hooks/useGenerateCachedObjectRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
type useCreateOneRecordInCacheProps = {
objectNameSingular: string;
};
export const useCreateOneRecordInCache = <T>({
objectNameSingular,
}: useCreateOneRecordInCacheProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
objectMetadataItem,
});
const addRecordInCache = useAddRecordInCache({
objectMetadataItem,
});
const createOneRecordInCache = async (input: ObjectRecord) => {
const generatedCachedObjectRecord = generateCachedObjectRecord({
createdAt: new Date().toISOString(),
...input,
});
addRecordInCache(generatedCachedObjectRecord);
return generatedCachedObjectRecord as T;
};
return {
createOneRecordInCache,
};
};

View File

@ -13,7 +13,6 @@ import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnec
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { logError } from '~/utils/logError';
import { capitalize } from '~/utils/string/capitalize';
@ -28,7 +27,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
filter,
orderBy,
limit = DEFAULT_SEARCH_REQUEST_LIMIT,
limit,
onCompleted,
skip,
useRecordsWithoutConnection = false,

View File

@ -20,7 +20,7 @@ export const useGenerateFindManyRecordsQuery = () => {
objectMetadataItem.nameSingular,
)}FilterInput, $orderBy: ${capitalize(
objectMetadataItem.nameSingular,
)}OrderByInput, $lastCursor: String, $limit: Float = 60) {
)}OrderByInput, $lastCursor: String, $limit: Float) {
${
objectMetadataItem.namePlural
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){

View File

@ -10,7 +10,7 @@ import {
URLFilter,
UUIDFilter,
} from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { Field } from '~/generated/graphql';
@ -260,5 +260,5 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
}
}
return andFilterVariables(objectRecordFilters);
return makeAndFilterVariables(objectRecordFilters);
};

View File

@ -238,16 +238,21 @@ export const RecordShowContainer = ({
</>
)}
</ShowPageLeftContainer>
<ShowPageRightContainer
targetableObject={{
id: record?.id ?? '',
targetObjectNameSingular: objectMetadataItem?.nameSingular,
}}
timeline
tasks
notes
emails
/>
{record ? (
<ShowPageRightContainer
targetableObject={{
id: record.id,
targetObjectNameSingular: objectMetadataItem?.nameSingular,
targetObjectRecord: record,
}}
timeline
tasks
notes
emails
/>
) : (
<></>
)}
</ShowPageContainer>
</RecoilScope>
);

View File

@ -1,5 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
import { isDefined } from '~/utils/isDefined';
import { capitalize } from '~/utils/string/capitalize';

View File

@ -5,7 +5,7 @@ import { useMultiObjectSearchSelectedItemsQuery } from '@/object-record/relation
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
export const DEFAULT_SEARCH_REQUEST_LIMIT = 5;
export const MULTI_OBJECT_SEARCH_REQUEST_LIMIT = 5;
export type ObjectRecordForSelect = {
objectMetadataItem: ObjectMetadataItem;

View File

@ -13,7 +13,7 @@ import {
import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem';
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
import { isDefined } from '~/utils/isDefined';
import { capitalize } from '~/utils/string/capitalize';
@ -69,7 +69,7 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
return [
`filter${capitalize(nameSingular)}`,
andFilterVariables(searchFilters),
makeAndFilterVariables(searchFilters),
];
})
.filter(isDefined),

View File

@ -2,13 +2,12 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields';
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
import { orFilterVariables } from '@/object-record/utils/orFilterVariables';
export const DEFAULT_SEARCH_REQUEST_LIMIT = 60;
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
export const useRecordsForSelect = ({
searchFilterText,
@ -53,7 +52,7 @@ export const useRecordsForSelect = ({
return undefined;
}
return orFilterVariables(
return makeOrFilterVariables(
fieldNames.map((fieldName) => {
const [parentFieldName, subFieldName] = fieldName.split('.');
@ -81,7 +80,7 @@ export const useRecordsForSelect = ({
loading: filteredSelectedRecordsLoading,
records: filteredSelectedRecordsData,
} = useFindManyRecords({
filter: andFilterVariables([...searchFilters, selectedIdsFilter]),
filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]),
orderBy: orderByField,
objectNameSingular,
skip: !selectedIds.length,
@ -93,7 +92,7 @@ export const useRecordsForSelect = ({
: undefined;
const { loading: recordsToSelectLoading, records: recordsToSelectData } =
useFindManyRecords({
filter: andFilterVariables([...searchFilters, notFilter]),
filter: makeAndFilterVariables([...searchFilters, notFilter]),
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
orderBy: orderByField,
objectNameSingular,

View File

@ -1,5 +1,8 @@
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from '~/generated/graphql';
import { capitalize } from '~/utils/string/capitalize';
export const generateEmptyFieldValue = (
fieldMetadataItem: FieldMetadataItem,
@ -39,7 +42,21 @@ export const generateEmptyFieldValue = (
return true;
}
case FieldMetadataType.Relation: {
return null;
if (
!isNonEmptyString(
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata
?.nameSingular,
)
) {
return null;
}
return {
__typename: `${capitalize(
fieldMetadataItem.fromRelationMetadata.toObjectMetadata.nameSingular,
)}Connection`,
edges: [],
};
}
case FieldMetadataType.Currency: {
return {

View File

@ -1,7 +1,7 @@
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { isDefined } from '~/utils/isDefined';
export const andFilterVariables = (
export const makeAndFilterVariables = (
filters: (ObjectRecordQueryFilter | undefined)[],
): ObjectRecordQueryFilter | undefined => {
const definedFilters = filters.filter(isDefined);

View File

@ -1,7 +1,7 @@
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { isDefined } from '~/utils/isDefined';
export const orFilterVariables = (
export const makeOrFilterVariables = (
filters: (ObjectRecordQueryFilter | undefined)[],
): ObjectRecordQueryFilter | undefined => {
const definedFilters = filters.filter(isDefined);

View File

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

View File

@ -5,7 +5,7 @@ export const query = gql`
$filter: PersonFilterInput
$orderBy: PersonOrderByInput
$lastCursor: String
$limit: Float = 30
$limit: Float = 60
) {
people(
filter: $filter

View File

@ -2,18 +2,17 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { OrderBy } from '@/object-metadata/types/OrderBy';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
import { orFilterVariables } from '@/object-record/utils/orFilterVariables';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
import { assertNotNull } from '~/utils/assert';
type SearchFilter = { fieldNames: string[]; filter: string | number };
export const DEFAULT_SEARCH_REQUEST_LIMIT = 60;
// TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search
// Filtered entities to select are
@ -56,7 +55,7 @@ export const useFilteredSearchEntityQuery = ({
return undefined;
}
return orFilterVariables(
return makeOrFilterVariables(
fieldNames.map((fieldName) => {
const [parentFieldName, subFieldName] = fieldName.split('.');
@ -85,7 +84,7 @@ export const useFilteredSearchEntityQuery = ({
records: filteredSelectedRecords,
} = useFindManyRecords({
objectNameSingular,
filter: andFilterVariables([...searchFilters, selectedIdsFilter]),
filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]),
orderBy: { [orderByField]: sortOrder },
skip: !selectedIds.length,
});
@ -97,7 +96,7 @@ export const useFilteredSearchEntityQuery = ({
const { loading: recordsToSelectLoading, records: recordsToSelect } =
useFindManyRecords({
objectNameSingular,
filter: andFilterVariables([...searchFilters, notFilter]),
filter: makeAndFilterVariables([...searchFilters, notFilter]),
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
orderBy: { [orderByField]: sortOrder },
});

View File

@ -92,6 +92,7 @@ export const RecordShowPage = () => {
entity={{
id: record.id,
targetObjectNameSingular: objectMetadataItem?.nameSingular,
targetObjectRecord: record,
}}
/>
<ShowPageMoreButton