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} />
|
||||
|
||||
Reference in New Issue
Block a user