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:
Lucas Bordeau
2024-02-23 17:54:27 +01:00
committed by GitHub
parent 5de1c2c31d
commit fb920a92e7
48 changed files with 1114 additions and 527 deletions

View File

@ -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,

View File

@ -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 <></>;
};

View File

@ -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(),
});

View File

@ -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}
/>
)}

View File

@ -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 <></>;
};

View File

@ -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>
);
};

View File

@ -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}

View File

@ -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 <></>;
};

View File

@ -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} />