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:
@ -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]);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -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);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
editModeContent={
|
editModeContent={
|
||||||
<ActivityTargetInlineCellEditMode
|
<ActivityTargetInlineCellEditMode
|
||||||
activity={activity}
|
activity={activity}
|
||||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Relations"
|
label="Relations"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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};
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
import { Activity } from '@/activities/types/Activity';
|
||||||
|
|
||||||
|
export const activityInDrawerState = atom<Activity | null>({
|
||||||
|
key: 'activityInDrawerState',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const canCreateActivityState = atom<boolean>({
|
||||||
|
key: 'canCreateActivityState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const isActivityInCreateModeState = atom<boolean>({
|
||||||
|
key: 'isActivityInCreateModeState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const isUpsertingActivityInDBState = atom<boolean>({
|
||||||
|
key: 'isUpsertingActivityInDBState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { atom } from 'recoil';
|
|
||||||
|
|
||||||
export const isCreatingActivityState = atom<boolean>({
|
|
||||||
key: 'isCreatingActivityState',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
|
||||||
|
export const targetableObjectsInDrawerState = atom<ActivityTargetableObject[]>({
|
||||||
|
key: 'targetableObjectsInDrawerState',
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
@ -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,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 ?? [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||||
|
|
||||||
|
export const FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY: OrderByField = {
|
||||||
|
createdAt: 'DescNullsFirst',
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
|
||||||
|
export const timelineTargetableObjectState =
|
||||||
|
atom<ActivityTargetableObject | null>({
|
||||||
|
key: 'timelineTargetableObjectState',
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
@ -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: {
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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 };
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -20,5 +20,6 @@ export const getRecordConnectionFromRecords = <T extends ObjectRecord>({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
pageInfo: getEmptyPageInfo(),
|
pageInfo: getEmptyPageInfo(),
|
||||||
|
totalCount: records.length,
|
||||||
} as ObjectRecordConnection<T>;
|
} as ObjectRecordConnection<T>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 = '',
|
||||||
|
|||||||
@ -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[];
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
|
export const sortByObjectRecordId = (a: ObjectRecord, b: ObjectRecord) => {
|
||||||
|
return a.id.localeCompare(b.id);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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}`);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
3
packages/twenty-front/src/utils/array/sortByAscString.ts
Normal file
3
packages/twenty-front/src/utils/array/sortByAscString.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const sortByAscString = (a: string, b: string) => {
|
||||||
|
return a.localeCompare(b);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user