Feat/activity optimistic activities (#4009)

* Fix naming

* Fixed cache.evict bug for relation target deletion

* Fixed cascade delete activity targets

* Working version

* Fix

* fix

* WIP

* Fixed optimistic effect target inline cell

* Removed openCreateActivityDrawer v1

* Ok for timeline

* Removed console.log

* Fix update record optimistic effect

* Refactored activity queries into useActivities for everything

* Fixed bugs

* Cleaned

* Fix lint

---------

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

View File

@ -218,7 +218,8 @@ export const PageChangeEffect = () => {
label: 'Create Task', label: 'Create Task',
type: CommandType.Create, type: CommandType.Create,
Icon: IconCheckbox, Icon: IconCheckbox,
onCommandClick: () => openCreateActivity({ type: 'Task' }), onCommandClick: () =>
openCreateActivity({ type: 'Task', targetableObjects: [] }),
}, },
]); ]);
}, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]); }, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]);

View File

@ -1,20 +1,22 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback } from 'react';
import { BlockNoteEditor } from '@blocknote/core'; import { BlockNoteEditor } from '@blocknote/core';
import { useBlockNote } from '@blocknote/react'; import { useBlockNote } from '@blocknote/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isArray, isNonEmptyString } from '@sniptt/guards'; import { isArray, isNonEmptyString } from '@sniptt/guards';
import { useRecoilState } from 'recoil'; import { useRecoilCallback, useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
@ -42,10 +44,6 @@ export const ActivityBodyEditor = ({
activity, activity,
fillTitleFromBody, fillTitleFromBody,
}: ActivityBodyEditorProps) => { }: ActivityBodyEditorProps) => {
const [stringifiedBodyFromEditor, setStringifiedBodyFromEditor] = useState<
string | null
>(activity.body);
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState( const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
activityTitleHasBeenSetFamilyState({ activityTitleHasBeenSetFamilyState({
activityId: activity.id, activityId: activity.id,
@ -96,19 +94,21 @@ export const ActivityBodyEditor = ({
const blockBody = JSON.parse(newStringifiedBody); const blockBody = JSON.parse(newStringifiedBody);
const newTitleFromBody = blockBody[0]?.content?.[0]?.text; const newTitleFromBody = blockBody[0]?.content?.[0]?.text;
modifyActivityFromCache(activity.id, {
title: () => {
return newTitleFromBody;
},
});
persistTitleAndBodyDebounced(newTitleFromBody, newStringifiedBody); persistTitleAndBodyDebounced(newTitleFromBody, newStringifiedBody);
}, },
[activity.id, modifyActivityFromCache, persistTitleAndBodyDebounced], [persistTitleAndBodyDebounced],
);
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
canCreateActivityState,
); );
const handleBodyChange = useCallback( const handleBodyChange = useCallback(
(activityBody: string) => { (activityBody: string) => {
if (!canCreateActivity) {
setCanCreateActivity(true);
}
if (!activityTitleHasBeenSet && fillTitleFromBody) { if (!activityTitleHasBeenSet && fillTitleFromBody) {
updateTitleAndBody(activityBody); updateTitleAndBody(activityBody);
} else { } else {
@ -120,18 +120,11 @@ export const ActivityBodyEditor = ({
persistBodyDebounced, persistBodyDebounced,
activityTitleHasBeenSet, activityTitleHasBeenSet,
updateTitleAndBody, updateTitleAndBody,
setCanCreateActivity,
canCreateActivity,
], ],
); );
useEffect(() => {
if (
isNonEmptyString(stringifiedBodyFromEditor) &&
activity.body !== stringifiedBodyFromEditor
) {
handleBodyChange(stringifiedBodyFromEditor);
}
}, [stringifiedBodyFromEditor, handleBodyChange, activity]);
const slashMenuItems = getSlashMenu(); const slashMenuItems = getSlashMenu();
const [uploadFile] = useUploadFileMutation(); const [uploadFile] = useUploadFileMutation();
@ -160,9 +153,57 @@ export const ActivityBodyEditor = ({
? JSON.parse(activity.body) ? JSON.parse(activity.body)
: undefined, : undefined,
domAttributes: { editor: { class: 'editor' } }, domAttributes: { editor: { class: 'editor' } },
onEditorContentChange: (editor: BlockNoteEditor) => { onEditorContentChange: useRecoilCallback(
setStringifiedBodyFromEditor(JSON.stringify(editor.topLevelBlocks) ?? ''); ({ snapshot, set }) =>
}, (editor: BlockNoteEditor) => {
const newStringifiedBody =
JSON.stringify(editor.topLevelBlocks) ?? '';
set(recordStoreFamilyState(activity.id), (oldActivity) => {
return {
...oldActivity,
id: activity.id,
body: newStringifiedBody,
};
});
modifyActivityFromCache(activity.id, {
body: () => {
return newStringifiedBody;
},
});
const activityTitleHasBeenSet = snapshot
.getLoadable(
activityTitleHasBeenSetFamilyState({
activityId: activity.id,
}),
)
.getValue();
const blockBody = JSON.parse(newStringifiedBody);
const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string;
if (!activityTitleHasBeenSet && fillTitleFromBody) {
set(recordStoreFamilyState(activity.id), (oldActivity) => {
return {
...oldActivity,
id: activity.id,
title: newTitleFromBody,
};
});
modifyActivityFromCache(activity.id, {
title: () => {
return newTitleFromBody;
},
});
}
handleBodyChange(newStringifiedBody);
},
[activity, fillTitleFromBody, modifyActivityFromCache, handleBodyChange],
),
slashMenuItems, slashMenuItems,
blockSpecs: blockSpecs, blockSpecs: blockSpecs,
uploadFile: handleUploadAttachment, uploadFile: handleUploadAttachment,

View File

@ -8,7 +8,9 @@ import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdo
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext'; import { useFieldContext } from '@/object-record/hooks/useFieldContext';
@ -18,6 +20,7 @@ import {
} from '@/object-record/record-field/contexts/FieldContext'; } from '@/object-record/record-field/contexts/FieldContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@ -78,8 +81,10 @@ export const ActivityEditor = ({
const { deleteActivityFromCache } = useDeleteActivityFromCache(); const { deleteActivityFromCache } = useDeleteActivityFromCache();
const useUpsertOneActivityMutation: RecordUpdateHook = () => { const useUpsertOneActivityMutation: RecordUpdateHook = () => {
const upsertActivityMutation = ({ variables }: RecordUpdateHookParams) => { const upsertActivityMutation = async ({
upsertActivity({ activity, input: variables.updateOneRecordInput }); variables,
}: RecordUpdateHookParams) => {
await upsertActivity({ activity, input: variables.updateOneRecordInput });
}; };
return [upsertActivityMutation, { loading: false }]; return [upsertActivityMutation, { loading: false }];
@ -104,6 +109,20 @@ export const ActivityEditor = ({
customUseUpdateOneObjectHook: useUpsertOneActivityMutation, customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
}); });
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
isActivityInCreateModeState,
);
const [isUpsertingActivityInDB] = useRecoilState(
isUpsertingActivityInDBState,
);
const [canCreateActivity] = useRecoilState(canCreateActivityState);
const [activityFromStore] = useRecoilState(
recordStoreFamilyState(activity.id),
);
const { FieldContextProvider: ActivityTargetsContextProvider } = const { FieldContextProvider: ActivityTargetsContextProvider } =
useFieldContext({ useFieldContext({
objectNameSingular: CoreObjectNameSingular.Activity, objectNameSingular: CoreObjectNameSingular.Activity,
@ -112,16 +131,40 @@ export const ActivityEditor = ({
fieldPosition: 2, fieldPosition: 2,
}); });
const [isCreatingActivity, setIsCreatingActivity] = useRecoilState(
isCreatingActivityState,
);
useRegisterClickOutsideListenerCallback({ useRegisterClickOutsideListenerCallback({
callbackId: 'activity-editor', callbackId: 'activity-editor',
callbackFunction: () => { callbackFunction: () => {
if (isCreatingActivity) { if (isUpsertingActivityInDB || !activityFromStore) {
setIsCreatingActivity(false); return;
deleteActivityFromCache(activity); }
if (isActivityInCreateMode) {
if (canCreateActivity) {
upsertActivity({
activity,
input: {
title: activityFromStore.title,
body: activityFromStore.body,
},
});
} else {
deleteActivityFromCache(activity);
}
setIsActivityInCreateMode(false);
} else {
if (
activityFromStore.title !== activity.title ||
activityFromStore.body !== activity.body
) {
upsertActivity({
activity,
input: {
title: activityFromStore.title,
body: activityFromStore.body,
},
});
}
} }
}, },
}); });

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject'; import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { RecordChip } from '@/object-record/components/RecordChip'; import { RecordChip } from '@/object-record/components/RecordChip';
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -12,14 +12,14 @@ const StyledContainer = styled.div`
export const ActivityTargetChips = ({ export const ActivityTargetChips = ({
activityTargetObjectRecords, activityTargetObjectRecords,
}: { }: {
activityTargetObjectRecords: ActivityTargetObjectRecord[]; activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
}) => { }) => {
return ( return (
<StyledContainer> <StyledContainer>
{activityTargetObjectRecords?.map((activityTargetObjectRecord) => ( {activityTargetObjectRecords?.map((activityTargetObjectRecord) => (
<RecordChip <RecordChip
key={activityTargetObjectRecord.targetObjectRecord.id} key={activityTargetObjectRecord.targetObject.id}
record={activityTargetObjectRecord.targetObjectRecord} record={activityTargetObjectRecord.targetObject}
objectNameSingular={ objectNameSingular={
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
} }

View File

@ -1,17 +1,19 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache'; import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { import {
Checkbox, Checkbox,
CheckboxShape, CheckboxShape,
@ -57,7 +59,13 @@ type ActivityTitleProps = {
}; };
export const ActivityTitle = ({ activity }: ActivityTitleProps) => { export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
const [internalTitle, setInternalTitle] = useState(activity.title); const [activityInStore, setActivityInStore] = useRecoilState(
recordStoreFamilyState(activity.id),
);
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
canCreateActivityState,
);
const { upsertActivity } = useUpsertActivity(); const { upsertActivity } = useUpsertActivity();
@ -115,7 +123,17 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
}, 500); }, 500);
const handleTitleChange = (newTitle: string) => { const handleTitleChange = (newTitle: string) => {
setInternalTitle(newTitle); setActivityInStore((currentActivity) => {
return {
...currentActivity,
id: activity.id,
title: newTitle,
};
});
if (isNonEmptyString(newTitle) && !canCreateActivity) {
setCanCreateActivity(true);
}
modifyActivityFromCache(activity.id, { modifyActivityFromCache(activity.id, {
title: () => { title: () => {
@ -153,7 +171,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
ref={titleInputRef} ref={titleInputRef}
placeholder={`${activity.type} title`} placeholder={`${activity.type} title`}
onChange={(event) => handleTitleChange(event.target.value)} onChange={(event) => handleTitleChange(event.target.value)}
value={internalTitle} value={activityInStore?.title ?? ''}
completed={completed} completed={completed}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}

View File

@ -0,0 +1,120 @@
import { useEffect, useState } from 'react';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback } from 'recoil';
import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects';
import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
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';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const useActivities = ({
targetableObjects,
activitiesFilters,
activitiesOrderByVariables,
skip,
skipActivityTargets,
}: {
targetableObjects: ActivityTargetableObject[];
activitiesFilters: ObjectRecordQueryFilter;
activitiesOrderByVariables: OrderByField;
skip?: boolean;
skipActivityTargets?: boolean;
}) => {
const [initialized, setInitialized] = useState(false);
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
const {
activityTargets,
loadingActivityTargets,
initialized: initializedActivityTargets,
} = useActivityTargetsForTargetableObjects({
targetableObjects,
skip: skipActivityTargets || skip,
});
const activityIds = activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString)
.toSorted(sortByAscString);
const activityTargetsFound =
initializedActivityTargets && isNonEmptyArray(activityTargets);
const filter: ObjectRecordQueryFilter = {
id: activityTargetsFound
? {
in: activityIds,
}
: undefined,
...activitiesFilters,
};
const skipActivities =
skip ||
(!skipActivityTargets &&
(!initializedActivityTargets || !activityTargetsFound));
const { records: activitiesWithConnection, loading: loadingActivities } =
useFindManyRecords<Activity>({
skip: skipActivities,
objectNameSingular: CoreObjectNameSingular.Activity,
filter,
orderBy: activitiesOrderByVariables,
onCompleted: useRecoilCallback(
({ set }) =>
(data) => {
if (!initialized) {
setInitialized(true);
}
const activities = getRecordsFromRecordConnection({
recordConnection: data,
});
for (const activity of activities) {
set(recordStoreFamilyState(activity.id), activity);
}
},
[initialized],
),
});
const loading = loadingActivities || loadingActivityTargets;
// TODO: fix connection in relation => automatically change to an array
const activities = activitiesWithConnection
?.map(makeActivityWithoutConnection as any)
.map(({ activity }: any) => activity);
const noActivities =
(!activityTargetsFound && !skipActivityTargets && initialized) ||
(initialized && !loading && !isNonEmptyArray(activities));
useEffect(() => {
if (skipActivities || noActivities) {
setInitialized(true);
}
}, [
activities,
initialized,
loading,
noActivities,
skipActivities,
skipActivityTargets,
]);
return {
activities,
loading,
initialized,
noActivities,
};
};

View File

@ -1,34 +1,26 @@
import { useSetRecoilState } from 'recoil';
import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils'; import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
const QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS = 3; const QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS = 3;
export const useActivityById = ({ activityId }: { activityId: string }) => { export const useActivityById = ({ activityId }: { activityId: string }) => {
const setEntityFields = useSetRecoilState(recordStoreFamilyState(activityId));
const { makeActivityWithoutConnection } = useActivityConnectionUtils(); const { makeActivityWithoutConnection } = useActivityConnectionUtils();
const { record: activityWithConnections } = useFindOneRecord({ // TODO: fix connection in relation => automatically change to an array
const { record: activityWithConnections, loading } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.Activity, objectNameSingular: CoreObjectNameSingular.Activity,
objectRecordId: activityId, objectRecordId: activityId,
skip: !activityId, skip: !activityId,
onCompleted: (activityWithConnections: any) => {
const { activity } = makeActivityWithoutConnection(
activityWithConnections,
);
setEntityFields(activity);
},
depth: QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS, depth: QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS,
}); });
const { activity } = makeActivityWithoutConnection(activityWithConnections); const { activity } = activityWithConnections
? makeActivityWithoutConnection(activityWithConnections as any)
: { activity: null };
return { return {
activity, activity,
loading,
}; };
}; };

View File

@ -1,7 +1,8 @@
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject'; import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
@ -16,7 +17,7 @@ export const useActivityTargetObjectRecords = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { records: activityTargets, loading: loadingActivityTargets } = const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({ useFindManyRecords<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget, objectNameSingular: CoreObjectNameSingular.ActivityTarget,
skip: !isNonEmptyString(activityId), skip: !isNonEmptyString(activityId),
filter: { filter: {
@ -27,7 +28,7 @@ export const useActivityTargetObjectRecords = ({
}); });
const activityTargetObjectRecords = activityTargets const activityTargetObjectRecords = activityTargets
.map<Nullable<ActivityTargetObjectRecord>>((activityTarget) => { .map<Nullable<ActivityTargetWithTargetRecord>>((activityTarget) => {
const correspondingObjectMetadataItem = objectMetadataItems.find( const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => (objectMetadataItem) =>
isDefined(activityTarget[objectMetadataItem.nameSingular]) && isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
@ -39,8 +40,8 @@ export const useActivityTargetObjectRecords = ({
} }
return { return {
activityTargetRecord: activityTarget, activityTarget: activityTarget,
targetObjectRecord: targetObject:
activityTarget[correspondingObjectMetadataItem.nameSingular], activityTarget[correspondingObjectMetadataItem.nameSingular],
targetObjectMetadataItem: correspondingObjectMetadataItem, targetObjectMetadataItem: correspondingObjectMetadataItem,
targetObjectNameSingular: correspondingObjectMetadataItem.nameSingular, targetObjectNameSingular: correspondingObjectMetadataItem.nameSingular,

View File

@ -22,6 +22,9 @@ export const useActivityTargetsForTargetableObject = ({
const skipRequest = !isNonEmptyString(targetableObjectId); const skipRequest = !isNonEmptyString(targetableObjectId);
// 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 } = const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({ useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget, objectNameSingular: CoreObjectNameSingular.ActivityTarget,

View File

@ -0,0 +1,42 @@
import { useState } from 'react';
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 { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useActivityTargetsForTargetableObjects = ({
targetableObjects,
skip,
}: {
targetableObjects: ActivityTargetableObject[];
skip?: boolean;
}) => {
const activityTargetsFilter = getActivityTargetsFilter({
targetableObjects: targetableObjects,
});
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({
skip,
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: activityTargetsFilter,
onCompleted: () => {
if (!initialized) {
setInitialized(true);
}
},
});
return {
activityTargets: activityTargets as ActivityTarget[],
loadingActivityTargets,
initialized,
};
};

View File

@ -1,10 +1,8 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections'; import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections';
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
import { useInjectIntoTimelineActivitiesQueryAfterDrawerMount } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueryAfterDrawerMount';
import { Activity, ActivityType } from '@/activities/types/Activity'; import { Activity, ActivityType } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
@ -14,6 +12,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache'; import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache';
import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache'; import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
export const useCreateActivityInCache = () => { export const useCreateActivityInCache = () => {
const { createManyRecordsInCache: createManyActivityTargetsInCache } = const { createManyRecordsInCache: createManyActivityTargetsInCache } =
@ -28,46 +27,36 @@ export const useCreateActivityInCache = () => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { record: workspaceMemberRecord } = useFindOneRecord({ const { record: currentWorkspaceMemberRecord } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember, objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
objectRecordId: currentWorkspaceMember?.id, objectRecordId: currentWorkspaceMember?.id,
depth: 3, depth: 3,
}); });
const { injectIntoTimelineActivitiesQueryAfterDrawerMount } =
useInjectIntoTimelineActivitiesQueryAfterDrawerMount();
const { injectIntoActivityTargetInlineCellCache } = const { injectIntoActivityTargetInlineCellCache } =
useInjectIntoActivityTargetInlineCellCache(); useInjectIntoActivityTargetInlineCellCache();
const { const { attachRelationInBothDirections } =
attachRelationInBothDirections: useAttachRelationInBothDirections();
attachRelationSourceRecordToItsRelationTargetRecordsAndViceVersaInCache,
} = useAttachRelationInBothDirections();
const createActivityInCache = ({ const createActivityInCache = ({
type, type,
targetableObjects, targetableObjects,
timelineTargetableObject, customAssignee,
assigneeId,
}: { }: {
type: ActivityType; type: ActivityType;
targetableObjects: ActivityTargetableObject[]; targetableObjects: ActivityTargetableObject[];
timelineTargetableObject: ActivityTargetableObject; customAssignee?: WorkspaceMember;
assigneeId?: string;
}) => { }) => {
const activityId = v4(); const activityId = v4();
const createdActivityInCache = createOneActivityInCache({ const createdActivityInCache = createOneActivityInCache({
id: activityId, id: activityId,
author: workspaceMemberRecord, author: currentWorkspaceMemberRecord,
authorId: workspaceMemberRecord?.id, authorId: currentWorkspaceMemberRecord?.id,
assignee: !assigneeId ? workspaceMemberRecord : undefined, assignee: customAssignee ?? currentWorkspaceMemberRecord,
assigneeId: assigneeId: customAssignee?.id ?? currentWorkspaceMemberRecord?.id,
assigneeId ?? isNonEmptyString(workspaceMemberRecord?.id) type,
? workspaceMemberRecord?.id
: undefined,
type: type,
}); });
const activityTargetsToCreate = const activityTargetsToCreate =
@ -80,18 +69,12 @@ export const useCreateActivityInCache = () => {
activityTargetsToCreate, activityTargetsToCreate,
); );
injectIntoTimelineActivitiesQueryAfterDrawerMount({
activityToInject: createdActivityInCache,
activityTargetsToInject: createdActivityTargetsInCache,
timelineTargetableObject,
});
injectIntoActivityTargetInlineCellCache({ injectIntoActivityTargetInlineCellCache({
activityId, activityId,
activityTargetsToInject: createdActivityTargetsInCache, activityTargetsToInject: createdActivityTargetsInCache,
}); });
attachRelationSourceRecordToItsRelationTargetRecordsAndViceVersaInCache({ attachRelationInBothDirections({
sourceRecord: createdActivityInCache, sourceRecord: createdActivityInCache,
fieldNameOnSourceRecord: 'activityTargets', fieldNameOnSourceRecord: 'activityTargets',
sourceObjectNameSingular: CoreObjectNameSingular.Activity, sourceObjectNameSingular: CoreObjectNameSingular.Activity,

View File

@ -1,16 +1,19 @@
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache'; import { useReadFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useReadFindManyRecordsQueryInCache';
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const useInjectIntoTimelineActivitiesQueryAfterDrawerMount = () => { // TODO: create a generic hook from this
export const useInjectIntoActivitiesQuery = () => {
const { objectMetadataItem: objectMetadataItemActivity } = const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItemOnly({ useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.Activity, objectNameSingular: CoreObjectNameSingular.Activity,
@ -46,79 +49,85 @@ export const useInjectIntoTimelineActivitiesQueryAfterDrawerMount = () => {
objectMetadataItem: objectMetadataItemActivityTarget, objectMetadataItem: objectMetadataItemActivityTarget,
}); });
const injectIntoTimelineActivitiesQueryAfterDrawerMount = ({ const injectActivitiesQueries = ({
activityToInject, activityToInject,
activityTargetsToInject, activityTargetsToInject,
timelineTargetableObject, targetableObjects,
activitiesFilters,
activitiesOrderByVariables,
}: { }: {
activityToInject: Activity; activityToInject: Activity;
activityTargetsToInject: ActivityTarget[]; activityTargetsToInject: ActivityTarget[];
timelineTargetableObject: ActivityTargetableObject; targetableObjects: ActivityTargetableObject[];
activitiesFilters: ObjectRecordQueryFilter;
activitiesOrderByVariables: OrderByField;
}) => { }) => {
const newActivity = { const newActivity = {
...activityToInject, ...activityToInject,
__typename: 'Activity', __typename: 'Activity',
}; };
const targetObjectFieldName = getActivityTargetObjectFieldIdName({ const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({
nameSingular: timelineTargetableObject.targetObjectNameSingular, targetableObjects,
}); });
const activitiyTargetsForTargetableObjectQueryVariables = { const findManyActivitiyTargetsQueryVariables = {
filter: { filter: findManyActivitiyTargetsQueryFilter,
[targetObjectFieldName]: {
eq: timelineTargetableObject.id,
},
},
}; };
const existingActivityTargetsForTargetableObject = const existingActivityTargets = readFindManyActivityTargetsQueryInCache({
readFindManyActivityTargetsQueryInCache({ queryVariables: findManyActivitiyTargetsQueryVariables,
queryVariables: activitiyTargetsForTargetableObjectQueryVariables, });
});
const newActivityTargetsForTargetableObject = [ const newActivityTargets = [
...existingActivityTargetsForTargetableObject, ...existingActivityTargets,
...activityTargetsToInject, ...activityTargetsToInject,
]; ];
const existingActivityIds = existingActivityTargetsForTargetableObject const existingActivityIds = existingActivityTargets
?.map((activityTarget) => activityTarget.activityId) ?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString); .filter(isNonEmptyString);
const timelineActivitiesQueryVariablesBeforeDrawerMount = const currentFindManyActivitiesQueryVariables = {
makeTimelineActivitiesQueryVariables({ filter: {
activityIds: existingActivityIds, id: {
}); in: existingActivityIds.toSorted(sortByAscString),
},
...activitiesFilters,
},
orderBy: activitiesOrderByVariables,
};
const existingActivities = readFindManyActivitiesQueryInCache({ const existingActivities = readFindManyActivitiesQueryInCache({
queryVariables: timelineActivitiesQueryVariablesBeforeDrawerMount, queryVariables: currentFindManyActivitiesQueryVariables,
}); });
const activityIdsAfterDrawerMount = [ const nextActivityIds = [...existingActivityIds, newActivity.id];
...existingActivityIds,
newActivity.id,
];
const timelineActivitiesQueryVariablesAfterDrawerMount = const nextFindManyActivitiesQueryVariables = {
makeTimelineActivitiesQueryVariables({ filter: {
activityIds: activityIdsAfterDrawerMount, id: {
}); in: nextActivityIds.toSorted(sortByAscString),
},
...activitiesFilters,
},
orderBy: activitiesOrderByVariables,
};
overwriteFindManyActivityTargetsQueryInCache({ overwriteFindManyActivityTargetsQueryInCache({
objectRecordsToOverwrite: newActivityTargetsForTargetableObject, objectRecordsToOverwrite: newActivityTargets,
queryVariables: activitiyTargetsForTargetableObjectQueryVariables, queryVariables: findManyActivitiyTargetsQueryVariables,
}); });
const newActivities = [newActivity, ...existingActivities]; const newActivities = [newActivity, ...existingActivities];
overwriteFindManyActivitiesInCache({ overwriteFindManyActivitiesInCache({
objectRecordsToOverwrite: newActivities, objectRecordsToOverwrite: newActivities,
queryVariables: timelineActivitiesQueryVariablesAfterDrawerMount, queryVariables: nextFindManyActivitiesQueryVariables,
}); });
}; };
return { return {
injectIntoTimelineActivitiesQueryAfterDrawerMount, injectActivitiesQueries,
}; };
}; };

View File

@ -1,5 +1,7 @@
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
import { Activity } from '@/activities/types/Activity';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
@ -8,13 +10,26 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { viewableActivityIdState } from '../states/viewableActivityIdState'; import { viewableActivityIdState } from '../states/viewableActivityIdState';
export const useOpenActivityRightDrawer = () => { export const useOpenActivityRightDrawer = () => {
const { openRightDrawer } = useRightDrawer(); const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState); useRightDrawer();
const [viewableActivityId, setViewableActivityId] = useRecoilState(
viewableActivityIdState,
);
const [, setActivityInDrawer] = useRecoilState(activityInDrawerState);
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();
return (activityId: string) => { return (activity: Activity) => {
if (
isRightDrawerOpen &&
rightDrawerPage === RightDrawerPages.EditActivity &&
viewableActivityId === activity.id
) {
return;
}
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(activityId); setViewableActivityId(activity.id);
setActivityInDrawer(activity);
openRightDrawer(RightDrawerPages.EditActivity); openRightDrawer(RightDrawerPages.EditActivity);
}; };
}; };

View File

@ -1,102 +1,69 @@
import { useCallback } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Activity, ActivityType } from '@/activities/types/Activity'; import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { ActivityType } from '@/activities/types/Activity';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState';
import { viewableActivityIdState } from '../states/viewableActivityIdState';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '../utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
export const useOpenCreateActivityDrawer = () => { export const useOpenCreateActivityDrawer = () => {
const { openRightDrawer } = useRightDrawer(); const { openRightDrawer } = useRightDrawer();
const { createManyRecords: createManyActivityTargets } =
useCreateManyRecords<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const { createOneRecord: createOneActivity } = useCreateOneRecord<Activity>({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();
const { createActivityInCache } = useCreateActivityInCache();
const [, setActivityTargetableEntityArray] = useRecoilState( const [, setActivityTargetableEntityArray] = useRecoilState(
activityTargetableEntityArrayState, activityTargetableEntityArrayState,
); );
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState); const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
return useCallback( const setIsCreatingActivity = useSetRecoilState(isActivityInCreateModeState);
async ({
const setTemporaryActivityForEditor = useSetRecoilState(
temporaryActivityForEditorState,
);
const setActivityInDrawer = useSetRecoilState(activityInDrawerState);
const [, setIsUpsertingActivityInDB] = useRecoilState(
isUpsertingActivityInDBState,
);
const openCreateActivityDrawer = async ({
type,
targetableObjects,
customAssignee,
}: {
type: ActivityType;
targetableObjects: ActivityTargetableObject[];
customAssignee?: WorkspaceMember;
}) => {
const { createdActivityInCache } = createActivityInCache({
type, type,
targetableObjects, targetableObjects,
assigneeId, customAssignee,
}: { });
type: ActivityType;
targetableObjects?: ActivityTargetableObject[];
assigneeId?: string;
}) => {
const flattenedTargetableObjects = targetableObjects
? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
targetableObjects,
)
: [];
const createdActivity = await createOneActivity?.({ setActivityInDrawer(createdActivityInCache);
authorId: currentWorkspaceMember?.id, setTemporaryActivityForEditor(createdActivityInCache);
assigneeId: setIsCreatingActivity(true);
assigneeId ?? isNonEmptyString(currentWorkspaceMember?.id) setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
? currentWorkspaceMember?.id setViewableActivityId(createdActivityInCache.id);
: undefined, setActivityTargetableEntityArray(targetableObjects ?? []);
type: type, openRightDrawer(RightDrawerPages.CreateActivity);
}); setIsUpsertingActivityInDB(false);
};
if (!createdActivity) { return openCreateActivityDrawer;
return;
}
const activityTargetsToCreate = flattenedTargetableObjects.map(
(targetableObject) => {
const targetableObjectFieldIdName =
getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
return {
[targetableObjectFieldIdName]: targetableObject.id,
activityId: createdActivity.id,
};
},
);
if (isNonEmptyArray(activityTargetsToCreate)) {
await createManyActivityTargets(activityTargetsToCreate);
}
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(createdActivity.id);
setActivityTargetableEntityArray(targetableObjects ?? []);
openRightDrawer(RightDrawerPages.CreateActivity);
},
[
openRightDrawer,
setActivityTargetableEntityArray,
setHotkeyScope,
setViewableActivityId,
createOneActivity,
createManyActivityTargets,
currentWorkspaceMember,
],
);
}; };

View File

@ -1,5 +1,6 @@
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityType } from '@/activities/types/Activity'; import { ActivityType } from '@/activities/types/Activity';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
@ -8,8 +9,6 @@ import { isDefined } from '~/utils/isDefined';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer';
export const useOpenCreateActivityDrawerForSelectedRowIds = ( export const useOpenCreateActivityDrawerForSelectedRowIds = (
recordTableId: string, recordTableId: string,
) => { ) => {

View File

@ -1,61 +0,0 @@
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState';
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { ActivityType } from '@/activities/types/Activity';
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 { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
export const useOpenCreateActivityDrawerV2 = () => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
const { createActivityInCache } = useCreateActivityInCache();
const [, setActivityTargetableEntityArray] = useRecoilState(
activityTargetableEntityArrayState,
);
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
const setIsCreatingActivity = useSetRecoilState(isCreatingActivityState);
const setTemporaryActivityForEditor = useSetRecoilState(
temporaryActivityForEditorState,
);
const openCreateActivityDrawer = async ({
type,
targetableObjects,
timelineTargetableObject,
assigneeId,
}: {
type: ActivityType;
targetableObjects: ActivityTargetableObject[];
timelineTargetableObject: ActivityTargetableObject;
assigneeId?: string;
}) => {
const { createdActivityInCache } = createActivityInCache({
type,
targetableObjects,
timelineTargetableObject,
assigneeId,
});
setTemporaryActivityForEditor(createdActivityInCache);
setIsCreatingActivity(true);
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(createdActivityInCache.id);
setActivityTargetableEntityArray(targetableObjects ?? []);
openRightDrawer(RightDrawerPages.CreateActivity);
};
return openCreateActivityDrawer;
};

View File

@ -0,0 +1,134 @@
import { isNonEmptyArray, isNonEmptyString } 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 { 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 { sortByAscString } from '~/utils/array/sortByAscString';
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 {
upsertFindManyRecordsQueryInCache:
overwriteFindManyActivityTargetsQueryInCache,
} = useUpsertFindManyRecordsQueryInCache({
objectMetadataItem: objectMetadataItemActivityTarget,
});
const removeFromActivitiesQueries = ({
activityIdToRemove,
activityTargetsToRemove,
targetableObjects,
activitiesFilters,
activitiesOrderByVariables,
}: {
activityIdToRemove: string;
activityTargetsToRemove: ActivityTarget[];
targetableObjects: ActivityTargetableObject[];
activitiesFilters?: ObjectRecordQueryFilter;
activitiesOrderByVariables?: OrderByField;
}) => {
const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({
targetableObjects,
});
const existingActivityTargetsForTargetableObject =
readFindManyActivityTargetsQueryInCache({
queryVariables: findManyActivitiyTargetsQueryFilter,
});
const newActivityTargetsForTargetableObject = isNonEmptyArray(
activityTargetsToRemove,
)
? existingActivityTargetsForTargetableObject.filter(
(existingActivityTarget) =>
activityTargetsToRemove.some(
(activityTargetToRemove) =>
activityTargetToRemove.id !== existingActivityTarget.id,
),
)
: existingActivityTargetsForTargetableObject;
overwriteFindManyActivityTargetsQueryInCache({
objectRecordsToOverwrite: newActivityTargetsForTargetableObject,
queryVariables: findManyActivitiyTargetsQueryFilter,
});
const existingActivityIds = existingActivityTargetsForTargetableObject
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString);
const currentFindManyActivitiesQueryVariables = {
filter: {
id: {
in: existingActivityIds.toSorted(sortByAscString),
},
...activitiesFilters,
},
orderBy: activitiesOrderByVariables,
};
const existingActivities = readFindManyActivitiesQueryInCache({
queryVariables: currentFindManyActivitiesQueryVariables,
});
const activityIdsAfterRemoval = existingActivityIds.filter(
(existingActivityId) => existingActivityId !== activityIdToRemove,
);
const nextFindManyActivitiesQueryVariables = {
filter: {
id: {
in: activityIdsAfterRemoval.toSorted(sortByAscString),
},
...activitiesFilters,
},
orderBy: activitiesOrderByVariables,
};
const newActivities = existingActivities.filter(
(existingActivity) => existingActivity.id !== activityIdToRemove,
);
overwriteFindManyActivitiesInCache({
objectRecordsToOverwrite: newActivities,
queryVariables: nextFindManyActivitiesQueryVariables,
});
};
return {
removeFromActivitiesQueries,
};
};

View File

@ -1,14 +1,21 @@
import { useRecoilState } from 'recoil'; import { useApolloClient } from '@apollo/client';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB'; import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { useInjectIntoTimelineActivitiesQueries } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries';
import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
// TODO: create a generic way to have records only in cache for create mode and delete them afterwards ?
export const useUpsertActivity = () => { export const useUpsertActivity = () => {
const [isCreatingActivity, setIsCreatingActivity] = useRecoilState( const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
isCreatingActivityState, isActivityInCreateModeState,
); );
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({ const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
@ -17,26 +24,67 @@ export const useUpsertActivity = () => {
const { createActivityInDB } = useCreateActivityInDB(); const { createActivityInDB } = useCreateActivityInDB();
const upsertActivity = ({ const [, setIsUpsertingActivityInDB] = useRecoilState(
isUpsertingActivityInDBState,
);
const setActivityInDrawer = useSetRecoilState(activityInDrawerState);
const timelineTargetableObject = useRecoilValue(
timelineTargetableObjectState,
);
const { injectIntoTimelineActivitiesQueries } =
useInjectIntoTimelineActivitiesQueries();
const { makeActivityWithConnection } = useActivityConnectionUtils();
const apolloClient = useApolloClient();
const upsertActivity = async ({
activity, activity,
input, input,
}: { }: {
activity: Activity; activity: Activity;
input: Partial<Activity>; input: Partial<Activity>;
}) => { }) => {
if (isCreatingActivity) { setIsUpsertingActivityInDB(true);
createActivityInDB({
if (isActivityInCreateMode) {
const activityToCreate: Activity = {
...activity, ...activity,
...input, ...input,
};
const { activityWithConnection } =
makeActivityWithConnection(activityToCreate);
// Call optimistic effects
if (timelineTargetableObject) {
injectIntoTimelineActivitiesQueries({
timelineTargetableObject: timelineTargetableObject,
activityToInject: activityWithConnection,
activityTargetsToInject: activityToCreate.activityTargets,
});
}
await createActivityInDB(activityToCreate);
await apolloClient.refetchQueries({
include: ['FindManyActivities', 'FindManyActivityTargets'],
}); });
setIsCreatingActivity(false); setActivityInDrawer(activityToCreate);
setIsActivityInCreateMode(false);
} else { } else {
updateOneActivity?.({ await updateOneActivity?.({
idToUpdate: activity.id, idToUpdate: activity.id,
updateOneRecordInput: input, updateOneRecordInput: input,
}); });
} }
setIsUpsertingActivityInDB(false);
}; };
return { return {

View File

@ -5,10 +5,10 @@ import { v4 } from 'uuid';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache'; import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget'; import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject'; import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly'; import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -27,19 +27,19 @@ const StyledSelectContainer = styled.div`
type ActivityTargetInlineCellEditModeProps = { type ActivityTargetInlineCellEditModeProps = {
activity: Activity; activity: Activity;
activityTargetObjectRecords: ActivityTargetObjectRecord[]; activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
}; };
export const ActivityTargetInlineCellEditMode = ({ export const ActivityTargetInlineCellEditMode = ({
activity, activity,
activityTargetObjectRecords, activityTargetWithTargetRecords,
}: ActivityTargetInlineCellEditModeProps) => { }: ActivityTargetInlineCellEditModeProps) => {
const [isCreatingActivity] = useRecoilState(isCreatingActivityState); const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
const selectedObjectRecordIds = activityTargetObjectRecords.map( const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
(activityTarget) => ({ (activityTarget) => ({
objectNameSingular: activityTarget.targetObjectNameSingular, objectNameSingular: activityTarget.targetObjectNameSingular,
id: activityTarget.targetObjectRecord.id, id: activityTarget.targetObject.id,
}), }),
); );
@ -73,90 +73,89 @@ export const ActivityTargetInlineCellEditMode = ({
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => { const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
closeEditableField(); closeEditableField();
const activityTargetRecordsToDelete = activityTargetObjectRecords.filter(
const activityTargetsToDelete = activityTargetWithTargetRecords.filter(
(activityTargetObjectRecord) => (activityTargetObjectRecord) =>
!selectedRecords.some( !selectedRecords.some(
(selectedRecord) => (selectedRecord) =>
selectedRecord.recordIdentifier.id === selectedRecord.recordIdentifier.id ===
activityTargetObjectRecord.targetObjectRecord.id, activityTargetObjectRecord.targetObject.id,
), ),
); );
const activityTargetRecordsToCreate = selectedRecords.filter( const selectedTargetObjectsToCreate = selectedRecords.filter(
(selectedRecord) => (selectedRecord) =>
!activityTargetObjectRecords.some( !activityTargetWithTargetRecords.some(
(activityTargetObjectRecord) => (activityTargetWithTargetRecord) =>
activityTargetObjectRecord.targetObjectRecord.id === activityTargetWithTargetRecord.targetObject.id ===
selectedRecord.recordIdentifier.id, selectedRecord.recordIdentifier.id,
), ),
); );
if (isCreatingActivity) { const existingActivityTargets = activityTargetWithTargetRecords.map(
let activityTargetsForCreation = activity.activityTargets; (activityTargetObjectRecord) => activityTargetObjectRecord.activityTarget,
);
if (isNonEmptyArray(activityTargetsForCreation)) { let activityTargetsAfterUpdate = Array.from(existingActivityTargets);
const generatedActivityTargets = activityTargetRecordsToCreate.map(
(selectedRecord) => {
const emptyActivityTarget =
generateObjectRecordOptimisticResponse<ActivityTarget>({
id: v4(),
activityId: activity.id,
activity,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
[getActivityTargetObjectFieldIdName({
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
})]: selectedRecord.recordIdentifier.id,
});
return emptyActivityTarget; const activityTargetsToCreate = selectedTargetObjectsToCreate.map(
}, (selectedRecord) => {
); const emptyActivityTarget =
generateObjectRecordOptimisticResponse<ActivityTarget>({
activityTargetsForCreation.push(...generatedActivityTargets);
}
if (isNonEmptyArray(activityTargetRecordsToDelete)) {
activityTargetsForCreation = activityTargetsForCreation.filter(
(activityTarget) =>
!activityTargetRecordsToDelete.some(
(activityTargetObjectRecord) =>
activityTargetObjectRecord.targetObjectRecord.id ===
activityTarget.id,
),
);
}
injectIntoActivityTargetInlineCellCache({
activityId: activity.id,
activityTargetsToInject: activityTargetsForCreation,
});
upsertActivity({
activity,
input: {
activityTargets: activityTargetsForCreation,
},
});
} else {
if (activityTargetRecordsToCreate.length > 0) {
await createManyActivityTargets(
activityTargetRecordsToCreate.map((selectedRecord) => ({
id: v4(), id: v4(),
activityId: activity.id, activityId: activity.id,
activity,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
[getActivityTargetObjectFieldIdName({ [getActivityTargetObjectFieldIdName({
nameSingular: selectedRecord.objectMetadataItem.nameSingular, nameSingular: selectedRecord.objectMetadataItem.nameSingular,
})]: selectedRecord.recordIdentifier.id, })]: selectedRecord.recordIdentifier.id,
})), });
);
return emptyActivityTarget;
},
);
activityTargetsAfterUpdate.push(...activityTargetsToCreate);
if (isNonEmptyArray(activityTargetsToDelete)) {
activityTargetsAfterUpdate = activityTargetsAfterUpdate.filter(
(activityTarget) =>
!activityTargetsToDelete.some(
(activityTargetToDelete) =>
activityTargetToDelete.activityTarget.id === activityTarget.id,
),
);
}
injectIntoActivityTargetInlineCellCache({
activityId: activity.id,
activityTargetsToInject: activityTargetsAfterUpdate,
});
if (isActivityInCreateMode) {
upsertActivity({
activity,
input: {
activityTargets: activityTargetsAfterUpdate,
},
});
} else {
if (activityTargetsToCreate.length > 0) {
await createManyActivityTargets(activityTargetsToCreate, {
skipOptimisticEffect: true,
});
} }
if (activityTargetRecordsToDelete.length > 0) { if (activityTargetsToDelete.length > 0) {
await deleteManyActivityTargets( await deleteManyActivityTargets(
activityTargetRecordsToDelete.map( activityTargetsToDelete.map(
(activityTargetObjectRecord) => (activityTargetObjectRecord) =>
activityTargetObjectRecord.activityTargetRecord.id, activityTargetObjectRecord.activityTarget.id,
), ),
{
skipOptimisticEffect: true,
},
); );
} }
} }
@ -169,7 +168,7 @@ export const ActivityTargetInlineCellEditMode = ({
return ( return (
<StyledSelectContainer> <StyledSelectContainer>
<MultipleObjectRecordSelect <MultipleObjectRecordSelect
selectedObjectRecordIds={selectedObjectRecordIds} selectedObjectRecordIds={selectedTargetObjectIds}
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={handleSubmit} onSubmit={handleSubmit}
/> />

View File

@ -42,7 +42,7 @@ export const ActivityTargetsInlineCell = ({
editModeContent={ editModeContent={
<ActivityTargetInlineCellEditMode <ActivityTargetInlineCellEditMode
activity={activity} activity={activity}
activityTargetObjectRecords={activityTargetObjectRecords} activityTargetWithTargetRecords={activityTargetObjectRecords}
/> />
} }
label="Relations" label="Relations"

View File

@ -94,7 +94,7 @@ export const NoteCard = ({
<FieldContext.Provider value={fieldContext as GenericFieldContextType}> <FieldContext.Provider value={fieldContext as GenericFieldContextType}>
<StyledCard isSingleNote={isSingleNote}> <StyledCard isSingleNote={isSingleNote}>
<StyledCardDetailsContainer <StyledCardDetailsContainer
onClick={() => openActivityRightDrawer(note.id)} onClick={() => openActivityRightDrawer(note)}
> >
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle> <StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
<StyledCardContent>{body}</StyledCardContent> <StyledCardContent>{body}</StyledCardContent>

View File

@ -29,7 +29,7 @@ const StyledTitleBar = styled.h3`
width: 100%; width: 100%;
`; `;
const StyledTitle = styled.h3` const StyledTitle = styled.span`
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.semiBold}; font-weight: ${({ theme }) => theme.font.weight.semiBold};
`; `;

View File

@ -27,10 +27,14 @@ export const Notes = ({
}: { }: {
targetableObject: ActivityTargetableObject; targetableObject: ActivityTargetableObject;
}) => { }) => {
const { notes } = useNotes(targetableObject); const { notes, initialized } = useNotes(targetableObject);
const openCreateActivity = useOpenCreateActivityDrawer(); const openCreateActivity = useOpenCreateActivityDrawer();
if (!initialized) {
return <></>;
}
if (notes?.length === 0) { if (notes?.length === 0) {
return ( return (
<AnimatedPlaceholderEmptyContainer> <AnimatedPlaceholderEmptyContainer>

View File

@ -1,35 +1,21 @@
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject'; import { useActivities } from '@/activities/hooks/useActivities';
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
import { Note } from '@/activities/types/Note'; import { Note } from '@/activities/types/Note';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity';
export const useNotes = (targetableObject: ActivityTargetableObject) => { export const useNotes = (targetableObject: ActivityTargetableObject) => {
const { activityTargets } = useActivityTargetsForTargetableObject({ const { activities, initialized, loading } = useActivities({
targetableObject, activitiesFilters: {
}); type: { eq: 'Note' },
const filter = {
id: {
in: activityTargets?.map((activityTarget) => activityTarget.activityId),
}, },
type: { eq: 'Note' }, activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
}; targetableObjects: [targetableObject],
const orderBy = {
createdAt: 'AscNullsFirst',
} as OrderByField;
const { records: notes } = useFindManyRecords({
skip: !activityTargets?.length,
objectNameSingular: CoreObjectNameSingular.Activity,
filter,
orderBy,
}); });
return { return {
notes: notes as Note[], notes: activities as Note[],
initialized,
loading,
}; };
}; };

View File

@ -1,17 +1,22 @@
import { useApolloClient } from '@apollo/client';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyArray } from '@sniptt/guards';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache'; import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
import { isCreatingActivityState } from '@/activities/states/isCreatingActivityState'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState'; import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState'; import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { useRemoveFromTimelineActivitiesQueries } from '@/activities/timeline/hooks/useRemoveFromTimelineActivitiesQueries';
import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { mapToRecordId } from '@/object-record/utils/mapToObjectId';
import { IconPlus, IconTrash } from '@/ui/display/icon'; import { IconPlus, IconTrash } from '@/ui/display/icon';
import { IconButton } from '@/ui/input/button/components/IconButton'; import { IconButton } from '@/ui/input/button/components/IconButton';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
@ -24,6 +29,8 @@ const StyledButtonContainer = styled.div`
export const ActivityActionBar = () => { export const ActivityActionBar = () => {
const viewableActivityId = useRecoilValue(viewableActivityIdState); const viewableActivityId = useRecoilValue(viewableActivityIdState);
const activityInDrawer = useRecoilValue(activityInDrawerState);
const activityTargetableEntityArray = useRecoilValue( const activityTargetableEntityArray = useRecoilValue(
activityTargetableEntityArrayState, activityTargetableEntityArrayState,
); );
@ -33,27 +40,52 @@ export const ActivityActionBar = () => {
refetchFindManyQuery: true, refetchFindManyQuery: true,
}); });
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
{
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
refetchFindManyQuery: true,
},
);
const [temporaryActivityForEditor, setTemporaryActivityForEditor] = const [temporaryActivityForEditor, setTemporaryActivityForEditor] =
useRecoilState(temporaryActivityForEditorState); useRecoilState(temporaryActivityForEditorState);
const { deleteActivityFromCache } = useDeleteActivityFromCache(); const { deleteActivityFromCache } = useDeleteActivityFromCache();
const [isCreatingActivity] = useRecoilState(isCreatingActivityState); const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
const [isUpsertingActivityInDB] = useRecoilState(
const apolloClient = useApolloClient(); isUpsertingActivityInDBState,
);
const timelineTargetableObject = useRecoilValue(
timelineTargetableObjectState,
);
const openCreateActivity = useOpenCreateActivityDrawer(); const openCreateActivity = useOpenCreateActivityDrawer();
const { removeFromTimelineActivitiesQueries } =
useRemoveFromTimelineActivitiesQueries();
const deleteActivity = () => { const deleteActivity = () => {
if (viewableActivityId) { if (viewableActivityId) {
if (isCreatingActivity && isDefined(temporaryActivityForEditor)) { if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) {
deleteActivityFromCache(temporaryActivityForEditor); deleteActivityFromCache(temporaryActivityForEditor);
setTemporaryActivityForEditor(null); setTemporaryActivityForEditor(null);
} else { } else {
deleteOneActivity?.(viewableActivityId); if (activityInDrawer) {
// TODO: find a better way to do this with custom optimistic rendering for activities const activityTargetIdsToDelete =
apolloClient.refetchQueries({ activityInDrawer?.activityTargets.map(mapToRecordId) ?? [];
include: ['FindManyActivities'],
}); if (isDefined(timelineTargetableObject)) {
removeFromTimelineActivitiesQueries({
activityTargetsToRemove: activityInDrawer?.activityTargets ?? [],
activityIdToRemove: viewableActivityId,
});
}
if (isNonEmptyArray(activityTargetIdsToDelete)) {
deleteManyActivityTargets(activityTargetIdsToDelete);
}
deleteOneActivity?.(viewableActivityId);
}
} }
} }
@ -66,17 +98,19 @@ export const ActivityActionBar = () => {
const addActivity = () => { const addActivity = () => {
setIsRightDrawerOpen(false); setIsRightDrawerOpen(false);
if (record) { if (record && timelineTargetableObject) {
openCreateActivity({ openCreateActivity({
type: record.type, type: record.type,
assigneeId: isNonEmptyString(record.assigneeId) customAssignee: record.assignee,
? record.assigneeId
: undefined,
targetableObjects: activityTargetableEntityArray, targetableObjects: activityTargetableEntityArray,
}); });
} }
}; };
const actionsAreDisabled = isUpsertingActivityInDB;
const isCreateActionDisabled = isActivityInCreateMode;
return ( return (
<StyledButtonContainer> <StyledButtonContainer>
<IconButton <IconButton
@ -84,12 +118,14 @@ export const ActivityActionBar = () => {
onClick={addActivity} onClick={addActivity}
size="medium" size="medium"
variant="secondary" variant="secondary"
disabled={actionsAreDisabled || isCreateActionDisabled}
/> />
<IconButton <IconButton
Icon={IconTrash} Icon={IconTrash}
onClick={deleteActivity} onClick={deleteActivity}
size="medium" size="medium"
variant="secondary" variant="secondary"
disabled={actionsAreDisabled}
/> />
</StyledButtonContainer> </StyledButtonContainer>
); );

View File

@ -24,11 +24,11 @@ export const RightDrawerActivity = ({
showComment = true, showComment = true,
fillTitleFromBody = false, fillTitleFromBody = false,
}: RightDrawerActivityProps) => { }: RightDrawerActivityProps) => {
const { activity } = useActivityById({ const { activity, loading } = useActivityById({
activityId, activityId,
}); });
if (!activity) { if (!activity || loading) {
return <></>; return <></>;
} }

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { Activity } from '@/activities/types/Activity';
export const activityInDrawerState = atom<Activity | null>({
key: 'activityInDrawerState',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const canCreateActivityState = atom<boolean>({
key: 'canCreateActivityState',
default: false,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isActivityInCreateModeState = atom<boolean>({
key: 'isActivityInCreateModeState',
default: false,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isUpsertingActivityInDBState = atom<boolean>({
key: 'isUpsertingActivityInDBState',
default: false,
});

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const isCreatingActivityState = atom<boolean>({
key: 'isCreatingActivityState',
default: false,
});

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
export const targetableObjectsInDrawerState = atom<ActivityTargetableObject[]>({
key: 'targetableObjectsInDrawerState',
default: [],
});

View File

@ -1,28 +1,15 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { PageAddButton } from '@/ui/layout/page/PageAddButton'; import { PageAddButton } from '@/ui/layout/page/PageAddButton';
type PageAddTaskButtonProps = { export const PageAddTaskButton = () => {
filterDropdownId: string;
};
export const PageAddTaskButton = ({
filterDropdownId,
}: PageAddTaskButtonProps) => {
const { selectedFilter } = useFilterDropdown({
filterDropdownId: filterDropdownId,
});
const openCreateActivity = useOpenCreateActivityDrawer(); const openCreateActivity = useOpenCreateActivityDrawer();
// TODO: fetch workspace member from filter here
const handleClick = () => { const handleClick = () => {
openCreateActivity({ openCreateActivity({
type: 'Task', type: 'Task',
assigneeId: isNonEmptyString(selectedFilter?.value) targetableObjects: [],
? selectedFilter?.value
: undefined,
}); });
}; };

View File

@ -40,6 +40,7 @@ export const TaskGroups = ({
upcomingTasks, upcomingTasks,
unscheduledTasks, unscheduledTasks,
completedTasks, completedTasks,
initialized,
} = useTasks({ } = useTasks({
filterDropdownId: filterDropdownId, filterDropdownId: filterDropdownId,
targetableObjects: targetableObjects ?? [], targetableObjects: targetableObjects ?? [],
@ -50,6 +51,10 @@ export const TaskGroups = ({
const { getActiveTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID); const { getActiveTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
const activeTabId = useRecoilValue(getActiveTabIdState()); const activeTabId = useRecoilValue(getActiveTabIdState());
if (!initialized) {
return <></>;
}
if ( if (
(activeTabId !== 'done' && (activeTabId !== 'done' &&
todayOrPreviousTasks?.length === 0 && todayOrPreviousTasks?.length === 0 &&
@ -73,7 +78,7 @@ export const TaskGroups = ({
onClick={() => onClick={() =>
openCreateActivity({ openCreateActivity({
type: 'Task', type: 'Task',
targetableObjects, targetableObjects: targetableObjects ?? [],
}) })
} }
/> />

View File

@ -2,13 +2,12 @@ import { ReactElement } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
import { TaskRow } from './TaskRow'; import { TaskRow } from './TaskRow';
type TaskListProps = { type TaskListProps = {
title?: string; title?: string;
tasks: Omit<Activity, 'assigneeId'>[]; tasks: Activity[];
button?: ReactElement | false; button?: ReactElement | false;
}; };
@ -61,7 +60,7 @@ export const TaskList = ({ title, tasks, button }: TaskListProps) => (
</StyledTitleBar> </StyledTitleBar>
<StyledTaskRows> <StyledTaskRows>
{tasks.map((task) => ( {tasks.map((task) => (
<TaskRow key={task.id} task={task as unknown as GraphQLActivity} /> <TaskRow key={task.id} task={task} />
))} ))}
</StyledTaskRows> </StyledTaskRows>
</StyledContainer> </StyledContainer>

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips'; import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords'; import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { GraphQLActivity } from '@/activities/types/GraphQLActivity'; import { Activity } from '@/activities/types/Activity';
import { getActivitySummary } from '@/activities/utils/getActivitySummary'; import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { IconCalendar, IconComment } from '@/ui/display/icon'; import { IconCalendar, IconComment } from '@/ui/display/icon';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip'; import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
@ -71,11 +71,7 @@ const StyledPlaceholder = styled.div`
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
`; `;
export const TaskRow = ({ export const TaskRow = ({ task }: { task: Activity }) => {
task,
}: {
task: Omit<GraphQLActivity, 'assigneeId'>;
}) => {
const theme = useTheme(); const theme = useTheme();
const openActivityRightDrawer = useOpenActivityRightDrawer(); const openActivityRightDrawer = useOpenActivityRightDrawer();
@ -89,7 +85,7 @@ export const TaskRow = ({
return ( return (
<StyledContainer <StyledContainer
onClick={() => { onClick={() => {
openActivityRightDrawer(task.id); openActivityRightDrawer(task);
}} }}
> >
<div <div

View File

@ -1,13 +1,11 @@
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyArray } from '@sniptt/guards';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useActivities } from '@/activities/hooks/useActivities';
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { LeafObjectRecordFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { parseDate } from '~/utils/date-utils'; import { parseDate } from '~/utils/date-utils';
type UseTasksProps = { type UseTasksProps = {
@ -23,43 +21,6 @@ export const useTasks = ({
filterDropdownId, filterDropdownId,
}); });
const isTargettingObjectRecords = isNonEmptyArray(targetableObjects);
const targetableObjectsFilter =
targetableObjects.reduce<LeafObjectRecordFilter>(
(aggregateFilter, targetableObject) => {
const targetableObjectFieldName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
if (isNonEmptyString(targetableObject.id)) {
aggregateFilter[targetableObjectFieldName] = {
eq: targetableObject.id,
};
}
return aggregateFilter;
},
{},
);
const { records: activityTargets } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
filter: targetableObjectsFilter,
skip: !isTargettingObjectRecords,
});
const skipRequest = !isNonEmptyArray(activityTargets) && !selectedFilter;
const idFilter = isTargettingObjectRecords
? {
id: {
in: activityTargets.map(
(activityTarget) => activityTarget.activityId,
),
},
}
: { id: {} };
const assigneeIdFilter = selectedFilter const assigneeIdFilter = selectedFilter
? { ? {
assigneeId: { assigneeId: {
@ -68,32 +29,34 @@ export const useTasks = ({
} }
: undefined; : undefined;
const { records: completeTasksData } = useFindManyRecords({ const skipActivityTargets = !isNonEmptyArray(targetableObjects);
objectNameSingular: CoreObjectNameSingular.Activity,
skip: skipRequest, const {
filter: { activities: completeTasksData,
initialized: initializedCompleteTasks,
} = useActivities({
targetableObjects,
activitiesFilters: {
completedAt: { is: 'NOT_NULL' }, completedAt: { is: 'NOT_NULL' },
...idFilter,
type: { eq: 'Task' }, type: { eq: 'Task' },
...assigneeIdFilter, ...assigneeIdFilter,
}, },
orderBy: { activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
createdAt: 'DescNullsFirst', skipActivityTargets,
},
}); });
const { records: incompleteTaskData } = useFindManyRecords({ const {
objectNameSingular: CoreObjectNameSingular.Activity, activities: incompleteTaskData,
skip: skipRequest, initialized: initializedIncompleteTasks,
filter: { } = useActivities({
targetableObjects,
activitiesFilters: {
completedAt: { is: 'NULL' }, completedAt: { is: 'NULL' },
...idFilter,
type: { eq: 'Task' }, type: { eq: 'Task' },
...assigneeIdFilter, ...assigneeIdFilter,
}, },
orderBy: { activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
createdAt: 'DescNullsFirst', skipActivityTargets,
},
}); });
const todayOrPreviousTasks = incompleteTaskData?.filter((task) => { const todayOrPreviousTasks = incompleteTaskData?.filter((task) => {
@ -125,5 +88,6 @@ export const useTasks = ({
upcomingTasks: (upcomingTasks ?? []) as Activity[], upcomingTasks: (upcomingTasks ?? []) as Activity[],
unscheduledTasks: (unscheduledTasks ?? []) as Activity[], unscheduledTasks: (unscheduledTasks ?? []) as Activity[],
completedTasks: (completedTasks ?? []) as Activity[], completedTasks: (completedTasks ?? []) as Activity[],
initialized: initializedCompleteTasks && initializedIncompleteTasks,
}; };
}; };

View File

@ -1,7 +1,11 @@
import { useEffect } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { useActivities } from '@/activities/hooks/useActivities';
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup'; import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
import { useTimelineActivities } from '@/activities/timeline/hooks/useTimelineActivities'; import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import { import {
@ -11,6 +15,7 @@ import {
AnimatedPlaceholderEmptyTitle, AnimatedPlaceholderEmptyTitle,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { isDefined } from '~/utils/isDefined';
import { TimelineItemsContainer } from './TimelineItemsContainer'; import { TimelineItemsContainer } from './TimelineItemsContainer';
@ -31,11 +36,22 @@ export const Timeline = ({
}: { }: {
targetableObject: ActivityTargetableObject; targetableObject: ActivityTargetableObject;
}) => { }) => {
const { activities, initialized } = useTimelineActivities({ const { activities, initialized, noActivities } = useActivities({
targetableObject, targetableObjects: [targetableObject],
activitiesFilters: {},
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
skip: !isDefined(targetableObject),
}); });
const showEmptyState = initialized && activities.length === 0; const setTimelineTargetableObject = useSetRecoilState(
timelineTargetableObjectState,
);
useEffect(() => {
setTimelineTargetableObject(targetableObject);
}, [targetableObject, setTimelineTargetableObject]);
const showEmptyState = noActivities;
const showLoadingState = !initialized; const showLoadingState = !initialized;

View File

@ -1,13 +1,14 @@
import { Tooltip } from 'react-tooltip'; import { Tooltip } from 'react-tooltip';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { IconCheckbox, IconNotes } from '@/ui/display/icon'; import { IconCheckbox, IconNotes } from '@/ui/display/icon';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { import {
beautifyExactDateTime, beautifyExactDateTime,
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
@ -135,19 +136,7 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
`; `;
type TimelineActivityProps = { type TimelineActivityProps = {
activity: Pick< activity: Activity;
Activity,
| 'id'
| 'title'
| 'body'
| 'createdAt'
| 'completedAt'
| 'type'
| 'comments'
| 'dueAt'
> & { author?: Pick<WorkspaceMember, 'name' | 'avatarUrl'> } & {
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
};
isLastActivity?: boolean; isLastActivity?: boolean;
}; };
@ -160,6 +149,8 @@ export const TimelineActivity = ({
const openActivityRightDrawer = useOpenActivityRightDrawer(); const openActivityRightDrawer = useOpenActivityRightDrawer();
const theme = useTheme(); const theme = useTheme();
const activityFromStore = useRecoilValue(recordStoreFamilyState(activity.id));
return ( return (
<> <>
<StyledTimelineItemContainer> <StyledTimelineItemContainer>
@ -191,11 +182,13 @@ export const TimelineActivity = ({
</StyledIconContainer> </StyledIconContainer>
{(activity.type === 'Note' || activity.type === 'Task') && ( {(activity.type === 'Note' || activity.type === 'Task') && (
<StyledActivityTitle <StyledActivityTitle
onClick={() => openActivityRightDrawer(activity.id)} onClick={() => openActivityRightDrawer(activity)}
> >
<StyledActivityLink title={activity.title ?? '(No Title)'}> <StyledActivityLink
{activity.title ?? '(No Title)'} title={activityFromStore?.title ?? '(No Title)'}
>
{activityFromStore?.title ?? '(No Title)'}
</StyledActivityLink> </StyledActivityLink>
</StyledActivityTitle> </StyledActivityTitle>

View File

@ -1,7 +1,7 @@
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { Button, ButtonGroup } from 'tsup.ui.index'; import { Button, ButtonGroup } from 'tsup.ui.index';
import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { import {
IconCheckbox, IconCheckbox,
@ -19,7 +19,7 @@ export const TimelineCreateButtonGroup = ({
const { getActiveTabIdState } = useTabList(TAB_LIST_COMPONENT_ID); const { getActiveTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
const setActiveTabId = useSetRecoilState(getActiveTabIdState()); const setActiveTabId = useSetRecoilState(getActiveTabIdState());
const openCreateActivity = useOpenCreateActivityDrawerV2(); const openCreateActivity = useOpenCreateActivityDrawer();
return ( return (
<ButtonGroup variant={'secondary'}> <ButtonGroup variant={'secondary'}>
@ -30,7 +30,6 @@ export const TimelineCreateButtonGroup = ({
openCreateActivity({ openCreateActivity({
type: 'Note', type: 'Note',
targetableObjects: [targetableObject], targetableObjects: [targetableObject],
timelineTargetableObject: targetableObject,
}) })
} }
/> />
@ -41,7 +40,6 @@ export const TimelineCreateButtonGroup = ({
openCreateActivity({ openCreateActivity({
type: 'Task', type: 'Task',
targetableObjects: [targetableObject], targetableObjects: [targetableObject],
timelineTargetableObject: targetableObject,
}) })
} }
/> />

View File

@ -0,0 +1,5 @@
import { OrderByField } from '@/object-metadata/types/OrderByField';
export const FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY: OrderByField = {
createdAt: 'DescNullsFirst',
};

View File

@ -0,0 +1,32 @@
import { useInjectIntoActivitiesQuery } from '@/activities/hooks/useInjectIntoActivitiesQuery';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
export const useInjectIntoTimelineActivitiesQueries = () => {
const { injectActivitiesQueries } = useInjectIntoActivitiesQuery();
const injectIntoTimelineActivitiesQueries = ({
activityToInject,
activityTargetsToInject,
timelineTargetableObject,
}: {
activityToInject: Activity;
activityTargetsToInject: ActivityTarget[];
timelineTargetableObject: ActivityTargetableObject;
}) => {
injectActivitiesQueries({
activitiesFilters: {},
activitiesOrderByVariables: {
createdAt: 'DescNullsFirst',
},
activityTargetsToInject,
activityToInject,
targetableObjects: [timelineTargetableObject],
});
};
return {
injectIntoTimelineActivitiesQueries,
};
};

View File

@ -0,0 +1,138 @@
import { useRecoilValue } from 'recoil';
import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries';
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
export const useRemoveFromTimelineActivitiesQueries = () => {
const timelineTargetableObject = useRecoilValue(
timelineTargetableObjectState,
);
// 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 {
// upsertFindManyRecordsQueryInCache:
// overwriteFindManyActivityTargetsQueryInCache,
// } = useUpsertFindManyRecordsQueryInCache({
// objectMetadataItem: objectMetadataItemActivityTarget,
// });
const { removeFromActivitiesQueries } = useRemoveFromActivitiesQueries();
const removeFromTimelineActivitiesQueries = ({
activityIdToRemove,
activityTargetsToRemove,
}: {
activityIdToRemove: string;
activityTargetsToRemove: ActivityTarget[];
}) => {
if (!timelineTargetableObject) {
throw new Error('Timeline targetable object is not defined');
}
removeFromActivitiesQueries({
activityIdToRemove,
activityTargetsToRemove,
targetableObjects: [timelineTargetableObject],
activitiesFilters: {},
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
});
// const targetObjectFieldName = getActivityTargetObjectFieldIdName({
// nameSingular: timelineTargetableObject.targetObjectNameSingular,
// });
// const activitiyTargetsForTargetableObjectQueryVariables = {
// filter: {
// [targetObjectFieldName]: {
// eq: timelineTargetableObject.id,
// },
// },
// };
// const existingActivityTargetsForTargetableObject =
// readFindManyActivityTargetsQueryInCache({
// queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
// });
// const newActivityTargetsForTargetableObject = isNonEmptyArray(
// activityTargetsToRemove,
// )
// ? existingActivityTargetsForTargetableObject.filter(
// (existingActivityTarget) =>
// activityTargetsToRemove.some(
// (activityTargetToRemove) =>
// activityTargetToRemove.id !== existingActivityTarget.id,
// ),
// )
// : existingActivityTargetsForTargetableObject;
// overwriteFindManyActivityTargetsQueryInCache({
// objectRecordsToOverwrite: newActivityTargetsForTargetableObject,
// queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
// });
// const existingActivityIds = existingActivityTargetsForTargetableObject
// ?.map((activityTarget) => activityTarget.activityId)
// .filter(isNonEmptyString);
// const timelineActivitiesQueryVariablesBeforeDrawerMount =
// makeTimelineActivitiesQueryVariables({
// activityIds: existingActivityIds,
// });
// const existingActivities = readFindManyActivitiesQueryInCache({
// queryVariables: timelineActivitiesQueryVariablesBeforeDrawerMount,
// });
// const activityIdsAfterRemoval = existingActivityIds.filter(
// (existingActivityId) => existingActivityId !== activityIdToRemove,
// );
// const timelineActivitiesQueryVariablesAfterRemoval =
// makeTimelineActivitiesQueryVariables({
// activityIds: activityIdsAfterRemoval,
// });
// const newActivities = existingActivities
// .filter((existingActivity) => existingActivity.id !== activityIdToRemove)
// .toSorted(sortObjectRecordByDateField('createdAt', 'DescNullsFirst'));
// overwriteFindManyActivitiesInCache({
// objectRecordsToOverwrite: newActivities,
// queryVariables: timelineActivitiesQueryVariablesAfterRemoval,
// });
};
return {
removeFromTimelineActivitiesQueries,
};
};

View File

@ -1,18 +1,37 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject'; import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables'; import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { sortByAscString } from '~/utils/array/sortByAscString';
import { isDefined } from '~/utils/isDefined';
export const useTimelineActivities = ({ export const useTimelineActivities = ({
targetableObject, targetableObject,
}: { }: {
targetableObject: ActivityTargetableObject; targetableObject: ActivityTargetableObject;
}) => { }) => {
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
const [, setTimelineTargetableObject] = useRecoilState(
timelineTargetableObjectState,
);
useEffect(() => {
if (isDefined(targetableObject)) {
setTimelineTargetableObject(targetableObject);
}
}, [targetableObject, setTimelineTargetableObject]);
const { const {
activityTargets, activityTargets,
loadingActivityTargets, loadingActivityTargets,
@ -23,9 +42,14 @@ export const useTimelineActivities = ({
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const activityIds = activityTargets const activityIds = Array.from(
?.map((activityTarget) => activityTarget.activityId) new Set(
.filter(isNonEmptyString); activityTargets
?.map((activityTarget) => activityTarget.activityId)
.filter(isNonEmptyString)
.toSorted(sortByAscString),
),
);
const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables( const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables(
{ {
@ -33,17 +57,30 @@ export const useTimelineActivities = ({
}, },
); );
const { records: activities, loading: loadingActivities } = const { records: activitiesWithConnection, loading: loadingActivities } =
useFindManyRecords<Activity>({ useFindManyRecords<Activity>({
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets), skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
objectNameSingular: CoreObjectNameSingular.Activity, objectNameSingular: CoreObjectNameSingular.Activity,
filter: timelineActivitiesQueryVariables.filter, filter: timelineActivitiesQueryVariables.filter,
orderBy: timelineActivitiesQueryVariables.orderBy, orderBy: timelineActivitiesQueryVariables.orderBy,
onCompleted: () => { onCompleted: useRecoilCallback(
if (!initialized) { ({ set }) =>
setInitialized(true); (data) => {
} if (!initialized) {
}, setInitialized(true);
}
const activities = getRecordsFromRecordConnection({
recordConnection: data,
});
for (const activity of activities) {
set(recordStoreFamilyState(activity.id), activity);
}
},
[initialized],
),
depth: 3,
}); });
const noActivityTargets = const noActivityTargets =
@ -57,6 +94,11 @@ export const useTimelineActivities = ({
const loading = loadingActivities || loadingActivityTargets; const loading = loadingActivities || loadingActivityTargets;
const activities = activitiesWithConnection
?.map(makeActivityWithoutConnection as any)
.map(({ activity }: any) => activity as any)
.filter(isDefined);
return { return {
activities, activities,
loading, loading,

View File

@ -0,0 +1,9 @@
import { atom } from 'recoil';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
export const timelineTargetableObjectState =
atom<ActivityTargetableObject | null>({
key: 'timelineTargetableObjectState',
default: null,
});

View File

@ -1,4 +1,5 @@
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const makeTimelineActivitiesQueryVariables = ({ export const makeTimelineActivitiesQueryVariables = ({
activityIds, activityIds,
@ -8,7 +9,7 @@ export const makeTimelineActivitiesQueryVariables = ({
return { return {
filter: { filter: {
id: { id: {
in: activityIds, in: activityIds.toSorted(sortByAscString),
}, },
}, },
orderBy: { orderBy: {

View File

@ -1,9 +1,10 @@
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export type ActivityTargetObjectRecord = { export type ActivityTargetWithTargetRecord = {
targetObjectMetadataItem: ObjectMetadataItem; targetObjectMetadataItem: ObjectMetadataItem;
activityTargetRecord: ObjectRecord; activityTarget: ActivityTarget;
targetObjectRecord: ObjectRecord; targetObject: ObjectRecord;
targetObjectNameSingular: string; targetObjectNameSingular: string;
}; };

View File

@ -0,0 +1,25 @@
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
export const getActivityTargetsFilter = ({
targetableObjects,
}: {
targetableObjects: ActivityTargetableObject[];
}) => {
const findManyActivitiyTargetsQueryFilter = Object.fromEntries(
targetableObjects.map((targetableObject) => {
const targetObjectFieldName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
return [
targetObjectFieldName,
{
eq: targetableObject.id,
},
];
}),
);
return findManyActivitiyTargetsQueryFilter;
};

View File

@ -13,9 +13,14 @@ import { isDefined } from '~/utils/isDefined';
export const useActivityConnectionUtils = () => { export const useActivityConnectionUtils = () => {
const mapConnectionToRecords = useMapConnectionToRecords(); const mapConnectionToRecords = useMapConnectionToRecords();
const makeActivityWithoutConnection = (activityWithConnections: any) => { const makeActivityWithoutConnection = (
activityWithConnections: Activity & {
activityTargets: ObjectRecordConnection<ActivityTarget>;
comments: ObjectRecordConnection<Comment>;
},
) => {
if (!isDefined(activityWithConnections)) { if (!isDefined(activityWithConnections)) {
return { activity: null }; throw new Error('Activity with connections is not defined');
} }
const hasActivityTargetsConnection = isObjectRecordConnection( const hasActivityTargetsConnection = isObjectRecordConnection(
@ -77,11 +82,13 @@ export const useActivityConnectionUtils = () => {
: []; : [];
const activityTargets = { const activityTargets = {
__typename: 'ActivityTargetConnection',
edges: activityTargetEdges, edges: activityTargetEdges,
pageInfo: getEmptyPageInfo(), pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection<ActivityTarget>; } as ObjectRecordConnection<ActivityTarget>;
const comments = { const comments = {
__typename: 'CommentConnection',
edges: commentEdges, edges: commentEdges,
pageInfo: getEmptyPageInfo(), pageInfo: getEmptyPageInfo(),
} as ObjectRecordConnection<Comment>; } as ObjectRecordConnection<Comment>;
@ -90,6 +97,9 @@ export const useActivityConnectionUtils = () => {
...activity, ...activity,
activityTargets, activityTargets,
comments, comments,
} as Activity & {
activityTargets: ObjectRecordConnection<ActivityTarget>;
comments: ObjectRecordConnection<Comment>;
}; };
return { activityWithConnection }; return { activityWithConnection };

View File

@ -11,17 +11,19 @@ export const isObjectRecordConnection = (
objectNameSingular, objectNameSingular,
)}Connection`; )}Connection`;
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`; const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
const objectConnectionSchema = z.object({ const objectConnectionSchema = z.object({
__typename: z.literal(objectConnectionTypeName), __typename: z.literal(objectConnectionTypeName).optional(),
edges: z.array( edges: z.array(
z.object({ z.object({
__typename: z.literal(objectEdgeTypeName), __typename: z.literal(objectEdgeTypeName).optional(),
node: z.object({ node: z.object({
id: z.string().uuid(), id: z.string().uuid(),
}), }),
}), }),
), ),
}); });
const connectionValidation = objectConnectionSchema.safeParse(value); const connectionValidation = objectConnectionSchema.safeParse(value);
return connectionValidation.success; return connectionValidation.success;

View File

@ -32,29 +32,25 @@ export const triggerDetachRelationOptimisticEffect = ({
targetRecordFieldValue, targetRecordFieldValue,
{ isReference, readField }, { isReference, readField },
) => { ) => {
const isRelationTargetFieldAnObjectRecordConnection = const isRecordConnection = isCachedObjectRecordConnection(
isCachedObjectRecordConnection( sourceObjectNameSingular,
sourceObjectNameSingular,
targetRecordFieldValue,
);
if (isRelationTargetFieldAnObjectRecordConnection) {
const relationTargetFieldEdgesWithoutRelationSourceRecordToDetach =
targetRecordFieldValue.edges.filter(
({ node }) => readField('id', node) !== sourceRecordId,
);
return {
...targetRecordFieldValue,
edges: relationTargetFieldEdgesWithoutRelationSourceRecordToDetach,
};
}
const isRelationTargetFieldASingleObjectRecord = isReference(
targetRecordFieldValue, targetRecordFieldValue,
); );
if (isRelationTargetFieldASingleObjectRecord) { if (isRecordConnection) {
const nextEdges = targetRecordFieldValue.edges.filter(
({ node }) => readField('id', node) !== sourceRecordId,
);
return {
...targetRecordFieldValue,
edges: nextEdges,
};
}
const isSingleReference = isReference(targetRecordFieldValue);
if (isSingleReference) {
return null; return null;
} }

View File

@ -45,41 +45,35 @@ export const triggerUpdateRecordOptimisticEffect = ({
rootQueryCachedResponse, rootQueryCachedResponse,
{ DELETE, readField, storeFieldName, toReference }, { DELETE, readField, storeFieldName, toReference },
) => { ) => {
const rootQueryCachedResponseIsNotACachedObjectRecordConnection = const shouldSkip = !isCachedObjectRecordConnection(
!isCachedObjectRecordConnection( objectMetadataItem.nameSingular,
objectMetadataItem.nameSingular, rootQueryCachedResponse,
rootQueryCachedResponse, );
);
if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) { if (shouldSkip) {
return rootQueryCachedResponse; return rootQueryCachedResponse;
} }
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse; const rootQueryConnection = rootQueryCachedResponse;
const { fieldArguments: rootQueryVariables } = const { fieldArguments: rootQueryVariables } =
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>( parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
storeFieldName, storeFieldName,
); );
const rootQueryCurrentCachedRecordEdges = const rootQueryCurrentEdges =
readField<CachedObjectRecordEdge[]>( readField<CachedObjectRecordEdge[]>('edges', rootQueryConnection) ??
'edges', [];
rootQueryCachedObjectRecordConnection,
) ?? [];
let rootQueryNextCachedRecordEdges = [ let rootQueryNextEdges = [...rootQueryCurrentEdges];
...rootQueryCurrentCachedRecordEdges,
];
const rootQueryFilter = rootQueryVariables?.filter; const rootQueryFilter = rootQueryVariables?.filter;
const rootQueryOrderBy = rootQueryVariables?.orderBy; const rootQueryOrderBy = rootQueryVariables?.orderBy;
const rootQueryLimit = rootQueryVariables?.first; const rootQueryLimit = rootQueryVariables?.first;
const shouldTestThatUpdatedRecordMatchesThisRootQueryFilter = const shouldTryToMatchFilter = isDefined(rootQueryFilter);
isDefined(rootQueryFilter);
if (shouldTestThatUpdatedRecordMatchesThisRootQueryFilter) { if (shouldTryToMatchFilter) {
const updatedRecordMatchesThisRootQueryFilter = const updatedRecordMatchesThisRootQueryFilter =
isRecordMatchingFilter({ isRecordMatchingFilter({
record: updatedRecord, record: updatedRecord,
@ -88,24 +82,27 @@ export const triggerUpdateRecordOptimisticEffect = ({
}); });
const updatedRecordIndexInRootQueryEdges = const updatedRecordIndexInRootQueryEdges =
rootQueryCurrentCachedRecordEdges.findIndex( rootQueryCurrentEdges.findIndex(
(cachedEdge) => (cachedEdge) =>
readField('id', cachedEdge.node) === updatedRecord.id, readField('id', cachedEdge.node) === updatedRecord.id,
); );
const updatedRecordFoundInRootQueryEdges =
updatedRecordIndexInRootQueryEdges > -1;
const updatedRecordShouldBeAddedToRootQueryEdges = const updatedRecordShouldBeAddedToRootQueryEdges =
updatedRecordMatchesThisRootQueryFilter && updatedRecordMatchesThisRootQueryFilter &&
updatedRecordIndexInRootQueryEdges === -1; !updatedRecordFoundInRootQueryEdges;
const updatedRecordShouldBeRemovedFromRootQueryEdges = const updatedRecordShouldBeRemovedFromRootQueryEdges =
updatedRecordMatchesThisRootQueryFilter && !updatedRecordMatchesThisRootQueryFilter &&
updatedRecordIndexInRootQueryEdges === -1; updatedRecordFoundInRootQueryEdges;
if (updatedRecordShouldBeAddedToRootQueryEdges) { if (updatedRecordShouldBeAddedToRootQueryEdges) {
const updatedRecordNodeReference = toReference(updatedRecord); const updatedRecordNodeReference = toReference(updatedRecord);
if (isDefined(updatedRecordNodeReference)) { if (isDefined(updatedRecordNodeReference)) {
rootQueryNextCachedRecordEdges.push({ rootQueryNextEdges.push({
__typename: objectEdgeTypeName, __typename: objectEdgeTypeName,
node: updatedRecordNodeReference, node: updatedRecordNodeReference,
cursor: '', cursor: '',
@ -114,18 +111,15 @@ export const triggerUpdateRecordOptimisticEffect = ({
} }
if (updatedRecordShouldBeRemovedFromRootQueryEdges) { if (updatedRecordShouldBeRemovedFromRootQueryEdges) {
rootQueryNextCachedRecordEdges.splice( rootQueryNextEdges.splice(updatedRecordIndexInRootQueryEdges, 1);
updatedRecordIndexInRootQueryEdges,
1,
);
} }
} }
const nextRootQueryEdgesShouldBeSorted = isDefined(rootQueryOrderBy); const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
if (nextRootQueryEdgesShouldBeSorted) { if (rootQueryNextEdgesShouldBeSorted) {
rootQueryNextCachedRecordEdges = sortCachedObjectEdges({ rootQueryNextEdges = sortCachedObjectEdges({
edges: rootQueryNextCachedRecordEdges, edges: rootQueryNextEdges,
orderBy: rootQueryOrderBy, orderBy: rootQueryOrderBy,
readCacheField: readField, readCacheField: readField,
}); });
@ -158,12 +152,12 @@ export const triggerUpdateRecordOptimisticEffect = ({
// the query's result. // the query's result.
// In this case, invalidate the cache entry so it can be re-fetched. // In this case, invalidate the cache entry so it can be re-fetched.
const rootQueryCurrentCachedRecordEdgesLengthIsAtLimit = const rootQueryCurrentCachedRecordEdgesLengthIsAtLimit =
rootQueryCurrentCachedRecordEdges.length === rootQueryLimit; rootQueryCurrentEdges.length === rootQueryLimit;
// If next edges length is under limit, then we can wait for the network response and merge the result // If next edges length is under limit, then we can wait for the network response and merge the result
// then in the merge function we could implement this mechanism to limit the number of edges in the cache // then in the merge function we could implement this mechanism to limit the number of edges in the cache
const rootQueryNextCachedRecordEdgesLengthIsUnderLimit = const rootQueryNextCachedRecordEdgesLengthIsUnderLimit =
rootQueryNextCachedRecordEdges.length < rootQueryLimit; rootQueryNextEdges.length < rootQueryLimit;
const shouldDeleteRootQuerySoItCanBeRefetched = const shouldDeleteRootQuerySoItCanBeRefetched =
rootQueryCurrentCachedRecordEdgesLengthIsAtLimit && rootQueryCurrentCachedRecordEdgesLengthIsAtLimit &&
@ -174,16 +168,16 @@ export const triggerUpdateRecordOptimisticEffect = ({
} }
const rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit = const rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit =
rootQueryNextCachedRecordEdges.length > rootQueryLimit; rootQueryNextEdges.length > rootQueryLimit;
if (rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit) { if (rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit) {
rootQueryNextCachedRecordEdges.splice(rootQueryLimit); rootQueryNextEdges.splice(rootQueryLimit);
} }
} }
return { return {
...rootQueryCachedObjectRecordConnection, ...rootQueryConnection,
edges: rootQueryNextCachedRecordEdges, edges: rootQueryNextEdges,
}; };
}, },
}, },

View File

@ -6,7 +6,7 @@ import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effec
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect'; import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord'; import { CachedObjectRecord } from '@/apollo/types/CachedObjectRecord';
import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH as CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach'; import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
@ -74,6 +74,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
return; return;
} }
// TODO: replace this by a relation type check, if it's one to many,
// it's an object record connection (we can still check it though as a safeguard)
const currentFieldValueOnSourceRecordIsARecordConnection = const currentFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection( isObjectRecordConnection(
targetObjectMetadataItem.nameSingular, targetObjectMetadataItem.nameSingular,
@ -104,12 +106,14 @@ export const triggerUpdateRelationsOptimisticEffect = ({
isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0; isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0;
if (shouldDetachSourceFromAllTargets) { if (shouldDetachSourceFromAllTargets) {
const shouldStartByDeletingRelationTargetRecordsFromCache = // TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH.includes( // Instead of hardcoding it here
const shouldCascadeDeleteTargetRecords =
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
targetObjectMetadataItem.nameSingular as CoreObjectNameSingular, targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
); );
if (shouldStartByDeletingRelationTargetRecordsFromCache) { if (shouldCascadeDeleteTargetRecords) {
triggerDeleteRecordsOptimisticEffect({ triggerDeleteRecordsOptimisticEffect({
cache, cache,
objectMetadataItem: targetObjectMetadataItem, objectMetadataItem: targetObjectMetadataItem,

View File

@ -2,4 +2,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
export const CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH = [ export const CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH = [
CoreObjectNameSingular.Favorite, CoreObjectNameSingular.Favorite,
CoreObjectNameSingular.ActivityTarget,
CoreObjectNameSingular.Comment,
]; ];

View File

@ -192,11 +192,11 @@ export const CommandMenu = () => {
const activityCommands = useMemo( const activityCommands = useMemo(
() => () =>
activities.map(({ id, title }) => ({ activities.map((activity) => ({
id, id: activity.id,
label: title ?? '', label: activity.title ?? '',
to: '', to: '',
onCommandClick: () => openActivityRightDrawer(id), onCommandClick: () => openActivityRightDrawer(activity),
})), })),
[activities, openActivityRightDrawer], [activities, openActivityRightDrawer],
); );
@ -372,7 +372,7 @@ export const CommandMenu = () => {
Icon={IconNotes} Icon={IconNotes}
key={activity.id} key={activity.id}
label={activity.title ?? ''} label={activity.title ?? ''}
onClick={() => openActivityRightDrawer(activity.id)} onClick={() => openActivityRightDrawer(activity)}
/> />
</SelectableItem> </SelectableItem>
))} ))}

View File

@ -43,12 +43,19 @@ export const useGenerateObjectRecordOptimisticResponse = ({
); );
const relationRecordId = result[relationIdFieldName] as string | null; const relationRecordId = result[relationIdFieldName] as string | null;
const relationRecord = input[fieldMetadataItem.name] as
| ObjectRecord
| undefined;
return { return {
...result, ...result,
[fieldMetadataItem.name]: relationRecordId [fieldMetadataItem.name]: relationRecordId
? { ? {
__typename: relationRecordTypeName, __typename: relationRecordTypeName,
id: relationRecordId, 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, : null,
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,12 +6,15 @@ import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages'; import { RightDrawerPages } from '../types/RightDrawerPages';
export const useRightDrawer = () => { export const useRightDrawer = () => {
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
isRightDrawerOpenState,
);
const [, setIsRightDrawerExpanded] = useRecoilState( const [, setIsRightDrawerExpanded] = useRecoilState(
isRightDrawerExpandedState, isRightDrawerExpandedState,
); );
const [, setRightDrawerPage] = useRecoilState(rightDrawerPageState); const [rightDrawerPage, setRightDrawerPage] =
useRecoilState(rightDrawerPageState);
const openRightDrawer = (rightDrawerPage: RightDrawerPages) => { const openRightDrawer = (rightDrawerPage: RightDrawerPages) => {
setRightDrawerPage(rightDrawerPage); setRightDrawerPage(rightDrawerPage);
@ -25,6 +28,8 @@ export const useRightDrawer = () => {
}; };
return { return {
rightDrawerPage,
isRightDrawerOpen,
openRightDrawer, openRightDrawer,
closeRightDrawer, closeRightDrawer,
}; };

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityType } from '@/activities/types/Activity'; import { ActivityType } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
@ -24,14 +24,14 @@ export const ShowPageAddButton = ({
activityTargetObject: ActivityTargetableObject; activityTargetObject: ActivityTargetableObject;
}) => { }) => {
const { closeDropdown, toggleDropdown } = useDropdown('add-show-page'); const { closeDropdown, toggleDropdown } = useDropdown('add-show-page');
const openCreateActivity = useOpenCreateActivityDrawerV2(); const openCreateActivity = useOpenCreateActivityDrawer();
const handleSelect = (type: ActivityType) => { const handleSelect = (type: ActivityType) => {
openCreateActivity({ openCreateActivity({
type, type,
targetableObjects: [activityTargetObject], targetableObjects: [activityTargetObject],
timelineTargetableObject: activityTargetObject,
}); });
closeDropdown(); closeDropdown();
}; };

View File

@ -52,7 +52,7 @@ export const Tasks = () => {
<RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}> <RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}>
<TasksEffect filterDropdownId={filterDropdownId} /> <TasksEffect filterDropdownId={filterDropdownId} />
<PageHeader title="Tasks" Icon={IconCheckbox}> <PageHeader title="Tasks" Icon={IconCheckbox}>
<PageAddTaskButton filterDropdownId={filterDropdownId} /> <PageAddTaskButton />
</PageHeader> </PageHeader>
<PageBody> <PageBody>
<StyledTasksContainer> <StyledTasksContainer>

View File

@ -0,0 +1,3 @@
export const sortByAscString = (a: string, b: string) => {
return a.localeCompare(b);
};