mirror of
https://github.com/lingble/twenty.git
synced 2025-11-02 13:47:55 +00:00
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',
|
||||
type: CommandType.Create,
|
||||
Icon: IconCheckbox,
|
||||
onCommandClick: () => openCreateActivity({ type: 'Task' }),
|
||||
onCommandClick: () =>
|
||||
openCreateActivity({ type: 'Task', targetableObjects: [] }),
|
||||
},
|
||||
]);
|
||||
}, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]);
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useBlockNote } from '@blocknote/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
@@ -42,10 +44,6 @@ export const ActivityBodyEditor = ({
|
||||
activity,
|
||||
fillTitleFromBody,
|
||||
}: ActivityBodyEditorProps) => {
|
||||
const [stringifiedBodyFromEditor, setStringifiedBodyFromEditor] = useState<
|
||||
string | null
|
||||
>(activity.body);
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activity.id,
|
||||
@@ -96,19 +94,21 @@ export const ActivityBodyEditor = ({
|
||||
const blockBody = JSON.parse(newStringifiedBody);
|
||||
const newTitleFromBody = blockBody[0]?.content?.[0]?.text;
|
||||
|
||||
modifyActivityFromCache(activity.id, {
|
||||
title: () => {
|
||||
return newTitleFromBody;
|
||||
},
|
||||
});
|
||||
|
||||
persistTitleAndBodyDebounced(newTitleFromBody, newStringifiedBody);
|
||||
},
|
||||
[activity.id, modifyActivityFromCache, persistTitleAndBodyDebounced],
|
||||
[persistTitleAndBodyDebounced],
|
||||
);
|
||||
|
||||
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
|
||||
canCreateActivityState,
|
||||
);
|
||||
|
||||
const handleBodyChange = useCallback(
|
||||
(activityBody: string) => {
|
||||
if (!canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
updateTitleAndBody(activityBody);
|
||||
} else {
|
||||
@@ -120,18 +120,11 @@ export const ActivityBodyEditor = ({
|
||||
persistBodyDebounced,
|
||||
activityTitleHasBeenSet,
|
||||
updateTitleAndBody,
|
||||
setCanCreateActivity,
|
||||
canCreateActivity,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isNonEmptyString(stringifiedBodyFromEditor) &&
|
||||
activity.body !== stringifiedBodyFromEditor
|
||||
) {
|
||||
handleBodyChange(stringifiedBodyFromEditor);
|
||||
}
|
||||
}, [stringifiedBodyFromEditor, handleBodyChange, activity]);
|
||||
|
||||
const slashMenuItems = getSlashMenu();
|
||||
|
||||
const [uploadFile] = useUploadFileMutation();
|
||||
@@ -160,9 +153,57 @@ export const ActivityBodyEditor = ({
|
||||
? JSON.parse(activity.body)
|
||||
: undefined,
|
||||
domAttributes: { editor: { class: 'editor' } },
|
||||
onEditorContentChange: (editor: BlockNoteEditor) => {
|
||||
setStringifiedBodyFromEditor(JSON.stringify(editor.topLevelBlocks) ?? '');
|
||||
},
|
||||
onEditorContentChange: useRecoilCallback(
|
||||
({ 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,
|
||||
blockSpecs: blockSpecs,
|
||||
uploadFile: handleUploadAttachment,
|
||||
|
||||
@@ -8,7 +8,9 @@ import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdo
|
||||
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
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 { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@@ -78,8 +81,10 @@ export const ActivityEditor = ({
|
||||
const { deleteActivityFromCache } = useDeleteActivityFromCache();
|
||||
|
||||
const useUpsertOneActivityMutation: RecordUpdateHook = () => {
|
||||
const upsertActivityMutation = ({ variables }: RecordUpdateHookParams) => {
|
||||
upsertActivity({ activity, input: variables.updateOneRecordInput });
|
||||
const upsertActivityMutation = async ({
|
||||
variables,
|
||||
}: RecordUpdateHookParams) => {
|
||||
await upsertActivity({ activity, input: variables.updateOneRecordInput });
|
||||
};
|
||||
|
||||
return [upsertActivityMutation, { loading: false }];
|
||||
@@ -104,6 +109,20 @@ export const ActivityEditor = ({
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
|
||||
isActivityInCreateModeState,
|
||||
);
|
||||
|
||||
const [isUpsertingActivityInDB] = useRecoilState(
|
||||
isUpsertingActivityInDBState,
|
||||
);
|
||||
|
||||
const [canCreateActivity] = useRecoilState(canCreateActivityState);
|
||||
|
||||
const [activityFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(activity.id),
|
||||
);
|
||||
|
||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
@@ -112,16 +131,40 @@ export const ActivityEditor = ({
|
||||
fieldPosition: 2,
|
||||
});
|
||||
|
||||
const [isCreatingActivity, setIsCreatingActivity] = useRecoilState(
|
||||
isCreatingActivityState,
|
||||
);
|
||||
|
||||
useRegisterClickOutsideListenerCallback({
|
||||
callbackId: 'activity-editor',
|
||||
callbackFunction: () => {
|
||||
if (isCreatingActivity) {
|
||||
setIsCreatingActivity(false);
|
||||
deleteActivityFromCache(activity);
|
||||
if (isUpsertingActivityInDB || !activityFromStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -12,14 +12,14 @@ const StyledContainer = styled.div`
|
||||
export const ActivityTargetChips = ({
|
||||
activityTargetObjectRecords,
|
||||
}: {
|
||||
activityTargetObjectRecords: ActivityTargetObjectRecord[];
|
||||
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
|
||||
}) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{activityTargetObjectRecords?.map((activityTargetObjectRecord) => (
|
||||
<RecordChip
|
||||
key={activityTargetObjectRecord.targetObjectRecord.id}
|
||||
record={activityTargetObjectRecord.targetObjectRecord}
|
||||
key={activityTargetObjectRecord.targetObject.id}
|
||||
record={activityTargetObjectRecord.targetObject}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useModifyRecordFromCache } from '@/object-record/cache/hooks/useModifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxShape,
|
||||
@@ -57,7 +59,13 @@ type 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();
|
||||
|
||||
@@ -115,7 +123,17 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
}, 500);
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setInternalTitle(newTitle);
|
||||
setActivityInStore((currentActivity) => {
|
||||
return {
|
||||
...currentActivity,
|
||||
id: activity.id,
|
||||
title: newTitle,
|
||||
};
|
||||
});
|
||||
|
||||
if (isNonEmptyString(newTitle) && !canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
modifyActivityFromCache(activity.id, {
|
||||
title: () => {
|
||||
@@ -153,7 +171,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
ref={titleInputRef}
|
||||
placeholder={`${activity.type} title`}
|
||||
onChange={(event) => handleTitleChange(event.target.value)}
|
||||
value={internalTitle}
|
||||
value={activityInStore?.title ?? ''}
|
||||
completed={completed}
|
||||
onBlur={handleBlur}
|
||||
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
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;
|
||||
|
||||
export const useActivityById = ({ activityId }: { activityId: string }) => {
|
||||
const setEntityFields = useSetRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
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,
|
||||
objectRecordId: activityId,
|
||||
skip: !activityId,
|
||||
onCompleted: (activityWithConnections: any) => {
|
||||
const { activity } = makeActivityWithoutConnection(
|
||||
activityWithConnections,
|
||||
);
|
||||
|
||||
setEntityFields(activity);
|
||||
},
|
||||
depth: QUERY_DEPTH_TO_GET_ACTIVITY_TARGET_RELATIONS,
|
||||
});
|
||||
|
||||
const { activity } = makeActivityWithoutConnection(activityWithConnections);
|
||||
const { activity } = activityWithConnections
|
||||
? makeActivityWithoutConnection(activityWithConnections as any)
|
||||
: { activity: null };
|
||||
|
||||
return {
|
||||
activity,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
@@ -16,7 +17,7 @@ export const useActivityTargetObjectRecords = ({
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const { records: activityTargets, loading: loadingActivityTargets } =
|
||||
useFindManyRecords({
|
||||
useFindManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
skip: !isNonEmptyString(activityId),
|
||||
filter: {
|
||||
@@ -27,7 +28,7 @@ export const useActivityTargetObjectRecords = ({
|
||||
});
|
||||
|
||||
const activityTargetObjectRecords = activityTargets
|
||||
.map<Nullable<ActivityTargetObjectRecord>>((activityTarget) => {
|
||||
.map<Nullable<ActivityTargetWithTargetRecord>>((activityTarget) => {
|
||||
const correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
|
||||
@@ -39,8 +40,8 @@ export const useActivityTargetObjectRecords = ({
|
||||
}
|
||||
|
||||
return {
|
||||
activityTargetRecord: activityTarget,
|
||||
targetObjectRecord:
|
||||
activityTarget: activityTarget,
|
||||
targetObject:
|
||||
activityTarget[correspondingObjectMetadataItem.nameSingular],
|
||||
targetObjectMetadataItem: correspondingObjectMetadataItem,
|
||||
targetObjectNameSingular: correspondingObjectMetadataItem.nameSingular,
|
||||
|
||||
@@ -22,6 +22,9 @@ export const useActivityTargetsForTargetableObject = ({
|
||||
|
||||
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 } =
|
||||
useFindManyRecords({
|
||||
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 { v4 } from 'uuid';
|
||||
|
||||
import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections';
|
||||
import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-cell/hooks/useInjectIntoActivityTargetInlineCellCache';
|
||||
import { useInjectIntoTimelineActivitiesQueryAfterDrawerMount } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueryAfterDrawerMount';
|
||||
import { Activity, ActivityType } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
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 { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export const useCreateActivityInCache = () => {
|
||||
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
|
||||
@@ -28,46 +27,36 @@ export const useCreateActivityInCache = () => {
|
||||
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { record: workspaceMemberRecord } = useFindOneRecord({
|
||||
const { record: currentWorkspaceMemberRecord } = useFindOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
objectRecordId: currentWorkspaceMember?.id,
|
||||
depth: 3,
|
||||
});
|
||||
|
||||
const { injectIntoTimelineActivitiesQueryAfterDrawerMount } =
|
||||
useInjectIntoTimelineActivitiesQueryAfterDrawerMount();
|
||||
|
||||
const { injectIntoActivityTargetInlineCellCache } =
|
||||
useInjectIntoActivityTargetInlineCellCache();
|
||||
|
||||
const {
|
||||
attachRelationInBothDirections:
|
||||
attachRelationSourceRecordToItsRelationTargetRecordsAndViceVersaInCache,
|
||||
} = useAttachRelationInBothDirections();
|
||||
const { attachRelationInBothDirections } =
|
||||
useAttachRelationInBothDirections();
|
||||
|
||||
const createActivityInCache = ({
|
||||
type,
|
||||
targetableObjects,
|
||||
timelineTargetableObject,
|
||||
assigneeId,
|
||||
customAssignee,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
timelineTargetableObject: ActivityTargetableObject;
|
||||
assigneeId?: string;
|
||||
customAssignee?: WorkspaceMember;
|
||||
}) => {
|
||||
const activityId = v4();
|
||||
|
||||
const createdActivityInCache = createOneActivityInCache({
|
||||
id: activityId,
|
||||
author: workspaceMemberRecord,
|
||||
authorId: workspaceMemberRecord?.id,
|
||||
assignee: !assigneeId ? workspaceMemberRecord : undefined,
|
||||
assigneeId:
|
||||
assigneeId ?? isNonEmptyString(workspaceMemberRecord?.id)
|
||||
? workspaceMemberRecord?.id
|
||||
: undefined,
|
||||
type: type,
|
||||
author: currentWorkspaceMemberRecord,
|
||||
authorId: currentWorkspaceMemberRecord?.id,
|
||||
assignee: customAssignee ?? currentWorkspaceMemberRecord,
|
||||
assigneeId: customAssignee?.id ?? currentWorkspaceMemberRecord?.id,
|
||||
type,
|
||||
});
|
||||
|
||||
const activityTargetsToCreate =
|
||||
@@ -80,18 +69,12 @@ export const useCreateActivityInCache = () => {
|
||||
activityTargetsToCreate,
|
||||
);
|
||||
|
||||
injectIntoTimelineActivitiesQueryAfterDrawerMount({
|
||||
activityToInject: createdActivityInCache,
|
||||
activityTargetsToInject: createdActivityTargetsInCache,
|
||||
timelineTargetableObject,
|
||||
});
|
||||
|
||||
injectIntoActivityTargetInlineCellCache({
|
||||
activityId,
|
||||
activityTargetsToInject: createdActivityTargetsInCache,
|
||||
});
|
||||
|
||||
attachRelationSourceRecordToItsRelationTargetRecordsAndViceVersaInCache({
|
||||
attachRelationInBothDirections({
|
||||
sourceRecord: createdActivityInCache,
|
||||
fieldNameOnSourceRecord: 'activityTargets',
|
||||
sourceObjectNameSingular: CoreObjectNameSingular.Activity,
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { 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 { 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 useInjectIntoTimelineActivitiesQueryAfterDrawerMount = () => {
|
||||
// TODO: create a generic hook from this
|
||||
export const useInjectIntoActivitiesQuery = () => {
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItemOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
@@ -46,79 +49,85 @@ export const useInjectIntoTimelineActivitiesQueryAfterDrawerMount = () => {
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
});
|
||||
|
||||
const injectIntoTimelineActivitiesQueryAfterDrawerMount = ({
|
||||
const injectActivitiesQueries = ({
|
||||
activityToInject,
|
||||
activityTargetsToInject,
|
||||
timelineTargetableObject,
|
||||
targetableObjects,
|
||||
activitiesFilters,
|
||||
activitiesOrderByVariables,
|
||||
}: {
|
||||
activityToInject: Activity;
|
||||
activityTargetsToInject: ActivityTarget[];
|
||||
timelineTargetableObject: ActivityTargetableObject;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
activitiesFilters: ObjectRecordQueryFilter;
|
||||
activitiesOrderByVariables: OrderByField;
|
||||
}) => {
|
||||
const newActivity = {
|
||||
...activityToInject,
|
||||
__typename: 'Activity',
|
||||
};
|
||||
|
||||
const targetObjectFieldName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: timelineTargetableObject.targetObjectNameSingular,
|
||||
const findManyActivitiyTargetsQueryFilter = getActivityTargetsFilter({
|
||||
targetableObjects,
|
||||
});
|
||||
|
||||
const activitiyTargetsForTargetableObjectQueryVariables = {
|
||||
filter: {
|
||||
[targetObjectFieldName]: {
|
||||
eq: timelineTargetableObject.id,
|
||||
},
|
||||
},
|
||||
const findManyActivitiyTargetsQueryVariables = {
|
||||
filter: findManyActivitiyTargetsQueryFilter,
|
||||
};
|
||||
|
||||
const existingActivityTargetsForTargetableObject =
|
||||
readFindManyActivityTargetsQueryInCache({
|
||||
queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
|
||||
});
|
||||
const existingActivityTargets = readFindManyActivityTargetsQueryInCache({
|
||||
queryVariables: findManyActivitiyTargetsQueryVariables,
|
||||
});
|
||||
|
||||
const newActivityTargetsForTargetableObject = [
|
||||
...existingActivityTargetsForTargetableObject,
|
||||
const newActivityTargets = [
|
||||
...existingActivityTargets,
|
||||
...activityTargetsToInject,
|
||||
];
|
||||
|
||||
const existingActivityIds = existingActivityTargetsForTargetableObject
|
||||
const existingActivityIds = existingActivityTargets
|
||||
?.map((activityTarget) => activityTarget.activityId)
|
||||
.filter(isNonEmptyString);
|
||||
|
||||
const timelineActivitiesQueryVariablesBeforeDrawerMount =
|
||||
makeTimelineActivitiesQueryVariables({
|
||||
activityIds: existingActivityIds,
|
||||
});
|
||||
const currentFindManyActivitiesQueryVariables = {
|
||||
filter: {
|
||||
id: {
|
||||
in: existingActivityIds.toSorted(sortByAscString),
|
||||
},
|
||||
...activitiesFilters,
|
||||
},
|
||||
orderBy: activitiesOrderByVariables,
|
||||
};
|
||||
|
||||
const existingActivities = readFindManyActivitiesQueryInCache({
|
||||
queryVariables: timelineActivitiesQueryVariablesBeforeDrawerMount,
|
||||
queryVariables: currentFindManyActivitiesQueryVariables,
|
||||
});
|
||||
|
||||
const activityIdsAfterDrawerMount = [
|
||||
...existingActivityIds,
|
||||
newActivity.id,
|
||||
];
|
||||
const nextActivityIds = [...existingActivityIds, newActivity.id];
|
||||
|
||||
const timelineActivitiesQueryVariablesAfterDrawerMount =
|
||||
makeTimelineActivitiesQueryVariables({
|
||||
activityIds: activityIdsAfterDrawerMount,
|
||||
});
|
||||
const nextFindManyActivitiesQueryVariables = {
|
||||
filter: {
|
||||
id: {
|
||||
in: nextActivityIds.toSorted(sortByAscString),
|
||||
},
|
||||
...activitiesFilters,
|
||||
},
|
||||
orderBy: activitiesOrderByVariables,
|
||||
};
|
||||
|
||||
overwriteFindManyActivityTargetsQueryInCache({
|
||||
objectRecordsToOverwrite: newActivityTargetsForTargetableObject,
|
||||
queryVariables: activitiyTargetsForTargetableObjectQueryVariables,
|
||||
objectRecordsToOverwrite: newActivityTargets,
|
||||
queryVariables: findManyActivitiyTargetsQueryVariables,
|
||||
});
|
||||
|
||||
const newActivities = [newActivity, ...existingActivities];
|
||||
|
||||
overwriteFindManyActivitiesInCache({
|
||||
objectRecordsToOverwrite: newActivities,
|
||||
queryVariables: timelineActivitiesQueryVariablesAfterDrawerMount,
|
||||
queryVariables: nextFindManyActivitiesQueryVariables,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
injectIntoTimelineActivitiesQueryAfterDrawerMount,
|
||||
injectActivitiesQueries,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
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 { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
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';
|
||||
|
||||
export const useOpenActivityRightDrawer = () => {
|
||||
const { openRightDrawer } = useRightDrawer();
|
||||
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
|
||||
const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
|
||||
useRightDrawer();
|
||||
const [viewableActivityId, setViewableActivityId] = useRecoilState(
|
||||
viewableActivityIdState,
|
||||
);
|
||||
const [, setActivityInDrawer] = useRecoilState(activityInDrawerState);
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return (activityId: string) => {
|
||||
return (activity: Activity) => {
|
||||
if (
|
||||
isRightDrawerOpen &&
|
||||
rightDrawerPage === RightDrawerPages.EditActivity &&
|
||||
viewableActivityId === activity.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
setViewableActivityId(activityId);
|
||||
setViewableActivityId(activity.id);
|
||||
setActivityInDrawer(activity);
|
||||
openRightDrawer(RightDrawerPages.EditActivity);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,102 +1,69 @@
|
||||
import { useCallback } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { Activity, ActivityType } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
|
||||
import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
|
||||
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
|
||||
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
|
||||
import { 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 { 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 { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '../utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
|
||||
|
||||
export const useOpenCreateActivityDrawer = () => {
|
||||
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 { createActivityInCache } = useCreateActivityInCache();
|
||||
|
||||
const [, setActivityTargetableEntityArray] = useRecoilState(
|
||||
activityTargetableEntityArrayState,
|
||||
);
|
||||
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
|
||||
|
||||
return useCallback(
|
||||
async ({
|
||||
const setIsCreatingActivity = useSetRecoilState(isActivityInCreateModeState);
|
||||
|
||||
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,
|
||||
targetableObjects,
|
||||
assigneeId,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
targetableObjects?: ActivityTargetableObject[];
|
||||
assigneeId?: string;
|
||||
}) => {
|
||||
const flattenedTargetableObjects = targetableObjects
|
||||
? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
|
||||
targetableObjects,
|
||||
)
|
||||
: [];
|
||||
customAssignee,
|
||||
});
|
||||
|
||||
const createdActivity = await createOneActivity?.({
|
||||
authorId: currentWorkspaceMember?.id,
|
||||
assigneeId:
|
||||
assigneeId ?? isNonEmptyString(currentWorkspaceMember?.id)
|
||||
? currentWorkspaceMember?.id
|
||||
: undefined,
|
||||
type: type,
|
||||
});
|
||||
setActivityInDrawer(createdActivityInCache);
|
||||
setTemporaryActivityForEditor(createdActivityInCache);
|
||||
setIsCreatingActivity(true);
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
setViewableActivityId(createdActivityInCache.id);
|
||||
setActivityTargetableEntityArray(targetableObjects ?? []);
|
||||
openRightDrawer(RightDrawerPages.CreateActivity);
|
||||
setIsUpsertingActivityInDB(false);
|
||||
};
|
||||
|
||||
if (!createdActivity) {
|
||||
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,
|
||||
],
|
||||
);
|
||||
return openCreateActivityDrawer;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityType } from '@/activities/types/Activity';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
@@ -8,8 +9,6 @@ import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer';
|
||||
|
||||
export const useOpenCreateActivityDrawerForSelectedRowIds = (
|
||||
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 { 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 { useActivityConnectionUtils } from '@/activities/utils/useActivityConnectionUtils';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
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 = () => {
|
||||
const [isCreatingActivity, setIsCreatingActivity] = useRecoilState(
|
||||
isCreatingActivityState,
|
||||
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
|
||||
isActivityInCreateModeState,
|
||||
);
|
||||
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
|
||||
@@ -17,26 +24,67 @@ export const useUpsertActivity = () => {
|
||||
|
||||
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,
|
||||
input,
|
||||
}: {
|
||||
activity: Activity;
|
||||
input: Partial<Activity>;
|
||||
}) => {
|
||||
if (isCreatingActivity) {
|
||||
createActivityInDB({
|
||||
setIsUpsertingActivityInDB(true);
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
const activityToCreate: Activity = {
|
||||
...activity,
|
||||
...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 {
|
||||
updateOneActivity?.({
|
||||
await updateOneActivity?.({
|
||||
idToUpdate: activity.id,
|
||||
updateOneRecordInput: input,
|
||||
});
|
||||
}
|
||||
|
||||
setIsUpsertingActivityInDB(false);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,10 +5,10 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
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 { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetObjectRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
@@ -27,19 +27,19 @@ const StyledSelectContainer = styled.div`
|
||||
|
||||
type ActivityTargetInlineCellEditModeProps = {
|
||||
activity: Activity;
|
||||
activityTargetObjectRecords: ActivityTargetObjectRecord[];
|
||||
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
|
||||
};
|
||||
|
||||
export const ActivityTargetInlineCellEditMode = ({
|
||||
activity,
|
||||
activityTargetObjectRecords,
|
||||
activityTargetWithTargetRecords,
|
||||
}: ActivityTargetInlineCellEditModeProps) => {
|
||||
const [isCreatingActivity] = useRecoilState(isCreatingActivityState);
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
|
||||
const selectedObjectRecordIds = activityTargetObjectRecords.map(
|
||||
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
|
||||
(activityTarget) => ({
|
||||
objectNameSingular: activityTarget.targetObjectNameSingular,
|
||||
id: activityTarget.targetObjectRecord.id,
|
||||
id: activityTarget.targetObject.id,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -73,90 +73,89 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
|
||||
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
|
||||
closeEditableField();
|
||||
const activityTargetRecordsToDelete = activityTargetObjectRecords.filter(
|
||||
|
||||
const activityTargetsToDelete = activityTargetWithTargetRecords.filter(
|
||||
(activityTargetObjectRecord) =>
|
||||
!selectedRecords.some(
|
||||
(selectedRecord) =>
|
||||
selectedRecord.recordIdentifier.id ===
|
||||
activityTargetObjectRecord.targetObjectRecord.id,
|
||||
activityTargetObjectRecord.targetObject.id,
|
||||
),
|
||||
);
|
||||
|
||||
const activityTargetRecordsToCreate = selectedRecords.filter(
|
||||
const selectedTargetObjectsToCreate = selectedRecords.filter(
|
||||
(selectedRecord) =>
|
||||
!activityTargetObjectRecords.some(
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.targetObjectRecord.id ===
|
||||
!activityTargetWithTargetRecords.some(
|
||||
(activityTargetWithTargetRecord) =>
|
||||
activityTargetWithTargetRecord.targetObject.id ===
|
||||
selectedRecord.recordIdentifier.id,
|
||||
),
|
||||
);
|
||||
|
||||
if (isCreatingActivity) {
|
||||
let activityTargetsForCreation = activity.activityTargets;
|
||||
const existingActivityTargets = activityTargetWithTargetRecords.map(
|
||||
(activityTargetObjectRecord) => activityTargetObjectRecord.activityTarget,
|
||||
);
|
||||
|
||||
if (isNonEmptyArray(activityTargetsForCreation)) {
|
||||
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,
|
||||
});
|
||||
let activityTargetsAfterUpdate = Array.from(existingActivityTargets);
|
||||
|
||||
return emptyActivityTarget;
|
||||
},
|
||||
);
|
||||
|
||||
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) => ({
|
||||
const activityTargetsToCreate = selectedTargetObjectsToCreate.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;
|
||||
},
|
||||
);
|
||||
|
||||
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(
|
||||
activityTargetRecordsToDelete.map(
|
||||
activityTargetsToDelete.map(
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.activityTargetRecord.id,
|
||||
activityTargetObjectRecord.activityTarget.id,
|
||||
),
|
||||
{
|
||||
skipOptimisticEffect: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -169,7 +168,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
return (
|
||||
<StyledSelectContainer>
|
||||
<MultipleObjectRecordSelect
|
||||
selectedObjectRecordIds={selectedObjectRecordIds}
|
||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ActivityTargetsInlineCell = ({
|
||||
editModeContent={
|
||||
<ActivityTargetInlineCellEditMode
|
||||
activity={activity}
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
||||
/>
|
||||
}
|
||||
label="Relations"
|
||||
|
||||
@@ -94,7 +94,7 @@ export const NoteCard = ({
|
||||
<FieldContext.Provider value={fieldContext as GenericFieldContextType}>
|
||||
<StyledCard isSingleNote={isSingleNote}>
|
||||
<StyledCardDetailsContainer
|
||||
onClick={() => openActivityRightDrawer(note.id)}
|
||||
onClick={() => openActivityRightDrawer(note)}
|
||||
>
|
||||
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
|
||||
<StyledCardContent>{body}</StyledCardContent>
|
||||
|
||||
@@ -29,7 +29,7 @@ const StyledTitleBar = styled.h3`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h3`
|
||||
const StyledTitle = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
@@ -27,10 +27,14 @@ export const Notes = ({
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { notes } = useNotes(targetableObject);
|
||||
const { notes, initialized } = useNotes(targetableObject);
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
if (!initialized) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (notes?.length === 0) {
|
||||
return (
|
||||
<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 { 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';
|
||||
|
||||
export const useNotes = (targetableObject: ActivityTargetableObject) => {
|
||||
const { activityTargets } = useActivityTargetsForTargetableObject({
|
||||
targetableObject,
|
||||
});
|
||||
|
||||
const filter = {
|
||||
id: {
|
||||
in: activityTargets?.map((activityTarget) => activityTarget.activityId),
|
||||
const { activities, initialized, loading } = useActivities({
|
||||
activitiesFilters: {
|
||||
type: { eq: 'Note' },
|
||||
},
|
||||
type: { eq: 'Note' },
|
||||
};
|
||||
|
||||
const orderBy = {
|
||||
createdAt: 'AscNullsFirst',
|
||||
} as OrderByField;
|
||||
|
||||
const { records: notes } = useFindManyRecords({
|
||||
skip: !activityTargets?.length,
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
filter,
|
||||
orderBy,
|
||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
targetableObjects: [targetableObject],
|
||||
});
|
||||
|
||||
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 { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
|
||||
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 { 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 { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { mapToRecordId } from '@/object-record/utils/mapToObjectId';
|
||||
import { IconPlus, IconTrash } from '@/ui/display/icon';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
|
||||
@@ -24,6 +29,8 @@ const StyledButtonContainer = styled.div`
|
||||
|
||||
export const ActivityActionBar = () => {
|
||||
const viewableActivityId = useRecoilValue(viewableActivityIdState);
|
||||
const activityInDrawer = useRecoilValue(activityInDrawerState);
|
||||
|
||||
const activityTargetableEntityArray = useRecoilValue(
|
||||
activityTargetableEntityArrayState,
|
||||
);
|
||||
@@ -33,27 +40,52 @@ export const ActivityActionBar = () => {
|
||||
refetchFindManyQuery: true,
|
||||
});
|
||||
|
||||
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
|
||||
{
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
refetchFindManyQuery: true,
|
||||
},
|
||||
);
|
||||
|
||||
const [temporaryActivityForEditor, setTemporaryActivityForEditor] =
|
||||
useRecoilState(temporaryActivityForEditorState);
|
||||
|
||||
const { deleteActivityFromCache } = useDeleteActivityFromCache();
|
||||
|
||||
const [isCreatingActivity] = useRecoilState(isCreatingActivityState);
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
const [isUpsertingActivityInDB] = useRecoilState(
|
||||
isUpsertingActivityInDBState,
|
||||
);
|
||||
const timelineTargetableObject = useRecoilValue(
|
||||
timelineTargetableObjectState,
|
||||
);
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
const { removeFromTimelineActivitiesQueries } =
|
||||
useRemoveFromTimelineActivitiesQueries();
|
||||
|
||||
const deleteActivity = () => {
|
||||
if (viewableActivityId) {
|
||||
if (isCreatingActivity && isDefined(temporaryActivityForEditor)) {
|
||||
if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) {
|
||||
deleteActivityFromCache(temporaryActivityForEditor);
|
||||
setTemporaryActivityForEditor(null);
|
||||
} else {
|
||||
deleteOneActivity?.(viewableActivityId);
|
||||
// TODO: find a better way to do this with custom optimistic rendering for activities
|
||||
apolloClient.refetchQueries({
|
||||
include: ['FindManyActivities'],
|
||||
});
|
||||
if (activityInDrawer) {
|
||||
const activityTargetIdsToDelete =
|
||||
activityInDrawer?.activityTargets.map(mapToRecordId) ?? [];
|
||||
|
||||
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 = () => {
|
||||
setIsRightDrawerOpen(false);
|
||||
if (record) {
|
||||
if (record && timelineTargetableObject) {
|
||||
openCreateActivity({
|
||||
type: record.type,
|
||||
assigneeId: isNonEmptyString(record.assigneeId)
|
||||
? record.assigneeId
|
||||
: undefined,
|
||||
customAssignee: record.assignee,
|
||||
targetableObjects: activityTargetableEntityArray,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const actionsAreDisabled = isUpsertingActivityInDB;
|
||||
|
||||
const isCreateActionDisabled = isActivityInCreateMode;
|
||||
|
||||
return (
|
||||
<StyledButtonContainer>
|
||||
<IconButton
|
||||
@@ -84,12 +118,14 @@ export const ActivityActionBar = () => {
|
||||
onClick={addActivity}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
disabled={actionsAreDisabled || isCreateActionDisabled}
|
||||
/>
|
||||
<IconButton
|
||||
Icon={IconTrash}
|
||||
onClick={deleteActivity}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
disabled={actionsAreDisabled}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
);
|
||||
|
||||
@@ -24,11 +24,11 @@ export const RightDrawerActivity = ({
|
||||
showComment = true,
|
||||
fillTitleFromBody = false,
|
||||
}: RightDrawerActivityProps) => {
|
||||
const { activity } = useActivityById({
|
||||
const { activity, loading } = useActivityById({
|
||||
activityId,
|
||||
});
|
||||
|
||||
if (!activity) {
|
||||
if (!activity || loading) {
|
||||
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 { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
||||
|
||||
type PageAddTaskButtonProps = {
|
||||
filterDropdownId: string;
|
||||
};
|
||||
|
||||
export const PageAddTaskButton = ({
|
||||
filterDropdownId,
|
||||
}: PageAddTaskButtonProps) => {
|
||||
const { selectedFilter } = useFilterDropdown({
|
||||
filterDropdownId: filterDropdownId,
|
||||
});
|
||||
|
||||
export const PageAddTaskButton = () => {
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
// TODO: fetch workspace member from filter here
|
||||
|
||||
const handleClick = () => {
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
assigneeId: isNonEmptyString(selectedFilter?.value)
|
||||
? selectedFilter?.value
|
||||
: undefined,
|
||||
targetableObjects: [],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export const TaskGroups = ({
|
||||
upcomingTasks,
|
||||
unscheduledTasks,
|
||||
completedTasks,
|
||||
initialized,
|
||||
} = useTasks({
|
||||
filterDropdownId: filterDropdownId,
|
||||
targetableObjects: targetableObjects ?? [],
|
||||
@@ -50,6 +51,10 @@ export const TaskGroups = ({
|
||||
const { getActiveTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
|
||||
const activeTabId = useRecoilValue(getActiveTabIdState());
|
||||
|
||||
if (!initialized) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (
|
||||
(activeTabId !== 'done' &&
|
||||
todayOrPreviousTasks?.length === 0 &&
|
||||
@@ -73,7 +78,7 @@ export const TaskGroups = ({
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
targetableObjects,
|
||||
targetableObjects: targetableObjects ?? [],
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -2,13 +2,12 @@ import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { GraphQLActivity } from '@/activities/types/GraphQLActivity';
|
||||
|
||||
import { TaskRow } from './TaskRow';
|
||||
|
||||
type TaskListProps = {
|
||||
title?: string;
|
||||
tasks: Omit<Activity, 'assigneeId'>[];
|
||||
tasks: Activity[];
|
||||
button?: ReactElement | false;
|
||||
};
|
||||
|
||||
@@ -61,7 +60,7 @@ export const TaskList = ({ title, tasks, button }: TaskListProps) => (
|
||||
</StyledTitleBar>
|
||||
<StyledTaskRows>
|
||||
{tasks.map((task) => (
|
||||
<TaskRow key={task.id} task={task as unknown as GraphQLActivity} />
|
||||
<TaskRow key={task.id} task={task} />
|
||||
))}
|
||||
</StyledTaskRows>
|
||||
</StyledContainer>
|
||||
|
||||
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
|
||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
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 { IconCalendar, IconComment } from '@/ui/display/icon';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
||||
@@ -71,11 +71,7 @@ const StyledPlaceholder = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
`;
|
||||
|
||||
export const TaskRow = ({
|
||||
task,
|
||||
}: {
|
||||
task: Omit<GraphQLActivity, 'assigneeId'>;
|
||||
}) => {
|
||||
export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
const theme = useTheme();
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
|
||||
@@ -89,7 +85,7 @@ export const TaskRow = ({
|
||||
return (
|
||||
<StyledContainer
|
||||
onClick={() => {
|
||||
openActivityRightDrawer(task.id);
|
||||
openActivityRightDrawer(task);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
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 { 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 { LeafObjectRecordFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
type UseTasksProps = {
|
||||
@@ -23,43 +21,6 @@ export const useTasks = ({
|
||||
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
|
||||
? {
|
||||
assigneeId: {
|
||||
@@ -68,32 +29,34 @@ export const useTasks = ({
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const { records: completeTasksData } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
skip: skipRequest,
|
||||
filter: {
|
||||
const skipActivityTargets = !isNonEmptyArray(targetableObjects);
|
||||
|
||||
const {
|
||||
activities: completeTasksData,
|
||||
initialized: initializedCompleteTasks,
|
||||
} = useActivities({
|
||||
targetableObjects,
|
||||
activitiesFilters: {
|
||||
completedAt: { is: 'NOT_NULL' },
|
||||
...idFilter,
|
||||
type: { eq: 'Task' },
|
||||
...assigneeIdFilter,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'DescNullsFirst',
|
||||
},
|
||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
skipActivityTargets,
|
||||
});
|
||||
|
||||
const { records: incompleteTaskData } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
skip: skipRequest,
|
||||
filter: {
|
||||
const {
|
||||
activities: incompleteTaskData,
|
||||
initialized: initializedIncompleteTasks,
|
||||
} = useActivities({
|
||||
targetableObjects,
|
||||
activitiesFilters: {
|
||||
completedAt: { is: 'NULL' },
|
||||
...idFilter,
|
||||
type: { eq: 'Task' },
|
||||
...assigneeIdFilter,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'DescNullsFirst',
|
||||
},
|
||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
skipActivityTargets,
|
||||
});
|
||||
|
||||
const todayOrPreviousTasks = incompleteTaskData?.filter((task) => {
|
||||
@@ -125,5 +88,6 @@ export const useTasks = ({
|
||||
upcomingTasks: (upcomingTasks ?? []) as Activity[],
|
||||
unscheduledTasks: (unscheduledTasks ?? []) as Activity[],
|
||||
completedTasks: (completedTasks ?? []) as Activity[],
|
||||
initialized: initializedCompleteTasks && initializedIncompleteTasks,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
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 AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||
import {
|
||||
@@ -11,6 +15,7 @@ import {
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { TimelineItemsContainer } from './TimelineItemsContainer';
|
||||
|
||||
@@ -31,11 +36,22 @@ export const Timeline = ({
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { activities, initialized } = useTimelineActivities({
|
||||
targetableObject,
|
||||
const { activities, initialized, noActivities } = useActivities({
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { IconCheckbox, IconNotes } from '@/ui/display/icon';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import {
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
@@ -135,19 +136,7 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
|
||||
`;
|
||||
|
||||
type TimelineActivityProps = {
|
||||
activity: Pick<
|
||||
Activity,
|
||||
| 'id'
|
||||
| 'title'
|
||||
| 'body'
|
||||
| 'createdAt'
|
||||
| 'completedAt'
|
||||
| 'type'
|
||||
| 'comments'
|
||||
| 'dueAt'
|
||||
> & { author?: Pick<WorkspaceMember, 'name' | 'avatarUrl'> } & {
|
||||
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
||||
};
|
||||
activity: Activity;
|
||||
isLastActivity?: boolean;
|
||||
};
|
||||
|
||||
@@ -160,6 +149,8 @@ export const TimelineActivity = ({
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
const theme = useTheme();
|
||||
|
||||
const activityFromStore = useRecoilValue(recordStoreFamilyState(activity.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTimelineItemContainer>
|
||||
@@ -191,11 +182,13 @@ export const TimelineActivity = ({
|
||||
</StyledIconContainer>
|
||||
{(activity.type === 'Note' || activity.type === 'Task') && (
|
||||
<StyledActivityTitle
|
||||
onClick={() => openActivityRightDrawer(activity.id)}
|
||||
onClick={() => openActivityRightDrawer(activity)}
|
||||
>
|
||||
“
|
||||
<StyledActivityLink title={activity.title ?? '(No Title)'}>
|
||||
{activity.title ?? '(No Title)'}
|
||||
<StyledActivityLink
|
||||
title={activityFromStore?.title ?? '(No Title)'}
|
||||
>
|
||||
{activityFromStore?.title ?? '(No Title)'}
|
||||
</StyledActivityLink>
|
||||
“
|
||||
</StyledActivityTitle>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
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 {
|
||||
IconCheckbox,
|
||||
@@ -19,7 +19,7 @@ export const TimelineCreateButtonGroup = ({
|
||||
const { getActiveTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
|
||||
const setActiveTabId = useSetRecoilState(getActiveTabIdState());
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawerV2();
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
return (
|
||||
<ButtonGroup variant={'secondary'}>
|
||||
@@ -30,7 +30,6 @@ export const TimelineCreateButtonGroup = ({
|
||||
openCreateActivity({
|
||||
type: 'Note',
|
||||
targetableObjects: [targetableObject],
|
||||
timelineTargetableObject: targetableObject,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -41,7 +40,6 @@ export const TimelineCreateButtonGroup = ({
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
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 { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
|
||||
import { timelineTargetableObjectState } from '@/activities/timeline/states/timelineTargetableObjectState';
|
||||
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
|
||||
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 { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useTimelineActivities = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
|
||||
|
||||
const [, setTimelineTargetableObject] = useRecoilState(
|
||||
timelineTargetableObjectState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(targetableObject)) {
|
||||
setTimelineTargetableObject(targetableObject);
|
||||
}
|
||||
}, [targetableObject, setTimelineTargetableObject]);
|
||||
|
||||
const {
|
||||
activityTargets,
|
||||
loadingActivityTargets,
|
||||
@@ -23,9 +42,14 @@ export const useTimelineActivities = ({
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
const activityIds = activityTargets
|
||||
?.map((activityTarget) => activityTarget.activityId)
|
||||
.filter(isNonEmptyString);
|
||||
const activityIds = Array.from(
|
||||
new Set(
|
||||
activityTargets
|
||||
?.map((activityTarget) => activityTarget.activityId)
|
||||
.filter(isNonEmptyString)
|
||||
.toSorted(sortByAscString),
|
||||
),
|
||||
);
|
||||
|
||||
const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables(
|
||||
{
|
||||
@@ -33,17 +57,30 @@ export const useTimelineActivities = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { records: activities, loading: loadingActivities } =
|
||||
const { records: activitiesWithConnection, loading: loadingActivities } =
|
||||
useFindManyRecords<Activity>({
|
||||
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
filter: timelineActivitiesQueryVariables.filter,
|
||||
orderBy: timelineActivitiesQueryVariables.orderBy,
|
||||
onCompleted: () => {
|
||||
if (!initialized) {
|
||||
setInitialized(true);
|
||||
}
|
||||
},
|
||||
onCompleted: useRecoilCallback(
|
||||
({ set }) =>
|
||||
(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 =
|
||||
@@ -57,6 +94,11 @@ export const useTimelineActivities = ({
|
||||
|
||||
const loading = loadingActivities || loadingActivityTargets;
|
||||
|
||||
const activities = activitiesWithConnection
|
||||
?.map(makeActivityWithoutConnection as any)
|
||||
.map(({ activity }: any) => activity as any)
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
activities,
|
||||
loading,
|
||||
|
||||
@@ -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 { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
|
||||
export const makeTimelineActivitiesQueryVariables = ({
|
||||
activityIds,
|
||||
@@ -8,7 +9,7 @@ export const makeTimelineActivitiesQueryVariables = ({
|
||||
return {
|
||||
filter: {
|
||||
id: {
|
||||
in: activityIds,
|
||||
in: activityIds.toSorted(sortByAscString),
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export type ActivityTargetObjectRecord = {
|
||||
export type ActivityTargetWithTargetRecord = {
|
||||
targetObjectMetadataItem: ObjectMetadataItem;
|
||||
activityTargetRecord: ObjectRecord;
|
||||
targetObjectRecord: ObjectRecord;
|
||||
activityTarget: ActivityTarget;
|
||||
targetObject: ObjectRecord;
|
||||
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 = () => {
|
||||
const mapConnectionToRecords = useMapConnectionToRecords();
|
||||
|
||||
const makeActivityWithoutConnection = (activityWithConnections: any) => {
|
||||
const makeActivityWithoutConnection = (
|
||||
activityWithConnections: Activity & {
|
||||
activityTargets: ObjectRecordConnection<ActivityTarget>;
|
||||
comments: ObjectRecordConnection<Comment>;
|
||||
},
|
||||
) => {
|
||||
if (!isDefined(activityWithConnections)) {
|
||||
return { activity: null };
|
||||
throw new Error('Activity with connections is not defined');
|
||||
}
|
||||
|
||||
const hasActivityTargetsConnection = isObjectRecordConnection(
|
||||
@@ -77,11 +82,13 @@ export const useActivityConnectionUtils = () => {
|
||||
: [];
|
||||
|
||||
const activityTargets = {
|
||||
__typename: 'ActivityTargetConnection',
|
||||
edges: activityTargetEdges,
|
||||
pageInfo: getEmptyPageInfo(),
|
||||
} as ObjectRecordConnection<ActivityTarget>;
|
||||
|
||||
const comments = {
|
||||
__typename: 'CommentConnection',
|
||||
edges: commentEdges,
|
||||
pageInfo: getEmptyPageInfo(),
|
||||
} as ObjectRecordConnection<Comment>;
|
||||
@@ -90,6 +97,9 @@ export const useActivityConnectionUtils = () => {
|
||||
...activity,
|
||||
activityTargets,
|
||||
comments,
|
||||
} as Activity & {
|
||||
activityTargets: ObjectRecordConnection<ActivityTarget>;
|
||||
comments: ObjectRecordConnection<Comment>;
|
||||
};
|
||||
|
||||
return { activityWithConnection };
|
||||
|
||||
@@ -11,17 +11,19 @@ export const isObjectRecordConnection = (
|
||||
objectNameSingular,
|
||||
)}Connection`;
|
||||
const objectEdgeTypeName = `${capitalize(objectNameSingular)}Edge`;
|
||||
|
||||
const objectConnectionSchema = z.object({
|
||||
__typename: z.literal(objectConnectionTypeName),
|
||||
__typename: z.literal(objectConnectionTypeName).optional(),
|
||||
edges: z.array(
|
||||
z.object({
|
||||
__typename: z.literal(objectEdgeTypeName),
|
||||
__typename: z.literal(objectEdgeTypeName).optional(),
|
||||
node: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const connectionValidation = objectConnectionSchema.safeParse(value);
|
||||
|
||||
return connectionValidation.success;
|
||||
|
||||
@@ -32,29 +32,25 @@ export const triggerDetachRelationOptimisticEffect = ({
|
||||
targetRecordFieldValue,
|
||||
{ isReference, readField },
|
||||
) => {
|
||||
const isRelationTargetFieldAnObjectRecordConnection =
|
||||
isCachedObjectRecordConnection(
|
||||
sourceObjectNameSingular,
|
||||
targetRecordFieldValue,
|
||||
);
|
||||
|
||||
if (isRelationTargetFieldAnObjectRecordConnection) {
|
||||
const relationTargetFieldEdgesWithoutRelationSourceRecordToDetach =
|
||||
targetRecordFieldValue.edges.filter(
|
||||
({ node }) => readField('id', node) !== sourceRecordId,
|
||||
);
|
||||
|
||||
return {
|
||||
...targetRecordFieldValue,
|
||||
edges: relationTargetFieldEdgesWithoutRelationSourceRecordToDetach,
|
||||
};
|
||||
}
|
||||
|
||||
const isRelationTargetFieldASingleObjectRecord = isReference(
|
||||
const isRecordConnection = isCachedObjectRecordConnection(
|
||||
sourceObjectNameSingular,
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,41 +45,35 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
rootQueryCachedResponse,
|
||||
{ DELETE, readField, storeFieldName, toReference },
|
||||
) => {
|
||||
const rootQueryCachedResponseIsNotACachedObjectRecordConnection =
|
||||
!isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
const shouldSkip = !isCachedObjectRecordConnection(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
if (rootQueryCachedResponseIsNotACachedObjectRecordConnection) {
|
||||
if (shouldSkip) {
|
||||
return rootQueryCachedResponse;
|
||||
}
|
||||
|
||||
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
|
||||
const rootQueryConnection = rootQueryCachedResponse;
|
||||
|
||||
const { fieldArguments: rootQueryVariables } =
|
||||
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||
storeFieldName,
|
||||
);
|
||||
|
||||
const rootQueryCurrentCachedRecordEdges =
|
||||
readField<CachedObjectRecordEdge[]>(
|
||||
'edges',
|
||||
rootQueryCachedObjectRecordConnection,
|
||||
) ?? [];
|
||||
const rootQueryCurrentEdges =
|
||||
readField<CachedObjectRecordEdge[]>('edges', rootQueryConnection) ??
|
||||
[];
|
||||
|
||||
let rootQueryNextCachedRecordEdges = [
|
||||
...rootQueryCurrentCachedRecordEdges,
|
||||
];
|
||||
let rootQueryNextEdges = [...rootQueryCurrentEdges];
|
||||
|
||||
const rootQueryFilter = rootQueryVariables?.filter;
|
||||
const rootQueryOrderBy = rootQueryVariables?.orderBy;
|
||||
const rootQueryLimit = rootQueryVariables?.first;
|
||||
|
||||
const shouldTestThatUpdatedRecordMatchesThisRootQueryFilter =
|
||||
isDefined(rootQueryFilter);
|
||||
const shouldTryToMatchFilter = isDefined(rootQueryFilter);
|
||||
|
||||
if (shouldTestThatUpdatedRecordMatchesThisRootQueryFilter) {
|
||||
if (shouldTryToMatchFilter) {
|
||||
const updatedRecordMatchesThisRootQueryFilter =
|
||||
isRecordMatchingFilter({
|
||||
record: updatedRecord,
|
||||
@@ -88,24 +82,27 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
});
|
||||
|
||||
const updatedRecordIndexInRootQueryEdges =
|
||||
rootQueryCurrentCachedRecordEdges.findIndex(
|
||||
rootQueryCurrentEdges.findIndex(
|
||||
(cachedEdge) =>
|
||||
readField('id', cachedEdge.node) === updatedRecord.id,
|
||||
);
|
||||
|
||||
const updatedRecordFoundInRootQueryEdges =
|
||||
updatedRecordIndexInRootQueryEdges > -1;
|
||||
|
||||
const updatedRecordShouldBeAddedToRootQueryEdges =
|
||||
updatedRecordMatchesThisRootQueryFilter &&
|
||||
updatedRecordIndexInRootQueryEdges === -1;
|
||||
!updatedRecordFoundInRootQueryEdges;
|
||||
|
||||
const updatedRecordShouldBeRemovedFromRootQueryEdges =
|
||||
updatedRecordMatchesThisRootQueryFilter &&
|
||||
updatedRecordIndexInRootQueryEdges === -1;
|
||||
!updatedRecordMatchesThisRootQueryFilter &&
|
||||
updatedRecordFoundInRootQueryEdges;
|
||||
|
||||
if (updatedRecordShouldBeAddedToRootQueryEdges) {
|
||||
const updatedRecordNodeReference = toReference(updatedRecord);
|
||||
|
||||
if (isDefined(updatedRecordNodeReference)) {
|
||||
rootQueryNextCachedRecordEdges.push({
|
||||
rootQueryNextEdges.push({
|
||||
__typename: objectEdgeTypeName,
|
||||
node: updatedRecordNodeReference,
|
||||
cursor: '',
|
||||
@@ -114,18 +111,15 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
}
|
||||
|
||||
if (updatedRecordShouldBeRemovedFromRootQueryEdges) {
|
||||
rootQueryNextCachedRecordEdges.splice(
|
||||
updatedRecordIndexInRootQueryEdges,
|
||||
1,
|
||||
);
|
||||
rootQueryNextEdges.splice(updatedRecordIndexInRootQueryEdges, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const nextRootQueryEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
|
||||
const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
|
||||
|
||||
if (nextRootQueryEdgesShouldBeSorted) {
|
||||
rootQueryNextCachedRecordEdges = sortCachedObjectEdges({
|
||||
edges: rootQueryNextCachedRecordEdges,
|
||||
if (rootQueryNextEdgesShouldBeSorted) {
|
||||
rootQueryNextEdges = sortCachedObjectEdges({
|
||||
edges: rootQueryNextEdges,
|
||||
orderBy: rootQueryOrderBy,
|
||||
readCacheField: readField,
|
||||
});
|
||||
@@ -158,12 +152,12 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
// the query's result.
|
||||
// In this case, invalidate the cache entry so it can be re-fetched.
|
||||
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
|
||||
// then in the merge function we could implement this mechanism to limit the number of edges in the cache
|
||||
const rootQueryNextCachedRecordEdgesLengthIsUnderLimit =
|
||||
rootQueryNextCachedRecordEdges.length < rootQueryLimit;
|
||||
rootQueryNextEdges.length < rootQueryLimit;
|
||||
|
||||
const shouldDeleteRootQuerySoItCanBeRefetched =
|
||||
rootQueryCurrentCachedRecordEdgesLengthIsAtLimit &&
|
||||
@@ -174,16 +168,16 @@ export const triggerUpdateRecordOptimisticEffect = ({
|
||||
}
|
||||
|
||||
const rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit =
|
||||
rootQueryNextCachedRecordEdges.length > rootQueryLimit;
|
||||
rootQueryNextEdges.length > rootQueryLimit;
|
||||
|
||||
if (rootQueryNextCachedRecordEdgesLengthIsAboveRootQueryLimit) {
|
||||
rootQueryNextCachedRecordEdges.splice(rootQueryLimit);
|
||||
rootQueryNextEdges.splice(rootQueryLimit);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...rootQueryCachedObjectRecordConnection,
|
||||
edges: rootQueryNextCachedRecordEdges,
|
||||
...rootQueryConnection,
|
||||
edges: rootQueryNextEdges,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effec
|
||||
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
|
||||
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
|
||||
@@ -74,6 +74,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
||||
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 =
|
||||
isObjectRecordConnection(
|
||||
targetObjectMetadataItem.nameSingular,
|
||||
@@ -104,12 +106,14 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
||||
isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0;
|
||||
|
||||
if (shouldDetachSourceFromAllTargets) {
|
||||
const shouldStartByDeletingRelationTargetRecordsFromCache =
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_OPTIMISTIC_RELATION_DETACH.includes(
|
||||
// TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
|
||||
// Instead of hardcoding it here
|
||||
const shouldCascadeDeleteTargetRecords =
|
||||
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
|
||||
targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
|
||||
);
|
||||
|
||||
if (shouldStartByDeletingRelationTargetRecordsFromCache) {
|
||||
if (shouldCascadeDeleteTargetRecords) {
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem: targetObjectMetadataItem,
|
||||
|
||||
@@ -2,4 +2,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
|
||||
export const CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH = [
|
||||
CoreObjectNameSingular.Favorite,
|
||||
CoreObjectNameSingular.ActivityTarget,
|
||||
CoreObjectNameSingular.Comment,
|
||||
];
|
||||
|
||||
@@ -192,11 +192,11 @@ export const CommandMenu = () => {
|
||||
|
||||
const activityCommands = useMemo(
|
||||
() =>
|
||||
activities.map(({ id, title }) => ({
|
||||
id,
|
||||
label: title ?? '',
|
||||
activities.map((activity) => ({
|
||||
id: activity.id,
|
||||
label: activity.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(id),
|
||||
onCommandClick: () => openActivityRightDrawer(activity),
|
||||
})),
|
||||
[activities, openActivityRightDrawer],
|
||||
);
|
||||
@@ -372,7 +372,7 @@ export const CommandMenu = () => {
|
||||
Icon={IconNotes}
|
||||
key={activity.id}
|
||||
label={activity.title ?? ''}
|
||||
onClick={() => openActivityRightDrawer(activity.id)}
|
||||
onClick={() => openActivityRightDrawer(activity)}
|
||||
/>
|
||||
</SelectableItem>
|
||||
))}
|
||||
|
||||
@@ -43,12 +43,19 @@ export const useGenerateObjectRecordOptimisticResponse = ({
|
||||
);
|
||||
const relationRecordId = result[relationIdFieldName] as string | null;
|
||||
|
||||
const relationRecord = input[fieldMetadataItem.name] as
|
||||
| ObjectRecord
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
...result,
|
||||
[fieldMetadataItem.name]: relationRecordId
|
||||
? {
|
||||
__typename: relationRecordTypeName,
|
||||
id: relationRecordId,
|
||||
// TODO: there are too many bugs if we don't include the entire relation record
|
||||
// See if we can find a way to work only with the id and typename
|
||||
...relationRecord,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { MAX_QUERY_DEPTH_FOR_CACHE_INJECTION } from '@/object-record/cache/constants/MaxQueryDepthForCacheInjection';
|
||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||
@@ -28,11 +27,11 @@ export const useUpsertFindManyRecordsQueryInCache = ({
|
||||
}) => {
|
||||
const findManyRecordsQueryForCacheOverwrite = generateFindManyRecordsQuery({
|
||||
objectMetadataItem,
|
||||
depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION,
|
||||
depth: MAX_QUERY_DEPTH_FOR_CACHE_INJECTION, // TODO: fix this
|
||||
});
|
||||
|
||||
const newObjectRecordConnection = getRecordConnectionFromRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
records: objectRecordsToOverwrite,
|
||||
});
|
||||
|
||||
|
||||
@@ -20,5 +20,6 @@ export const getRecordConnectionFromRecords = <T extends ObjectRecord>({
|
||||
});
|
||||
}),
|
||||
pageInfo: getEmptyPageInfo(),
|
||||
totalCount: records.length,
|
||||
} as ObjectRecordConnection<T>;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,10 @@ type useDeleteOneRecordProps = {
|
||||
refetchFindManyQuery?: boolean;
|
||||
};
|
||||
|
||||
type DeleteManyRecordsOptions = {
|
||||
skipOptimisticEffect?: boolean;
|
||||
};
|
||||
|
||||
export const useDeleteManyRecords = ({
|
||||
objectNameSingular,
|
||||
}: useDeleteOneRecordProps) => {
|
||||
@@ -26,34 +30,41 @@ export const useDeleteManyRecords = ({
|
||||
objectMetadataItem.namePlural,
|
||||
);
|
||||
|
||||
const deleteManyRecords = async (idsToDelete: string[]) => {
|
||||
const deleteManyRecords = async (
|
||||
idsToDelete: string[],
|
||||
options?: DeleteManyRecordsOptions,
|
||||
) => {
|
||||
const deletedRecords = await apolloClient.mutate({
|
||||
mutation: deleteManyRecordsMutation,
|
||||
variables: {
|
||||
filter: { id: { in: idsToDelete } },
|
||||
},
|
||||
optimisticResponse: {
|
||||
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDelete,
|
||||
})),
|
||||
},
|
||||
update: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
optimisticResponse: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: {
|
||||
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
|
||||
__typename: capitalize(objectNameSingular),
|
||||
id: idToDelete,
|
||||
})),
|
||||
},
|
||||
update: options?.skipOptimisticEffect
|
||||
? undefined
|
||||
: (cache, { data }) => {
|
||||
const records = data?.[mutationResponseField];
|
||||
|
||||
if (!records?.length) return;
|
||||
if (!records?.length) return;
|
||||
|
||||
const cachedRecords = records
|
||||
.map((record) => getRecordFromCache(record.id, cache))
|
||||
.filter(isDefined);
|
||||
const cachedRecords = records
|
||||
.map((record) => getRecordFromCache(record.id, cache))
|
||||
.filter(isDefined);
|
||||
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: cachedRecords,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
triggerDeleteRecordsOptimisticEffect({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
recordsToDelete: cachedRecords,
|
||||
objectMetadataItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return deletedRecords.data?.[mutationResponseField] ?? null;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
// TODO: fix connection in relation => automatically change to an array
|
||||
export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
||||
objectNameSingular,
|
||||
objectRecordId = '',
|
||||
|
||||
@@ -79,7 +79,8 @@ export type LeafFilter =
|
||||
| CurrencyFilter
|
||||
| URLFilter
|
||||
| FullNameFilter
|
||||
| BooleanFilter;
|
||||
| BooleanFilter
|
||||
| undefined;
|
||||
|
||||
export type AndObjectRecordFilter = {
|
||||
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';
|
||||
|
||||
export const useRightDrawer = () => {
|
||||
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
|
||||
const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
|
||||
isRightDrawerOpenState,
|
||||
);
|
||||
const [, setIsRightDrawerExpanded] = useRecoilState(
|
||||
isRightDrawerExpandedState,
|
||||
);
|
||||
|
||||
const [, setRightDrawerPage] = useRecoilState(rightDrawerPageState);
|
||||
const [rightDrawerPage, setRightDrawerPage] =
|
||||
useRecoilState(rightDrawerPageState);
|
||||
|
||||
const openRightDrawer = (rightDrawerPage: RightDrawerPages) => {
|
||||
setRightDrawerPage(rightDrawerPage);
|
||||
@@ -25,6 +28,8 @@ export const useRightDrawer = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
rightDrawerPage,
|
||||
isRightDrawerOpen,
|
||||
openRightDrawer,
|
||||
closeRightDrawer,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useOpenCreateActivityDrawerV2 } from '@/activities/hooks/useOpenCreateActivityDrawerV2';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityType } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
@@ -24,14 +24,14 @@ export const ShowPageAddButton = ({
|
||||
activityTargetObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { closeDropdown, toggleDropdown } = useDropdown('add-show-page');
|
||||
const openCreateActivity = useOpenCreateActivityDrawerV2();
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
const handleSelect = (type: ActivityType) => {
|
||||
openCreateActivity({
|
||||
type,
|
||||
targetableObjects: [activityTargetObject],
|
||||
timelineTargetableObject: activityTargetObject,
|
||||
});
|
||||
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const Tasks = () => {
|
||||
<RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}>
|
||||
<TasksEffect filterDropdownId={filterDropdownId} />
|
||||
<PageHeader title="Tasks" Icon={IconCheckbox}>
|
||||
<PageAddTaskButton filterDropdownId={filterDropdownId} />
|
||||
<PageAddTaskButton />
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<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