Improved activity editor re-renders (#4149)
* Refactor task count * Fixed show page rerender * Less rerenders and way better title and body UX * Finished breaking down activity editor subscriptions * Removed console.log * Last console.log * Fixed bugs and cleaned
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useBlockNote } from '@blocknote/react';
|
||||
import styled from '@emotion/styled';
|
||||
@ -9,6 +9,7 @@ import { useDebouncedCallback } from 'use-debounce';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
@ -37,17 +38,27 @@ const StyledBlockNoteStyledContainer = styled.div`
|
||||
`;
|
||||
|
||||
type ActivityBodyEditorProps = {
|
||||
activity: Activity;
|
||||
activityId: string;
|
||||
fillTitleFromBody: boolean;
|
||||
};
|
||||
|
||||
export const ActivityBodyEditor = ({
|
||||
activity,
|
||||
activityId,
|
||||
fillTitleFromBody,
|
||||
}: ActivityBodyEditorProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
const activity = activityInStore as Activity | null;
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activity.id,
|
||||
activityId: activityId,
|
||||
}),
|
||||
);
|
||||
|
||||
const [activityBody, setActivityBody] = useRecoilState(
|
||||
activityBodyFamilyState({
|
||||
activityId: activityId,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -67,27 +78,31 @@ export const ActivityBodyEditor = ({
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const persistBodyDebounced = useDebouncedCallback((newBody: string) => {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
body: newBody,
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const persistTitleAndBodyDebounced = useDebouncedCallback(
|
||||
(newTitle: string, newBody: string) => {
|
||||
if (activity) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: newTitle,
|
||||
body: newBody,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
|
||||
setActivityTitleHasBeenSet(true);
|
||||
const persistTitleAndBodyDebounced = useDebouncedCallback(
|
||||
(newTitle: string, newBody: string) => {
|
||||
if (activity) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: newTitle,
|
||||
body: newBody,
|
||||
},
|
||||
});
|
||||
|
||||
setActivityTitleHasBeenSet(true);
|
||||
}
|
||||
},
|
||||
500,
|
||||
200,
|
||||
);
|
||||
|
||||
const updateTitleAndBody = useCallback(
|
||||
@ -104,28 +119,6 @@ export const ActivityBodyEditor = ({
|
||||
canCreateActivityState,
|
||||
);
|
||||
|
||||
const handleBodyChange = useCallback(
|
||||
(activityBody: string) => {
|
||||
if (!canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
updateTitleAndBody(activityBody);
|
||||
} else {
|
||||
persistBodyDebounced(activityBody);
|
||||
}
|
||||
},
|
||||
[
|
||||
fillTitleFromBody,
|
||||
persistBodyDebounced,
|
||||
activityTitleHasBeenSet,
|
||||
updateTitleAndBody,
|
||||
setCanCreateActivity,
|
||||
canCreateActivity,
|
||||
],
|
||||
);
|
||||
|
||||
const slashMenuItems = getSlashMenu();
|
||||
|
||||
const [uploadFile] = useUploadFileMutation();
|
||||
@ -148,63 +141,105 @@ export const ActivityBodyEditor = ({
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
const editor: BlockNoteEditor<typeof blockSpecs> | null = useBlockNote({
|
||||
initialContent:
|
||||
isNonEmptyString(activity.body) && activity.body !== '{}'
|
||||
? JSON.parse(activity.body)
|
||||
: undefined,
|
||||
domAttributes: { editor: { class: 'editor' } },
|
||||
onEditorContentChange: useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(editor: BlockNoteEditor) => {
|
||||
const newStringifiedBody =
|
||||
JSON.stringify(editor.topLevelBlocks) ?? '';
|
||||
const handlePersistBody = useCallback(
|
||||
(activityBody: string) => {
|
||||
if (!canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
set(recordStoreFamilyState(activity.id), (oldActivity) => {
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
updateTitleAndBody(activityBody);
|
||||
} else {
|
||||
persistBodyDebounced(activityBody);
|
||||
}
|
||||
},
|
||||
[
|
||||
fillTitleFromBody,
|
||||
persistBodyDebounced,
|
||||
activityTitleHasBeenSet,
|
||||
updateTitleAndBody,
|
||||
setCanCreateActivity,
|
||||
canCreateActivity,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBodyChange = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(newStringifiedBody: string) => {
|
||||
set(recordStoreFamilyState(activityId), (oldActivity) => {
|
||||
return {
|
||||
...oldActivity,
|
||||
id: activityId,
|
||||
body: newStringifiedBody,
|
||||
};
|
||||
});
|
||||
|
||||
modifyActivityFromCache(activityId, {
|
||||
body: () => {
|
||||
return newStringifiedBody;
|
||||
},
|
||||
});
|
||||
|
||||
const activityTitleHasBeenSet = snapshot
|
||||
.getLoadable(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activityId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const blockBody = JSON.parse(newStringifiedBody);
|
||||
const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string;
|
||||
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
set(recordStoreFamilyState(activityId), (oldActivity) => {
|
||||
return {
|
||||
...oldActivity,
|
||||
id: activity.id,
|
||||
body: newStringifiedBody,
|
||||
id: activityId,
|
||||
title: newTitleFromBody,
|
||||
};
|
||||
});
|
||||
|
||||
modifyActivityFromCache(activity.id, {
|
||||
body: () => {
|
||||
return newStringifiedBody;
|
||||
modifyActivityFromCache(activityId, {
|
||||
title: () => {
|
||||
return newTitleFromBody;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const activityTitleHasBeenSet = snapshot
|
||||
.getLoadable(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activity.id,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
handlePersistBody(newStringifiedBody);
|
||||
},
|
||||
[activityId, fillTitleFromBody, modifyActivityFromCache, handlePersistBody],
|
||||
);
|
||||
|
||||
const blockBody = JSON.parse(newStringifiedBody);
|
||||
const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string;
|
||||
const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500);
|
||||
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
set(recordStoreFamilyState(activity.id), (oldActivity) => {
|
||||
return {
|
||||
...oldActivity,
|
||||
id: activity.id,
|
||||
title: newTitleFromBody,
|
||||
};
|
||||
});
|
||||
const handleEditorChange = (newEditor: BlockNoteEditor) => {
|
||||
const newStringifiedBody = JSON.stringify(newEditor.topLevelBlocks) ?? '';
|
||||
|
||||
modifyActivityFromCache(activity.id, {
|
||||
title: () => {
|
||||
return newTitleFromBody;
|
||||
},
|
||||
});
|
||||
}
|
||||
setActivityBody(newStringifiedBody);
|
||||
|
||||
handleBodyChange(newStringifiedBody);
|
||||
},
|
||||
[activity, fillTitleFromBody, modifyActivityFromCache, handleBodyChange],
|
||||
),
|
||||
handleBodyChangeDebounced(newStringifiedBody);
|
||||
};
|
||||
|
||||
const initialBody = useMemo(() => {
|
||||
if (isNonEmptyString(activityBody) && activityBody !== '{}') {
|
||||
return JSON.parse(activityBody);
|
||||
} else if (
|
||||
activity &&
|
||||
isNonEmptyString(activity.body) &&
|
||||
activity?.body !== '{}'
|
||||
) {
|
||||
return JSON.parse(activity.body);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [activity, activityBody]);
|
||||
|
||||
const editor: BlockNoteEditor<typeof blockSpecs> | null = useBlockNote({
|
||||
initialContent: initialBody,
|
||||
domAttributes: { editor: { class: 'editor' } },
|
||||
onEditorContentChange: handleEditorChange,
|
||||
slashMenuItems,
|
||||
blockSpecs: blockSpecs,
|
||||
uploadFile: handleUploadAttachment,
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
|
||||
export const ActivityBodyEffect = ({ activityId }: { activityId: string }) => {
|
||||
const [activityFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const [activityBody, setActivityBody] = useRecoilState(
|
||||
activityBodyFamilyState({ activityId }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
activityBody === '' &&
|
||||
activityFromStore &&
|
||||
activityBody !== activityFromStore.body
|
||||
) {
|
||||
setActivityBody(activityFromStore.body);
|
||||
}
|
||||
}, [activityFromStore, activityBody, setActivityBody]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { Comment } from '@/activities/comment/Comment';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { Comment as CommentType } from '@/activities/types/Comment';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
@ -50,12 +49,12 @@ const StyledThreadCommentTitle = styled.div`
|
||||
`;
|
||||
|
||||
type ActivityCommentsProps = {
|
||||
activity: Pick<Activity, 'id'>;
|
||||
activityId: string;
|
||||
scrollableContainerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const ActivityComments = ({
|
||||
activity,
|
||||
activityId,
|
||||
scrollableContainerRef,
|
||||
}: ActivityCommentsProps) => {
|
||||
const { createOneRecord: createOneComment } = useCreateOneRecord({
|
||||
@ -66,10 +65,10 @@ export const ActivityComments = ({
|
||||
|
||||
const { records: comments } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Comment,
|
||||
skip: !isNonEmptyString(activity?.id),
|
||||
skip: !isNonEmptyString(activityId),
|
||||
filter: {
|
||||
activityId: {
|
||||
eq: activity?.id ?? '',
|
||||
eq: activityId,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -87,7 +86,7 @@ export const ActivityComments = ({
|
||||
id: v4(),
|
||||
authorId: currentWorkspaceMember?.id ?? '',
|
||||
author: currentWorkspaceMember,
|
||||
activityId: activity?.id ?? '',
|
||||
activityId: activityId,
|
||||
body: commentText,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
@ -1,28 +1,12 @@
|
||||
import { useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
|
||||
import { ActivityBodyEffect } from '@/activities/components/ActivityBodyEffect';
|
||||
import { ActivityComments } from '@/activities/components/ActivityComments';
|
||||
import { ActivityEditorFields } from '@/activities/components/ActivityEditorFields';
|
||||
import { ActivityTitleEffect } from '@/activities/components/ActivityTitleEffect';
|
||||
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
|
||||
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
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';
|
||||
import {
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} 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';
|
||||
|
||||
import { ActivityTitle } from './ActivityTitle';
|
||||
@ -60,152 +44,36 @@ const StyledTopContainer = styled.div`
|
||||
`;
|
||||
|
||||
type ActivityEditorProps = {
|
||||
activity: Activity;
|
||||
activityId: string;
|
||||
showComment?: boolean;
|
||||
fillTitleFromBody?: boolean;
|
||||
};
|
||||
|
||||
export const ActivityEditor = ({
|
||||
activity,
|
||||
activityId,
|
||||
showComment = true,
|
||||
fillTitleFromBody = false,
|
||||
}: ActivityEditorProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
|
||||
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
|
||||
);
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
const { deleteActivityFromCache } = useDeleteActivityFromCache();
|
||||
|
||||
const useUpsertOneActivityMutation: RecordUpdateHook = () => {
|
||||
const upsertActivityMutation = async ({
|
||||
variables,
|
||||
}: RecordUpdateHookParams) => {
|
||||
await upsertActivity({ activity, input: variables.updateOneRecordInput });
|
||||
};
|
||||
|
||||
return [upsertActivityMutation, { loading: false }];
|
||||
};
|
||||
|
||||
const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activity.id,
|
||||
fieldMetadataName: 'dueAt',
|
||||
fieldPosition: 0,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: AssigneeFieldContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activity.id,
|
||||
fieldMetadataName: 'assignee',
|
||||
fieldPosition: 1,
|
||||
clearable: true,
|
||||
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,
|
||||
objectRecordId: activity?.id ?? '',
|
||||
fieldMetadataName: 'activityTargets',
|
||||
fieldPosition: 2,
|
||||
});
|
||||
|
||||
useRegisterClickOutsideListenerCallback({
|
||||
callbackId: 'activity-editor',
|
||||
callbackFunction: () => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!activity) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
<StyledUpperPartContainer>
|
||||
<StyledTopContainer>
|
||||
<ActivityTypeDropdown activity={activity} />
|
||||
<ActivityTitle activity={activity} />
|
||||
<PropertyBox>
|
||||
{activity.type === 'Task' &&
|
||||
DueAtFieldContextProvider &&
|
||||
AssigneeFieldContextProvider && (
|
||||
<>
|
||||
<DueAtFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</DueAtFieldContextProvider>
|
||||
<AssigneeFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</AssigneeFieldContextProvider>
|
||||
</>
|
||||
)}
|
||||
{ActivityTargetsContextProvider && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell activity={activity} />
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
</PropertyBox>
|
||||
<ActivityTypeDropdown activityId={activityId} />
|
||||
<ActivityTitleEffect activityId={activityId} />
|
||||
<ActivityTitle activityId={activityId} />
|
||||
<ActivityEditorFields activityId={activityId} />
|
||||
</StyledTopContainer>
|
||||
</StyledUpperPartContainer>
|
||||
<ActivityBodyEffect activityId={activityId} />
|
||||
<ActivityBodyEditor
|
||||
activity={activity}
|
||||
activityId={activityId}
|
||||
fillTitleFromBody={fillTitleFromBody}
|
||||
/>
|
||||
{showComment && (
|
||||
<ActivityComments
|
||||
activity={activity}
|
||||
activityId={activityId}
|
||||
scrollableContainerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
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 { 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';
|
||||
|
||||
export const ActivityEditorEffect = ({
|
||||
activityId,
|
||||
}: {
|
||||
activityId: string;
|
||||
}) => {
|
||||
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
|
||||
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
|
||||
);
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
const { deleteActivityFromCache } = useDeleteActivityFromCache();
|
||||
|
||||
const upsertActivityCallback = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const isUpsertingActivityInDB = snapshot
|
||||
.getLoadable(isUpsertingActivityInDBState)
|
||||
.getValue();
|
||||
|
||||
const canCreateActivity = snapshot
|
||||
.getLoadable(canCreateActivityState)
|
||||
.getValue();
|
||||
|
||||
const isActivityInCreateMode = snapshot
|
||||
.getLoadable(isActivityInCreateModeState)
|
||||
.getValue();
|
||||
|
||||
const activityFromStore = snapshot
|
||||
.getLoadable(recordStoreFamilyState(activityId))
|
||||
.getValue();
|
||||
|
||||
const activity = activityFromStore as Activity | null;
|
||||
|
||||
const activityTitle = snapshot
|
||||
.getLoadable(activityTitleFamilyState({ activityId }))
|
||||
.getValue();
|
||||
|
||||
const activityBody = snapshot
|
||||
.getLoadable(activityBodyFamilyState({ activityId }))
|
||||
.getValue();
|
||||
|
||||
if (isUpsertingActivityInDB || !activityFromStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActivityInCreateMode && activity) {
|
||||
if (canCreateActivity) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: activityFromStore.title,
|
||||
body: activityFromStore.body,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
deleteActivityFromCache(activity);
|
||||
}
|
||||
|
||||
set(isActivityInCreateModeState, false);
|
||||
} else if (activity) {
|
||||
if (
|
||||
activity.title !== activityTitle ||
|
||||
activity.body !== activityBody
|
||||
) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: activityTitle,
|
||||
body: activityBody,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[activityId, deleteActivityFromCache, upsertActivity],
|
||||
);
|
||||
|
||||
useRegisterClickOutsideListenerCallback({
|
||||
callbackId: 'activity-editor',
|
||||
callbackFunction: upsertActivityCallback,
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,92 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import {
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} 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';
|
||||
|
||||
export const ActivityEditorFields = ({
|
||||
activityId,
|
||||
}: {
|
||||
activityId: string;
|
||||
}) => {
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const [activityFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const activity = activityFromStore as Activity;
|
||||
|
||||
const useUpsertOneActivityMutation: RecordUpdateHook = () => {
|
||||
const upsertActivityMutation = async ({
|
||||
variables,
|
||||
}: RecordUpdateHookParams) => {
|
||||
if (activityFromStore) {
|
||||
await upsertActivity({
|
||||
activity: activityFromStore as Activity,
|
||||
input: variables.updateOneRecordInput,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return [upsertActivityMutation, { loading: false }];
|
||||
};
|
||||
|
||||
const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'dueAt',
|
||||
fieldPosition: 0,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: AssigneeFieldContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'assignee',
|
||||
fieldPosition: 1,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'activityTargets',
|
||||
fieldPosition: 2,
|
||||
});
|
||||
|
||||
return (
|
||||
<PropertyBox>
|
||||
{activity.type === 'Task' &&
|
||||
DueAtFieldContextProvider &&
|
||||
AssigneeFieldContextProvider && (
|
||||
<>
|
||||
<DueAtFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</DueAtFieldContextProvider>
|
||||
<AssigneeFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</AssigneeFieldContextProvider>
|
||||
</>
|
||||
)}
|
||||
{ActivityTargetsContextProvider && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell activity={activity} />
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
</PropertyBox>
|
||||
);
|
||||
};
|
||||
@ -6,6 +6,7 @@ import { Key } from 'ts-key-enum';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
@ -55,14 +56,20 @@ const StyledContainer = styled.div`
|
||||
`;
|
||||
|
||||
type ActivityTitleProps = {
|
||||
activity: Activity;
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
export const ActivityTitle = ({ activityId }: ActivityTitleProps) => {
|
||||
const [activityInStore, setActivityInStore] = useRecoilState(
|
||||
recordStoreFamilyState(activity.id),
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const [activityTitle, setActivityTitle] = useRecoilState(
|
||||
activityTitleFamilyState({ activityId }),
|
||||
);
|
||||
|
||||
const activity = activityInStore as Activity;
|
||||
|
||||
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
|
||||
canCreateActivityState,
|
||||
);
|
||||
@ -96,7 +103,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activity.id,
|
||||
activityId: activityId,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -122,7 +129,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
const setTitleDebounced = useDebouncedCallback((newTitle: string) => {
|
||||
setActivityInStore((currentActivity) => {
|
||||
return {
|
||||
...currentActivity,
|
||||
@ -140,6 +147,12 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
return newTitle;
|
||||
},
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setActivityTitle(newTitle);
|
||||
|
||||
setTitleDebounced(newTitle);
|
||||
|
||||
persistTitleDebounced(newTitle);
|
||||
};
|
||||
@ -171,7 +184,7 @@ export const ActivityTitle = ({ activity }: ActivityTitleProps) => {
|
||||
ref={titleInputRef}
|
||||
placeholder={`${activity.type} title`}
|
||||
onChange={(event) => handleTitleChange(event.target.value)}
|
||||
value={activityInStore?.title ?? ''}
|
||||
value={activityTitle}
|
||||
completed={completed}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
|
||||
export const ActivityTitleEffect = ({ activityId }: { activityId: string }) => {
|
||||
const [activityFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const [activityTitle, setActivityTitle] = useRecoilState(
|
||||
activityTitleFamilyState({ activityId }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
activityTitle === '' &&
|
||||
activityFromStore &&
|
||||
activityTitle !== activityFromStore.title
|
||||
) {
|
||||
setActivityTitle(activityFromStore.title);
|
||||
}
|
||||
}, [activityFromStore, activityTitle, setActivityTitle]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import {
|
||||
Chip,
|
||||
ChipAccent,
|
||||
@ -10,18 +11,21 @@ import {
|
||||
import { IconCheckbox, IconNotes } from '@/ui/display/icon';
|
||||
|
||||
type ActivityTypeDropdownProps = {
|
||||
activity: Pick<Activity, 'type'>;
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const ActivityTypeDropdown = ({
|
||||
activity,
|
||||
activityId,
|
||||
}: ActivityTypeDropdownProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={activity.type}
|
||||
label={activityInStore?.type}
|
||||
leftComponent={
|
||||
activity.type === 'Note' ? (
|
||||
activityInStore?.type === 'Note' ? (
|
||||
<IconNotes size={theme.icon.size.md} />
|
||||
) : (
|
||||
<IconCheckbox size={theme.icon.size.md} />
|
||||
|
||||
@ -90,7 +90,7 @@ export const useActivities = ({
|
||||
const loading = loadingActivities || loadingActivityTargets;
|
||||
|
||||
// TODO: fix connection in relation => automatically change to an array
|
||||
const activities = activitiesWithConnection
|
||||
const activities: Activity[] = activitiesWithConnection
|
||||
?.map(makeActivityWithoutConnection as any)
|
||||
.map(({ activity }: any) => activity);
|
||||
|
||||
|
||||
@ -10,7 +10,10 @@ export const useActivityTargetsForTargetableObjects = ({
|
||||
targetableObjects,
|
||||
skip,
|
||||
}: {
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
targetableObjects: Pick<
|
||||
ActivityTargetableObject,
|
||||
'id' | 'targetObjectNameSingular'
|
||||
>[];
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const activityTargetsFilter = getActivityTargetsFilter({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useAttachRelationInBothDirections } from '@/activities/hooks/useAttachRelationInBothDirections';
|
||||
@ -6,13 +6,15 @@ import { useInjectIntoActivityTargetInlineCellCache } from '@/activities/inline-
|
||||
import { Activity, ActivityType } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects';
|
||||
import { makeActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateManyRecordsInCache } from '@/object-record/hooks/useCreateManyRecordsInCache';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/hooks/useCreateOneRecordInCache';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useCreateActivityInCache = () => {
|
||||
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
|
||||
@ -39,58 +41,86 @@ export const useCreateActivityInCache = () => {
|
||||
const { attachRelationInBothDirections } =
|
||||
useAttachRelationInBothDirections();
|
||||
|
||||
const createActivityInCache = ({
|
||||
type,
|
||||
targetableObjects,
|
||||
customAssignee,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
customAssignee?: WorkspaceMember;
|
||||
}) => {
|
||||
const activityId = v4();
|
||||
|
||||
const createdActivityInCache = createOneActivityInCache({
|
||||
id: activityId,
|
||||
author: currentWorkspaceMemberRecord,
|
||||
authorId: currentWorkspaceMemberRecord?.id,
|
||||
assignee: customAssignee ?? currentWorkspaceMemberRecord,
|
||||
assigneeId: customAssignee?.id ?? currentWorkspaceMemberRecord?.id,
|
||||
type,
|
||||
});
|
||||
|
||||
const activityTargetsToCreate =
|
||||
getActivityTargetsToCreateFromTargetableObjects({
|
||||
activityId,
|
||||
const createActivityInCache = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
type,
|
||||
targetableObjects,
|
||||
});
|
||||
customAssignee,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
customAssignee?: WorkspaceMember;
|
||||
}) => {
|
||||
const activityId = v4();
|
||||
|
||||
const createdActivityTargetsInCache = createManyActivityTargetsInCache(
|
||||
activityTargetsToCreate,
|
||||
);
|
||||
const createdActivityInCache = createOneActivityInCache({
|
||||
id: activityId,
|
||||
author: currentWorkspaceMemberRecord,
|
||||
authorId: currentWorkspaceMemberRecord?.id,
|
||||
assignee: customAssignee ?? currentWorkspaceMemberRecord,
|
||||
assigneeId: customAssignee?.id ?? currentWorkspaceMemberRecord?.id,
|
||||
type,
|
||||
});
|
||||
|
||||
injectIntoActivityTargetInlineCellCache({
|
||||
activityId,
|
||||
activityTargetsToInject: createdActivityTargetsInCache,
|
||||
});
|
||||
const targetObjectRecords = targetableObjects
|
||||
.map((targetableObject) => {
|
||||
const targetObject = snapshot
|
||||
.getLoadable(recordStoreFamilyState(targetableObject.id))
|
||||
.getValue();
|
||||
|
||||
attachRelationInBothDirections({
|
||||
sourceRecord: createdActivityInCache,
|
||||
fieldNameOnSourceRecord: 'activityTargets',
|
||||
sourceObjectNameSingular: CoreObjectNameSingular.Activity,
|
||||
fieldNameOnTargetRecord: 'activity',
|
||||
targetObjectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
targetRecords: createdActivityTargetsInCache,
|
||||
});
|
||||
return targetObject;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
createdActivityInCache: {
|
||||
...createdActivityInCache,
|
||||
activityTargets: createdActivityTargetsInCache,
|
||||
const activityTargetsToCreate =
|
||||
makeActivityTargetsToCreateFromTargetableObjects({
|
||||
activityId,
|
||||
targetableObjects,
|
||||
targetObjectRecords,
|
||||
});
|
||||
|
||||
const createdActivityTargetsInCache = createManyActivityTargetsInCache(
|
||||
activityTargetsToCreate,
|
||||
);
|
||||
|
||||
injectIntoActivityTargetInlineCellCache({
|
||||
activityId,
|
||||
activityTargetsToInject: createdActivityTargetsInCache,
|
||||
});
|
||||
|
||||
attachRelationInBothDirections({
|
||||
sourceRecord: createdActivityInCache,
|
||||
fieldNameOnSourceRecord: 'activityTargets',
|
||||
sourceObjectNameSingular: CoreObjectNameSingular.Activity,
|
||||
fieldNameOnTargetRecord: 'activity',
|
||||
targetObjectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
targetRecords: createdActivityTargetsInCache,
|
||||
});
|
||||
|
||||
// TODO: should refactor when refactoring make activity connection utils
|
||||
set(recordStoreFamilyState(activityId), {
|
||||
...createdActivityInCache,
|
||||
activityTargets: createdActivityTargetsInCache,
|
||||
comments: [],
|
||||
});
|
||||
|
||||
return {
|
||||
createdActivityInCache: {
|
||||
...createdActivityInCache,
|
||||
activityTargets: createdActivityTargetsInCache,
|
||||
},
|
||||
createdActivityTargetsInCache,
|
||||
};
|
||||
},
|
||||
createdActivityTargetsInCache,
|
||||
};
|
||||
};
|
||||
[
|
||||
attachRelationInBothDirections,
|
||||
createManyActivityTargetsInCache,
|
||||
createOneActivityInCache,
|
||||
currentWorkspaceMemberRecord,
|
||||
injectIntoActivityTargetInlineCellCache,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
createActivityInCache,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
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';
|
||||
@ -15,21 +14,21 @@ export const useOpenActivityRightDrawer = () => {
|
||||
const [viewableActivityId, setViewableActivityId] = useRecoilState(
|
||||
viewableActivityIdState,
|
||||
);
|
||||
const [, setActivityInDrawer] = useRecoilState(activityInDrawerState);
|
||||
const [, setActivityIdInDrawer] = useRecoilState(activityIdInDrawerState);
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return (activity: Activity) => {
|
||||
return (activityId: string) => {
|
||||
if (
|
||||
isRightDrawerOpen &&
|
||||
rightDrawerPage === RightDrawerPages.EditActivity &&
|
||||
viewableActivityId === activity.id
|
||||
viewableActivityId === activityId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
setViewableActivityId(activity.id);
|
||||
setActivityInDrawer(activity);
|
||||
setViewableActivityId(activityId);
|
||||
setActivityIdInDrawer(activityId);
|
||||
openRightDrawer(RightDrawerPages.EditActivity);
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
|
||||
import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
@ -34,7 +34,7 @@ export const useOpenCreateActivityDrawer = () => {
|
||||
temporaryActivityForEditorState,
|
||||
);
|
||||
|
||||
const setActivityInDrawer = useSetRecoilState(activityInDrawerState);
|
||||
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
|
||||
|
||||
const [, setIsUpsertingActivityInDB] = useRecoilState(
|
||||
isUpsertingActivityInDBState,
|
||||
@ -55,7 +55,7 @@ export const useOpenCreateActivityDrawer = () => {
|
||||
customAssignee,
|
||||
});
|
||||
|
||||
setActivityInDrawer(createdActivityInCache);
|
||||
setActivityIdInDrawer(createdActivityInCache.id);
|
||||
setTemporaryActivityForEditor(createdActivityInCache);
|
||||
setIsCreatingActivity(true);
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
|
||||
@ -6,13 +6,13 @@ import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB'
|
||||
import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries';
|
||||
import { useInjectIntoActivityTargetsQueries } from '@/activities/hooks/useInjectIntoActivityTargetsQueries';
|
||||
import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState';
|
||||
import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
|
||||
import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState';
|
||||
import { useInjectIntoTimelineActivitiesQueries } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
@ -34,7 +34,7 @@ export const useUpsertActivity = () => {
|
||||
isUpsertingActivityInDBState,
|
||||
);
|
||||
|
||||
const setActivityInDrawer = useSetRecoilState(activityInDrawerState);
|
||||
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
|
||||
|
||||
const objectShowPageTargetableObject = useRecoilValue(
|
||||
objectShowPageTargetableObjectState,
|
||||
@ -169,7 +169,7 @@ export const useUpsertActivity = () => {
|
||||
|
||||
await createActivityInDB(activityToCreate);
|
||||
|
||||
setActivityInDrawer(activityToCreate);
|
||||
setActivityIdInDrawer(activityToCreate.id);
|
||||
|
||||
setIsActivityInCreateMode(false);
|
||||
} else {
|
||||
|
||||
@ -94,7 +94,7 @@ export const NoteCard = ({
|
||||
<FieldContext.Provider value={fieldContext as GenericFieldContextType}>
|
||||
<StyledCard isSingleNote={isSingleNote}>
|
||||
<StyledCardDetailsContainer
|
||||
onClick={() => openActivityRightDrawer(note)}
|
||||
onClick={() => openActivityRightDrawer(note.id)}
|
||||
>
|
||||
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
|
||||
<StyledCardContent>{body}</StyledCardContent>
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useDeleteActivityFromCache } from '@/activities/hooks/useDeleteActivityFromCache';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { useRemoveFromActivitiesQueries } from '@/activities/hooks/useRemoveFromActivitiesQueries';
|
||||
import { useRemoveFromActivityTargetsQueries } from '@/activities/hooks/useRemoveFromActivityTargetsQueries';
|
||||
import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState';
|
||||
import { activityInDrawerState } from '@/activities/states/activityInDrawerState';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
@ -17,11 +17,13 @@ import { viewableActivityIdState } from '@/activities/states/viewableActivityIdS
|
||||
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
|
||||
import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
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 { getChildRelationArray } from '@/object-record/utils/getChildRelationArray';
|
||||
import { mapToRecordId } from '@/object-record/utils/mapToObjectId';
|
||||
import { IconPlus, IconTrash } from '@/ui/display/icon';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
@ -35,7 +37,7 @@ const StyledButtonContainer = styled.div`
|
||||
|
||||
export const ActivityActionBar = () => {
|
||||
const viewableActivityId = useRecoilValue(viewableActivityIdState);
|
||||
const activityInDrawer = useRecoilValue(activityInDrawerState);
|
||||
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
|
||||
|
||||
const activityTargetableEntityArray = useRecoilValue(
|
||||
activityTargetableEntityArrayState,
|
||||
@ -60,9 +62,11 @@ export const ActivityActionBar = () => {
|
||||
const [isUpsertingActivityInDB] = useRecoilState(
|
||||
isUpsertingActivityInDBState,
|
||||
);
|
||||
|
||||
const objectShowPageTargetableObject = useRecoilValue(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
const currentCompletedTaskQueryVariables = useRecoilValue(
|
||||
@ -85,90 +89,130 @@ export const ActivityActionBar = () => {
|
||||
const weAreOnObjectShowPage = pathname.startsWith('/object');
|
||||
const weAreOnTaskPage = pathname.startsWith('/tasks');
|
||||
|
||||
const deleteActivity = async () => {
|
||||
setIsRightDrawerOpen(false);
|
||||
|
||||
if (viewableActivityId) {
|
||||
if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) {
|
||||
deleteActivityFromCache(temporaryActivityForEditor);
|
||||
setTemporaryActivityForEditor(null);
|
||||
} else {
|
||||
if (activityInDrawer) {
|
||||
const activityTargetIdsToDelete =
|
||||
activityInDrawer?.activityTargets.map(mapToRecordId) ?? [];
|
||||
|
||||
if (weAreOnTaskPage) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [],
|
||||
activitiesFilters: currentCompletedTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentCompletedTaskQueryVariables?.orderBy,
|
||||
});
|
||||
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [],
|
||||
activitiesFilters: currentIncompleteTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentIncompleteTaskQueryVariables?.orderBy,
|
||||
});
|
||||
} else if (
|
||||
weAreOnObjectShowPage &&
|
||||
isDefined(objectShowPageTargetableObject)
|
||||
) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables:
|
||||
FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
});
|
||||
|
||||
if (isDefined(currentCompletedTaskQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: currentCompletedTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentCompletedTaskQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(currentIncompleteTaskQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: currentIncompleteTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentIncompleteTaskQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(currentNotesQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: currentNotesQueryVariables?.filter,
|
||||
activitiesOrderByVariables: currentNotesQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
removeFromActivityTargetsQueries({
|
||||
activityTargetsToRemove: activityInDrawer?.activityTargets ?? [],
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
});
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(activityTargetIdsToDelete)) {
|
||||
await deleteManyActivityTargets(activityTargetIdsToDelete);
|
||||
}
|
||||
|
||||
await deleteOneActivity?.(viewableActivityId);
|
||||
const deleteActivity = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
if (!activityIdInDrawer) {
|
||||
throw new Error(
|
||||
'activityIdInDrawer is not defined, this should not happen',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const activity = snapshot
|
||||
.getLoadable(recordStoreFamilyState(activityIdInDrawer))
|
||||
.getValue() as Activity;
|
||||
|
||||
const activityTargets = getChildRelationArray({
|
||||
childRelation: activity.activityTargets,
|
||||
});
|
||||
|
||||
setIsRightDrawerOpen(false);
|
||||
|
||||
if (viewableActivityId) {
|
||||
if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) {
|
||||
deleteActivityFromCache(temporaryActivityForEditor);
|
||||
setTemporaryActivityForEditor(null);
|
||||
} else {
|
||||
if (activityIdInDrawer) {
|
||||
const activityTargetIdsToDelete: string[] =
|
||||
activityTargets.map(mapToRecordId) ?? [];
|
||||
|
||||
if (weAreOnTaskPage) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [],
|
||||
activitiesFilters: currentCompletedTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentCompletedTaskQueryVariables?.orderBy,
|
||||
});
|
||||
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [],
|
||||
activitiesFilters:
|
||||
currentIncompleteTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentIncompleteTaskQueryVariables?.orderBy,
|
||||
});
|
||||
} else if (
|
||||
weAreOnObjectShowPage &&
|
||||
isDefined(objectShowPageTargetableObject)
|
||||
) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables:
|
||||
FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
});
|
||||
|
||||
if (isDefined(currentCompletedTaskQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters:
|
||||
currentCompletedTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentCompletedTaskQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(currentIncompleteTaskQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters:
|
||||
currentIncompleteTaskQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentIncompleteTaskQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(currentNotesQueryVariables)) {
|
||||
removeFromActivitiesQueries({
|
||||
activityIdToRemove: viewableActivityId,
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
activitiesFilters: currentNotesQueryVariables?.filter,
|
||||
activitiesOrderByVariables:
|
||||
currentNotesQueryVariables?.orderBy,
|
||||
});
|
||||
}
|
||||
|
||||
removeFromActivityTargetsQueries({
|
||||
activityTargetsToRemove: activity?.activityTargets ?? [],
|
||||
targetableObjects: [objectShowPageTargetableObject],
|
||||
});
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(activityTargetIdsToDelete)) {
|
||||
await deleteManyActivityTargets(activityTargetIdsToDelete);
|
||||
}
|
||||
|
||||
await deleteOneActivity?.(viewableActivityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
activityIdInDrawer,
|
||||
currentCompletedTaskQueryVariables,
|
||||
currentIncompleteTaskQueryVariables,
|
||||
currentNotesQueryVariables,
|
||||
deleteActivityFromCache,
|
||||
deleteManyActivityTargets,
|
||||
deleteOneActivity,
|
||||
isActivityInCreateMode,
|
||||
objectShowPageTargetableObject,
|
||||
removeFromActivitiesQueries,
|
||||
removeFromActivityTargetsQueries,
|
||||
setTemporaryActivityForEditor,
|
||||
temporaryActivityForEditor,
|
||||
viewableActivityId,
|
||||
weAreOnObjectShowPage,
|
||||
weAreOnTaskPage,
|
||||
setIsRightDrawerOpen,
|
||||
],
|
||||
);
|
||||
|
||||
const record = useRecoilValue(
|
||||
recordStoreFamilyState(viewableActivityId ?? ''),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityEditor } from '@/activities/components/ActivityEditor';
|
||||
import { useActivityById } from '@/activities/hooks/useActivityById';
|
||||
import { ActivityEditorEffect } from '@/activities/components/ActivityEditorEffect';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
@ -24,18 +24,11 @@ export const RightDrawerActivity = ({
|
||||
showComment = true,
|
||||
fillTitleFromBody = false,
|
||||
}: RightDrawerActivityProps) => {
|
||||
const { activity, loading } = useActivityById({
|
||||
activityId,
|
||||
});
|
||||
|
||||
if (!activity || loading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ActivityEditorEffect activityId={activityId} />
|
||||
<ActivityEditor
|
||||
activity={activity}
|
||||
activityId={activityId}
|
||||
showComment={showComment}
|
||||
fillTitleFromBody={fillTitleFromBody}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const activityBodyFamilyState = atomFamily<
|
||||
string,
|
||||
{ activityId: string }
|
||||
>({
|
||||
key: 'activityBodyFamilyState',
|
||||
default: '',
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const activityIdInDrawerState = atom<string | null>({
|
||||
key: 'activityIdInDrawerState',
|
||||
default: null,
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
|
||||
export const activityInDrawerState = atom<Activity | null>({
|
||||
key: 'activityInDrawerState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const activityTitleFamilyState = atomFamily<
|
||||
string,
|
||||
{ activityId: string }
|
||||
>({
|
||||
key: 'activityTitleFamilyState',
|
||||
default: '',
|
||||
});
|
||||
@ -0,0 +1,47 @@
|
||||
import { useEffect } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentUserDueTaskCountState } from '@/activities/tasks/states/currentUserTaskCountState';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
export const CurrentUserDueTaskCountEffect = () => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const [currentUserDueTaskCount, setCurrentUserDueTaskCount] = useRecoilState(
|
||||
currentUserDueTaskCountState,
|
||||
);
|
||||
|
||||
const { records: tasks } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
filter: {
|
||||
type: { eq: 'Task' },
|
||||
completedAt: { is: 'NULL' },
|
||||
assigneeId: { eq: currentWorkspaceMember?.id },
|
||||
},
|
||||
});
|
||||
|
||||
const computedCurrentUserDueTaskCount = tasks.filter((task) => {
|
||||
if (!task.dueAt) {
|
||||
return false;
|
||||
}
|
||||
const dueDate = parseDate(task.dueAt).toJSDate();
|
||||
const today = DateTime.now().endOf('day').toJSDate();
|
||||
return dueDate <= today;
|
||||
}).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUserDueTaskCount !== computedCurrentUserDueTaskCount) {
|
||||
setCurrentUserDueTaskCount(computedCurrentUserDueTaskCount);
|
||||
}
|
||||
}, [
|
||||
computedCurrentUserDueTaskCount,
|
||||
currentUserDueTaskCount,
|
||||
setCurrentUserDueTaskCount,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -85,7 +85,7 @@ export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
return (
|
||||
<StyledContainer
|
||||
onClick={() => {
|
||||
openActivityRightDrawer(task);
|
||||
openActivityRightDrawer(task.id);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const currentUserDueTaskCountState = atom<number>({
|
||||
default: 0,
|
||||
key: 'currentUserDueTaskCountState',
|
||||
});
|
||||
@ -1,11 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState';
|
||||
import { timelineActivitiesNetworkingState } from '@/activities/timeline/states/timelineActivitiesNetworkingState';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||
import {
|
||||
@ -15,7 +12,6 @@ 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';
|
||||
|
||||
@ -36,21 +32,10 @@ export const Timeline = ({
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { activities, initialized, noActivities } = useActivities({
|
||||
targetableObjects: [targetableObject],
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
skip: !isDefined(targetableObject),
|
||||
});
|
||||
|
||||
const setTimelineTargetableObject = useSetRecoilState(
|
||||
objectShowPageTargetableObjectState,
|
||||
const { initialized, noActivities } = useRecoilValue(
|
||||
timelineActivitiesNetworkingState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimelineTargetableObject(targetableObject);
|
||||
}, [targetableObject, setTimelineTargetableObject]);
|
||||
|
||||
const showEmptyState = noActivities;
|
||||
|
||||
const showLoadingState = !initialized;
|
||||
@ -79,7 +64,7 @@ export const Timeline = ({
|
||||
|
||||
return (
|
||||
<StyledMainContainer>
|
||||
<TimelineItemsContainer activities={activities} />
|
||||
<TimelineItemsContainer />
|
||||
</StyledMainContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityFirstLevelFamilySelector';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { IconCheckbox, IconNotes } from '@/ui/display/icon';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@ -136,28 +136,42 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
|
||||
`;
|
||||
|
||||
type TimelineActivityProps = {
|
||||
activity: Activity;
|
||||
isLastActivity?: boolean;
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const TimelineActivity = ({
|
||||
activity,
|
||||
isLastActivity,
|
||||
activityId,
|
||||
}: TimelineActivityProps) => {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt);
|
||||
const exactCreatedAt = beautifyExactDateTime(activity.createdAt);
|
||||
const activityForTimeline = useRecoilValue(
|
||||
timelineActivityWithoutTargetsFamilyState(activityId),
|
||||
);
|
||||
|
||||
const beautifiedCreatedAt = activityForTimeline
|
||||
? beautifyPastDateRelativeToNow(activityForTimeline.createdAt)
|
||||
: '';
|
||||
const exactCreatedAt = activityForTimeline
|
||||
? beautifyExactDateTime(activityForTimeline.createdAt)
|
||||
: '';
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
const theme = useTheme();
|
||||
|
||||
const activityFromStore = useRecoilValue(recordStoreFamilyState(activity.id));
|
||||
const activityFromStore = useRecoilValue(
|
||||
recordStoreFamilyState(activityForTimeline?.id ?? ''),
|
||||
);
|
||||
|
||||
if (!activityForTimeline) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledAvatarContainer>
|
||||
<Avatar
|
||||
avatarUrl={activity.author?.avatarUrl}
|
||||
placeholder={activity.author?.name.firstName ?? ''}
|
||||
avatarUrl={activityForTimeline.author?.avatarUrl}
|
||||
placeholder={activityForTimeline.author?.name.firstName ?? ''}
|
||||
size="sm"
|
||||
type="rounded"
|
||||
/>
|
||||
@ -166,23 +180,26 @@ export const TimelineActivity = ({
|
||||
<StyledItemTitleContainer>
|
||||
<StyledItemAuthorText>
|
||||
<span>
|
||||
{activity.author?.name.firstName}{' '}
|
||||
{activity.author?.name.lastName}
|
||||
{activityForTimeline.author?.name.firstName}{' '}
|
||||
{activityForTimeline.author?.name.lastName}
|
||||
</span>
|
||||
created a {activity.type.toLowerCase()}
|
||||
created a {activityForTimeline.type.toLowerCase()}
|
||||
</StyledItemAuthorText>
|
||||
<StyledItemTitle>
|
||||
<StyledIconContainer>
|
||||
{activity.type === 'Note' && (
|
||||
{activityForTimeline.type === 'Note' && (
|
||||
<IconNotes size={theme.icon.size.sm} />
|
||||
)}
|
||||
{activity.type === 'Task' && (
|
||||
{activityForTimeline.type === 'Task' && (
|
||||
<IconCheckbox size={theme.icon.size.sm} />
|
||||
)}
|
||||
</StyledIconContainer>
|
||||
{(activity.type === 'Note' || activity.type === 'Task') && (
|
||||
{(activityForTimeline.type === 'Note' ||
|
||||
activityForTimeline.type === 'Task') && (
|
||||
<StyledActivityTitle
|
||||
onClick={() => openActivityRightDrawer(activity)}
|
||||
onClick={() =>
|
||||
openActivityRightDrawer(activityForTimeline.id)
|
||||
}
|
||||
>
|
||||
“
|
||||
<StyledActivityLink
|
||||
@ -195,11 +212,11 @@ export const TimelineActivity = ({
|
||||
)}
|
||||
</StyledItemTitle>
|
||||
</StyledItemTitleContainer>
|
||||
<StyledItemTitleDate id={`id-${activity.id}`}>
|
||||
<StyledItemTitleDate id={`id-${activityForTimeline.id}`}>
|
||||
{beautifiedCreatedAt}
|
||||
</StyledItemTitleDate>
|
||||
<StyledTooltip
|
||||
anchorSelect={`#id-${activity.id}`}
|
||||
anchorSelect={`#id-${activityForTimeline.id}`}
|
||||
content={exactCreatedAt}
|
||||
clickable
|
||||
noArrow
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer';
|
||||
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
import { groupActivitiesByMonth } from '../utils/groupActivitiesByMonth';
|
||||
@ -23,14 +24,12 @@ const StyledTimelineContainer = styled.div`
|
||||
|
||||
const StyledScrollWrapper = styled(ScrollWrapper)``;
|
||||
|
||||
export type TimelineItemsContainerProps = {
|
||||
activities: ActivityForDrawer[];
|
||||
};
|
||||
export const TimelineItemsContainer = () => {
|
||||
const timelineActivitiesForGroup = useRecoilValue(
|
||||
timelineActivitiesForGroupState,
|
||||
);
|
||||
|
||||
export const TimelineItemsContainer = ({
|
||||
activities,
|
||||
}: TimelineItemsContainerProps) => {
|
||||
const groupedActivities = groupActivitiesByMonth(activities);
|
||||
const groupedActivities = groupActivitiesByMonth(timelineActivitiesForGroup);
|
||||
|
||||
return (
|
||||
<StyledScrollWrapper>
|
||||
|
||||
@ -0,0 +1,131 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { timelineActivitiesFammilyState } from '@/activities/timeline/states/timelineActivitiesFamilyState';
|
||||
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
|
||||
import { timelineActivitiesNetworkingState } from '@/activities/timeline/states/timelineActivitiesNetworkingState';
|
||||
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityFirstLevelFamilySelector';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { sortObjectRecordByDateField } from '@/object-record/utils/sortObjectRecordByDateField';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const TimelineQueryEffect = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const setTimelineTargetableObject = useSetRecoilState(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimelineTargetableObject(targetableObject);
|
||||
}, [targetableObject, setTimelineTargetableObject]);
|
||||
|
||||
const { activities, initialized, noActivities } = useActivities({
|
||||
targetableObjects: [targetableObject],
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
skip: !isDefined(targetableObject),
|
||||
});
|
||||
|
||||
const [timelineActivitiesNetworking, setTimelineActivitiesNetworking] =
|
||||
useRecoilState(timelineActivitiesNetworkingState);
|
||||
|
||||
const [timelineActivitiesForGroup, setTimelineActivitiesForGroup] =
|
||||
useRecoilState(timelineActivitiesForGroupState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(targetableObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activitiesForGroup = activities
|
||||
.map((activity) => ({
|
||||
id: activity.id,
|
||||
createdAt: activity.createdAt,
|
||||
}))
|
||||
.toSorted(sortObjectRecordByDateField('createdAt', 'DescNullsLast'));
|
||||
|
||||
const timelineActivitiesForGroupSorted =
|
||||
timelineActivitiesForGroup.toSorted(
|
||||
sortObjectRecordByDateField('createdAt', 'DescNullsLast'),
|
||||
);
|
||||
|
||||
if (!isDeeplyEqual(activitiesForGroup, timelineActivitiesForGroupSorted)) {
|
||||
setTimelineActivitiesForGroup(activitiesForGroup);
|
||||
}
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(timelineActivitiesNetworking.initialized, initialized) ||
|
||||
!isDeeplyEqual(timelineActivitiesNetworking.noActivities, noActivities)
|
||||
) {
|
||||
setTimelineActivitiesNetworking({
|
||||
initialized,
|
||||
noActivities,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
activities,
|
||||
initialized,
|
||||
noActivities,
|
||||
setTimelineActivitiesNetworking,
|
||||
targetableObject,
|
||||
timelineActivitiesNetworking,
|
||||
timelineActivitiesForGroup,
|
||||
setTimelineActivitiesForGroup,
|
||||
]);
|
||||
|
||||
const updateTimelineActivities = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(newActivities: Activity[]) => {
|
||||
for (const newActivity of newActivities) {
|
||||
const currentActivity = snapshot
|
||||
.getLoadable(timelineActivitiesFammilyState(newActivity.id))
|
||||
.getValue();
|
||||
|
||||
if (!isDeeplyEqual(newActivity, currentActivity)) {
|
||||
set(timelineActivitiesFammilyState(newActivity.id), newActivity);
|
||||
}
|
||||
|
||||
const currentActivityWithoutTarget = snapshot
|
||||
.getLoadable(
|
||||
timelineActivityWithoutTargetsFamilyState(newActivity.id),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const newActivityWithoutTarget = {
|
||||
id: newActivity.id,
|
||||
title: newActivity.title,
|
||||
createdAt: newActivity.createdAt,
|
||||
author: newActivity.author,
|
||||
type: newActivity.type,
|
||||
};
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
newActivityWithoutTarget,
|
||||
currentActivityWithoutTarget,
|
||||
)
|
||||
) {
|
||||
set(
|
||||
timelineActivityWithoutTargetsFamilyState(newActivity.id),
|
||||
newActivityWithoutTarget,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateTimelineActivities(activities);
|
||||
}, [activities, updateTimelineActivities]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -68,7 +68,7 @@ export const TimelineActivityGroup = ({
|
||||
{group.items.map((activity, index) => (
|
||||
<TimelineActivity
|
||||
key={activity.id}
|
||||
activity={activity}
|
||||
activityId={activity.id}
|
||||
isLastActivity={index === group.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -4,7 +4,7 @@ import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectState';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
|
||||
export const timelineActivitiesFammilyState = atomFamily<
|
||||
Activity | null,
|
||||
string
|
||||
>({
|
||||
key: 'timelineActivitiesFammilyState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { ActivityForActivityGroup } from '@/activities/timeline/utils/groupActivitiesByMonth';
|
||||
|
||||
export const timelineActivitiesForGroupState = atom<ActivityForActivityGroup[]>(
|
||||
{
|
||||
key: 'timelineActivitiesForGroupState',
|
||||
default: [],
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,12 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const timelineActivitiesNetworkingState = atom<{
|
||||
initialized: boolean;
|
||||
noActivities: boolean;
|
||||
}>({
|
||||
key: 'timelineActivitiesNetworkingState',
|
||||
default: {
|
||||
initialized: false,
|
||||
noActivities: false,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
|
||||
export const timelineActivityWithoutTargetsFamilyState = atomFamily<
|
||||
Pick<Activity, 'id' | 'title' | 'createdAt' | 'author' | 'type'> | null,
|
||||
string
|
||||
>({
|
||||
key: 'timelineActivityFirstLevelFamilySelector',
|
||||
default: null,
|
||||
});
|
||||
@ -1,13 +1,18 @@
|
||||
import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
|
||||
export interface ActivityGroup {
|
||||
export type ActivityForActivityGroup = Pick<Activity, 'id' | 'createdAt'>;
|
||||
|
||||
export type ActivityGroup = {
|
||||
month: number;
|
||||
year: number;
|
||||
items: ActivityForDrawer[];
|
||||
}
|
||||
items: ActivityForActivityGroup[];
|
||||
};
|
||||
|
||||
export const groupActivitiesByMonth = (activities: ActivityForDrawer[]) => {
|
||||
export const groupActivitiesByMonth = (
|
||||
activities: ActivityForActivityGroup[],
|
||||
) => {
|
||||
const acitivityGroups: ActivityGroup[] = [];
|
||||
|
||||
for (const activity of activities) {
|
||||
const d = new Date(activity.createdAt);
|
||||
const month = d.getMonth();
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export type ActivityTargetableObject = {
|
||||
id: string;
|
||||
targetObjectNameSingular: string;
|
||||
targetObjectRecord: ObjectRecord;
|
||||
relatedTargetableObjects?: ActivityTargetableObject[];
|
||||
};
|
||||
|
||||
@ -4,13 +4,16 @@ import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { flattenTargetableObjectsAndTheirRelatedTargetableObjects } from '@/activities/utils/flattenTargetableObjectsAndTheirRelatedTargetableObjects';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const getActivityTargetsToCreateFromTargetableObjects = ({
|
||||
export const makeActivityTargetsToCreateFromTargetableObjects = ({
|
||||
targetableObjects,
|
||||
activityId,
|
||||
targetObjectRecords,
|
||||
}: {
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
activityId: string;
|
||||
targetObjectRecords: ObjectRecord[];
|
||||
}): Partial<ActivityTarget>[] => {
|
||||
const activityTargetableObjects = targetableObjects
|
||||
? flattenTargetableObjectsAndTheirRelatedTargetableObjects(
|
||||
@ -24,9 +27,12 @@ export const getActivityTargetsToCreateFromTargetableObjects = ({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const relatedObjectRecord = targetObjectRecords.find(
|
||||
(record) => record.id === targetableObject.id,
|
||||
);
|
||||
|
||||
const activityTarget = {
|
||||
[targetableObject.targetObjectNameSingular]:
|
||||
targetableObject.targetObjectRecord,
|
||||
[targetableObject.targetObjectNameSingular]: relatedObjectRecord,
|
||||
[targetableObjectFieldIdName]: targetableObject.id,
|
||||
activityId,
|
||||
id: v4(),
|
||||
|
||||
@ -196,7 +196,7 @@ export const CommandMenu = () => {
|
||||
id: activity.id,
|
||||
label: activity.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(activity),
|
||||
onCommandClick: () => openActivityRightDrawer(activity.id),
|
||||
})),
|
||||
[activities, openActivityRightDrawer],
|
||||
);
|
||||
@ -372,7 +372,7 @@ export const CommandMenu = () => {
|
||||
Icon={IconNotes}
|
||||
key={activity.id}
|
||||
label={activity.title ?? ''}
|
||||
onClick={() => openActivityRightDrawer(activity)}
|
||||
onClick={() => openActivityRightDrawer(activity.id)}
|
||||
/>
|
||||
</SelectableItem>
|
||||
))}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount';
|
||||
import { CurrentUserDueTaskCountEffect } from '@/activities/tasks/components/CurrentUserDueTaskCountEffect';
|
||||
import { currentUserDueTaskCountState } from '@/activities/tasks/states/currentUserTaskCountState';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { Favorites } from '@/favorites/components/Favorites';
|
||||
import { ObjectMetadataNavItems } from '@/object-metadata/components/ObjectMetadataNavItems';
|
||||
@ -23,7 +24,7 @@ export const MainNavigationDrawerItems = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
const isTasksPage = useIsTasksPage();
|
||||
const { currentUserDueTaskCount } = useCurrentUserTaskCount();
|
||||
const currentUserDueTaskCount = useRecoilValue(currentUserDueTaskCountState);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||
@ -54,6 +55,7 @@ export const MainNavigationDrawerItems = () => {
|
||||
}}
|
||||
Icon={IconSettings}
|
||||
/>
|
||||
<CurrentUserDueTaskCountEffect />
|
||||
<NavigationDrawerItem
|
||||
label="Tasks"
|
||||
to="/tasks"
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
|
||||
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import {
|
||||
FieldContext,
|
||||
@ -16,7 +14,9 @@ import { RecordInlineCell } from '@/object-record/record-inline-cell/components/
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { RecordRelationFieldCardSection } from '@/object-record/record-relation-card/components/RecordRelationFieldCardSection';
|
||||
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
|
||||
import { isFieldMetadataItemAvailable } from '@/object-record/utils/isFieldMetadataItemAvailable';
|
||||
import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer';
|
||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
|
||||
@ -29,6 +29,7 @@ import {
|
||||
FileFolder,
|
||||
useUploadImageMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type RecordShowContainerProps = {
|
||||
objectNameSingular: string;
|
||||
@ -39,28 +40,25 @@ export const RecordShowContainer = ({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
}: RecordShowContainerProps) => {
|
||||
const {
|
||||
objectMetadataItem,
|
||||
labelIdentifierFieldMetadata,
|
||||
mapToObjectRecordIdentifier,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { objectMetadataItem, labelIdentifierFieldMetadata } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const setEntityFields = useSetRecoilState(
|
||||
const [recordLoading] = useRecoilState(
|
||||
recordLoadingFamilyState(objectRecordId),
|
||||
);
|
||||
|
||||
const [recordFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(objectRecordId),
|
||||
);
|
||||
|
||||
const { record, loading } = useFindOneRecord({
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
depth: 3,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!record) return;
|
||||
setEntityFields(record);
|
||||
}, [record, setEntityFields]);
|
||||
const recordIdentifier = useRecoilValue(
|
||||
recordStoreIdentifierFamilySelector({
|
||||
objectNameSingular,
|
||||
recordId: objectRecordId,
|
||||
}),
|
||||
);
|
||||
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular });
|
||||
@ -96,12 +94,12 @@ export const RecordShowContainer = ({
|
||||
if (!updateOneRecord) {
|
||||
return;
|
||||
}
|
||||
if (!record) {
|
||||
if (!recordFromStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
idToUpdate: record.id,
|
||||
idToUpdate: objectRecordId,
|
||||
updateOneRecordInput: {
|
||||
avatarUrl,
|
||||
},
|
||||
@ -132,23 +130,19 @@ export const RecordShowContainer = ({
|
||||
<RecoilScope CustomRecoilScopeContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
{!loading && !!record && (
|
||||
{!recordLoading && isDefined(recordFromStore) && (
|
||||
<>
|
||||
<ShowPageSummaryCard
|
||||
id={record.id}
|
||||
logoOrAvatar={
|
||||
mapToObjectRecordIdentifier(record).avatarUrl ?? ''
|
||||
}
|
||||
avatarPlaceholder={
|
||||
mapToObjectRecordIdentifier(record).name ?? ''
|
||||
}
|
||||
date={record.createdAt ?? ''}
|
||||
id={objectRecordId}
|
||||
logoOrAvatar={recordIdentifier?.avatarUrl ?? ''}
|
||||
avatarPlaceholder={recordIdentifier?.name ?? ''}
|
||||
date={recordFromStore.createdAt ?? ''}
|
||||
title={
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: record.id,
|
||||
entityId: objectRecordId,
|
||||
recoilScopeId:
|
||||
record.id + labelIdentifierFieldMetadata?.id,
|
||||
objectRecordId + labelIdentifierFieldMetadata?.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
type: parseFieldType(
|
||||
@ -169,9 +163,7 @@ export const RecordShowContainer = ({
|
||||
<RecordInlineCell />
|
||||
</FieldContext.Provider>
|
||||
}
|
||||
avatarType={
|
||||
mapToObjectRecordIdentifier(record).avatarType ?? 'rounded'
|
||||
}
|
||||
avatarType={recordIdentifier?.avatarType ?? 'rounded'}
|
||||
onUploadPicture={
|
||||
objectNameSingular === 'person' ? onUploadPicture : undefined
|
||||
}
|
||||
@ -179,11 +171,11 @@ export const RecordShowContainer = ({
|
||||
<PropertyBox extraPadding={true}>
|
||||
{inlineFieldMetadataItems.map((fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={record.id + fieldMetadataItem.id}
|
||||
key={objectRecordId + fieldMetadataItem.id}
|
||||
value={{
|
||||
entityId: record.id,
|
||||
entityId: objectRecordId,
|
||||
maxWidth: 200,
|
||||
recoilScopeId: record.id + fieldMetadataItem.id,
|
||||
recoilScopeId: objectRecordId + fieldMetadataItem.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
@ -217,10 +209,10 @@ export const RecordShowContainer = ({
|
||||
})
|
||||
.map((fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={record.id + fieldMetadataItem.id}
|
||||
key={objectRecordId + fieldMetadataItem.id}
|
||||
value={{
|
||||
entityId: record.id,
|
||||
recoilScopeId: record.id + fieldMetadataItem.id,
|
||||
entityId: objectRecordId,
|
||||
recoilScopeId: objectRecordId + fieldMetadataItem.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition:
|
||||
formatFieldMetadataItemAsColumnDefinition({
|
||||
@ -238,12 +230,11 @@ export const RecordShowContainer = ({
|
||||
</>
|
||||
)}
|
||||
</ShowPageLeftContainer>
|
||||
{record ? (
|
||||
{recordFromStore ? (
|
||||
<ShowPageRightContainer
|
||||
targetableObject={{
|
||||
id: record.id,
|
||||
id: objectRecordId,
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
targetObjectRecord: record,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityConnectionUtils } from '@/activities/hooks/useActivityConnectionUtils';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const RecordShowContainer = ({
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectRecordId: string;
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
const { record, loading } = useFindOneRecord({
|
||||
objectRecordId,
|
||||
objectNameSingular,
|
||||
depth: 3,
|
||||
});
|
||||
|
||||
const setRecordStore = useSetRecoilState(
|
||||
recordStoreFamilyState(objectRecordId),
|
||||
);
|
||||
|
||||
const [recordLoading, setRecordLoading] = useRecoilState(
|
||||
recordLoadingFamilyState(objectRecordId),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading !== recordLoading) {
|
||||
setRecordLoading(loading);
|
||||
}
|
||||
}, [loading, recordLoading, setRecordLoading]);
|
||||
|
||||
const { makeActivityWithoutConnection } = useActivityConnectionUtils();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && isDefined(record)) {
|
||||
const { activity: activityWithoutConnection } =
|
||||
makeActivityWithoutConnection(record as any);
|
||||
|
||||
setRecordStore(activityWithoutConnection as Activity);
|
||||
}
|
||||
}, [loading, record, setRecordStore, makeActivityWithoutConnection]);
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const recordLoadingFamilyState = atomFamily<boolean, string>({
|
||||
key: 'recordLoadingFamilyState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
|
||||
export const recordStoreIdentifierFamilySelector = selectorFamily({
|
||||
key: 'recordStoreIdentifierFamilySelector',
|
||||
get:
|
||||
({
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
}: {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
}) =>
|
||||
({ get }) => {
|
||||
const recordFromStore = get(recordStoreFamilyState(recordId));
|
||||
|
||||
const objectMetadataItems = get(objectMetadataItemsState);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
if (!objectMetadataItem || !recordFromStore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getObjectRecordIdentifier({
|
||||
objectMetadataItem: objectMetadataItem,
|
||||
record: recordFromStore,
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const getChildRelationArray = ({
|
||||
childRelation,
|
||||
}: {
|
||||
childRelation: any;
|
||||
}) => {
|
||||
if (isDefined(childRelation.edges) && Array.isArray(childRelation.edges)) {
|
||||
return childRelation.edges.map((edge: ObjectRecordEdge) => edge.node);
|
||||
} else {
|
||||
return childRelation;
|
||||
}
|
||||
};
|
||||
@ -6,6 +6,7 @@ import { Attachments } from '@/activities/files/components/Attachments';
|
||||
import { Notes } from '@/activities/notes/components/Notes';
|
||||
import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
|
||||
import { Timeline } from '@/activities/timeline/components/Timeline';
|
||||
import { TimelineQueryEffect } from '@/activities/timeline/components/TimelineQueryEffect';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
@ -42,7 +43,10 @@ const StyledTabListContainer = styled.div`
|
||||
export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list';
|
||||
|
||||
type ShowPageRightContainerProps = {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
targetableObject: Pick<
|
||||
ActivityTargetableObject,
|
||||
'targetObjectNameSingular' | 'id'
|
||||
>;
|
||||
timeline?: boolean;
|
||||
tasks?: boolean;
|
||||
notes?: boolean;
|
||||
@ -114,7 +118,10 @@ export const ShowPageRightContainer = ({
|
||||
<TabList tabListId={TAB_LIST_COMPONENT_ID} tabs={TASK_TABS} />
|
||||
</StyledTabListContainer>
|
||||
{activeTabId === 'timeline' && (
|
||||
<Timeline targetableObject={targetableObject} />
|
||||
<>
|
||||
<TimelineQueryEffect targetableObject={targetableObject} />
|
||||
<Timeline targetableObject={targetableObject} />
|
||||
</>
|
||||
)}
|
||||
{activeTabId === 'tasks' && (
|
||||
<ObjectTasks targetableObject={targetableObject} />
|
||||
|
||||
@ -97,7 +97,6 @@ export const RecordShowPage = () => {
|
||||
activityTargetObject={{
|
||||
id: record.id,
|
||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||
targetObjectRecord: record,
|
||||
}}
|
||||
/>
|
||||
<ShowPageMoreButton
|
||||
|
||||
Reference in New Issue
Block a user