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

View File

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

View File

@ -10,7 +10,10 @@ export const useActivityTargetsForTargetableObjects = ({
targetableObjects,
skip,
}: {
targetableObjects: ActivityTargetableObject[];
targetableObjects: Pick<
ActivityTargetableObject,
'id' | 'targetObjectNameSingular'
>[];
skip?: boolean;
}) => {
const activityTargetsFilter = getActivityTargetsFilter({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ?? ''),

View File

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

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const activityBodyFamilyState = atomFamily<
string,
{ activityId: string }
>({
key: 'activityBodyFamilyState',
default: '',
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const activityIdInDrawerState = atom<string | null>({
key: 'activityIdInDrawerState',
default: null,
});

View File

@ -1,8 +0,0 @@
import { atom } from 'recoil';
import { Activity } from '@/activities/types/Activity';
export const activityInDrawerState = atom<Activity | null>({
key: 'activityInDrawerState',
default: null,
});

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const activityTitleFamilyState = atomFamily<
string,
{ activityId: string }
>({
key: 'activityTitleFamilyState',
default: '',
});

View File

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

View File

@ -85,7 +85,7 @@ export const TaskRow = ({ task }: { task: Activity }) => {
return (
<StyledContainer
onClick={() => {
openActivityRightDrawer(task);
openActivityRightDrawer(task.id);
}}
>
<div

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const currentUserDueTaskCountState = atom<number>({
default: 0,
key: 'currentUserDueTaskCountState',
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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';

View File

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

View File

@ -0,0 +1,10 @@
import { atom } from 'recoil';
import { ActivityForActivityGroup } from '@/activities/timeline/utils/groupActivitiesByMonth';
export const timelineActivitiesForGroupState = atom<ActivityForActivityGroup[]>(
{
key: 'timelineActivitiesForGroupState',
default: [],
},
);

View File

@ -0,0 +1,12 @@
import { atom } from 'recoil';
export const timelineActivitiesNetworkingState = atom<{
initialized: boolean;
noActivities: boolean;
}>({
key: 'timelineActivitiesNetworkingState',
default: {
initialized: false,
noActivities: false,
},
});

View File

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

View File

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

View File

@ -1,8 +1,5 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export type ActivityTargetableObject = {
id: string;
targetObjectNameSingular: string;
targetObjectRecord: ObjectRecord;
relatedTargetableObjects?: ActivityTargetableObject[];
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const recordLoadingFamilyState = atomFamily<boolean, string>({
key: 'recordLoadingFamilyState',
default: false,
});

View File

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

View File

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

View File

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

View File

@ -97,7 +97,6 @@ export const RecordShowPage = () => {
activityTargetObject={{
id: record.id,
targetObjectNameSingular: objectMetadataItem?.nameSingular,
targetObjectRecord: record,
}}
/>
<ShowPageMoreButton