Activity as standard object (#6219)
In this PR I layout the first steps to migrate Activity to a traditional Standard objects Since this is a big transition, I'd rather split it into several deployments / PRs <img width="1512" alt="image" src="https://github.com/user-attachments/assets/012e2bbf-9d1b-4723-aaf6-269ef588b050"> --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: Weiko <corentin@twenty.com> Co-authored-by: Faisal-imtiyaz123 <142205282+Faisal-imtiyaz123@users.noreply.github.com> Co-authored-by: Prateek Jain <prateekj1171998@gmail.com>
This commit is contained in:
@ -1,129 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { Comment } from '@/activities/comment/Comment';
|
||||
import { Comment as CommentType } from '@/activities/types/Comment';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import {
|
||||
AutosizeTextInput,
|
||||
AutosizeTextInputVariant,
|
||||
} from '@/ui/input/components/AutosizeTextInput';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
const StyledThreadItemListContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
justify-content: flex-start;
|
||||
padding: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(12)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCommentActionBar = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
display: flex;
|
||||
padding: 16px 24px 16px 48px;
|
||||
width: calc(
|
||||
${({ theme }) => (useIsMobile() ? '100%' : theme.rightDrawerWidth)} - 72px
|
||||
);
|
||||
`;
|
||||
|
||||
const StyledThreadCommentTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
type ActivityCommentsProps = {
|
||||
activityId: string;
|
||||
scrollableContainerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const ActivityComments = ({
|
||||
activityId,
|
||||
scrollableContainerRef,
|
||||
}: ActivityCommentsProps) => {
|
||||
const { createOneRecord: createOneComment } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Comment,
|
||||
});
|
||||
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { records: comments } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Comment,
|
||||
skip: !isNonEmptyString(activityId),
|
||||
filter: {
|
||||
activityId: {
|
||||
eq: activityId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!currentWorkspaceMember) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleSendComment = (commentText: string) => {
|
||||
if (!isNonEmptyString(commentText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
createOneComment?.({
|
||||
id: v4(),
|
||||
authorId: currentWorkspaceMember?.id ?? '',
|
||||
author: currentWorkspaceMember,
|
||||
activityId: activityId,
|
||||
body: commentText,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
const scrollableContainer = scrollableContainerRef.current;
|
||||
|
||||
scrollableContainer?.scrollTo({
|
||||
top: scrollableContainer.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{comments.length > 0 && (
|
||||
<>
|
||||
<StyledThreadItemListContainer>
|
||||
<StyledThreadCommentTitle>Comments</StyledThreadCommentTitle>
|
||||
{comments?.map((comment) => (
|
||||
<Comment key={comment.id} comment={comment as CommentType} />
|
||||
))}
|
||||
</StyledThreadItemListContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
<StyledCommentActionBar>
|
||||
{currentWorkspaceMember && (
|
||||
<AutosizeTextInput
|
||||
onValidate={handleSendComment}
|
||||
onFocus={handleFocus}
|
||||
variant={AutosizeTextInputVariant.Button}
|
||||
placeholder={comments.length > 0 ? 'Reply...' : undefined}
|
||||
/>
|
||||
)}
|
||||
</StyledCommentActionBar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,43 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
|
||||
const StyledCreationDisplay = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type ActivityCreationDateProps = {
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const ActivityCreationDate = ({
|
||||
activityId,
|
||||
}: ActivityCreationDateProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
const activity = activityInStore as Activity;
|
||||
|
||||
const beautifiedDate = activity.createdAt
|
||||
? beautifyPastDateRelativeToNow(activity.createdAt)
|
||||
: null;
|
||||
|
||||
const authorName = activity.author?.name
|
||||
? `${activity.author.name.firstName} ${activity.author.name.lastName}`
|
||||
: null;
|
||||
|
||||
if (!activity.createdAt || !authorName) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCreationDisplay>
|
||||
Created {beautifiedDate} by {authorName}
|
||||
</StyledCreationDisplay>
|
||||
);
|
||||
};
|
||||
@ -1,89 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
|
||||
import { ActivityBodyEffect } from '@/activities/components/ActivityBodyEffect';
|
||||
import { ActivityComments } from '@/activities/components/ActivityComments';
|
||||
import { ActivityCreationDate } from '@/activities/components/ActivityCreationDate';
|
||||
import { ActivityEditorFields } from '@/activities/components/ActivityEditorFields';
|
||||
import { ActivityTitleEffect } from '@/activities/components/ActivityTitleEffect';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { ActivityTitle } from './ActivityTitle';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledUpperPartContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTopContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border-bottom: ${({ theme }) =>
|
||||
useIsMobile() ? 'none' : `1px solid ${theme.border.color.medium}`};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
type ActivityEditorProps = {
|
||||
activityId: string;
|
||||
showComment?: boolean;
|
||||
fillTitleFromBody?: boolean;
|
||||
};
|
||||
|
||||
export const ActivityEditor = ({
|
||||
activityId,
|
||||
showComment = true,
|
||||
fillTitleFromBody = false,
|
||||
}: ActivityEditorProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
<StyledUpperPartContainer>
|
||||
<StyledTopContainer>
|
||||
<ActivityTitleEffect activityId={activityId} />
|
||||
<StyledTitleContainer>
|
||||
<ActivityTitle activityId={activityId} />
|
||||
<ActivityCreationDate activityId={activityId} />
|
||||
</StyledTitleContainer>
|
||||
<ActivityEditorFields activityId={activityId} />
|
||||
</StyledTopContainer>
|
||||
</StyledUpperPartContainer>
|
||||
<ActivityBodyEffect activityId={activityId} />
|
||||
<ActivityBodyEditor
|
||||
activityId={activityId}
|
||||
fillTitleFromBody={fillTitleFromBody}
|
||||
/>
|
||||
{showComment && (
|
||||
<ActivityComments
|
||||
activityId={activityId}
|
||||
scrollableContainerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,102 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache';
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const ActivityEditorEffect = ({
|
||||
activityId,
|
||||
}: {
|
||||
activityId: string;
|
||||
}) => {
|
||||
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
|
||||
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
|
||||
);
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
const deleteRecordFromCache = useDeleteRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
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 && isDefined(activity)) {
|
||||
if (canCreateActivity) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: activityFromStore.title,
|
||||
body: activityFromStore.body,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
deleteRecordFromCache(activity);
|
||||
}
|
||||
|
||||
set(isActivityInCreateModeState, false);
|
||||
} else if (isDefined(activity)) {
|
||||
if (
|
||||
activity.title !== activityTitle ||
|
||||
activity.body !== activityBody
|
||||
) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: activityTitle,
|
||||
body: activityBody,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[activityId, deleteRecordFromCache, upsertActivity],
|
||||
);
|
||||
|
||||
useRegisterClickOutsideListenerCallback({
|
||||
callbackId: 'activity-editor',
|
||||
callbackFunction: upsertActivityCallback,
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,115 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } 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 { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
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 { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompletedState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledPropertyBox = styled(PropertyBox)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const ActivityEditorFields = ({
|
||||
activityId,
|
||||
}: {
|
||||
activityId: string;
|
||||
}) => {
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const isRightDrawerAnimationCompleted = useRecoilValue(
|
||||
isRightDrawerAnimationCompletedState,
|
||||
);
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const activityFromCache = getRecordFromCache<Activity>(activityId);
|
||||
|
||||
const activityFromStore = useRecoilValue(recordStoreFamilyState(activityId));
|
||||
|
||||
const activity = activityFromStore as Activity;
|
||||
|
||||
const useUpsertOneActivityMutation: RecordUpdateHook = () => {
|
||||
const upsertActivityMutation = async ({
|
||||
variables,
|
||||
}: RecordUpdateHookParams) => {
|
||||
if (isDefined(activityFromStore)) {
|
||||
await upsertActivity({
|
||||
activity: activityFromStore as Activity,
|
||||
input: variables.updateOneRecordInput,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return [upsertActivityMutation, { loading: false }];
|
||||
};
|
||||
|
||||
const { FieldContextProvider: ReminderAtFieldContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'reminderAt',
|
||||
fieldPosition: 0,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'dueAt',
|
||||
fieldPosition: 1,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: AssigneeFieldContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'assignee',
|
||||
fieldPosition: 2,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledPropertyBox>
|
||||
{activity.type === 'Task' &&
|
||||
ReminderAtFieldContextProvider &&
|
||||
DueAtFieldContextProvider &&
|
||||
AssigneeFieldContextProvider && (
|
||||
<>
|
||||
<ReminderAtFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</ReminderAtFieldContextProvider>
|
||||
<DueAtFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</DueAtFieldContextProvider>
|
||||
<AssigneeFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</AssigneeFieldContextProvider>
|
||||
</>
|
||||
)}
|
||||
{isDefined(activityFromCache) && isRightDrawerAnimationCompleted && (
|
||||
<ActivityTargetsInlineCell
|
||||
activity={activityFromCache}
|
||||
maxWidth={340}
|
||||
/>
|
||||
)}
|
||||
</StyledPropertyBox>
|
||||
);
|
||||
};
|
||||
@ -1,199 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxShape,
|
||||
CheckboxSize,
|
||||
} from '@/ui/input/components/Checkbox';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledEditableTitleInput = styled.input<{
|
||||
completed: boolean;
|
||||
value: string;
|
||||
}>`
|
||||
background: transparent;
|
||||
|
||||
border: none;
|
||||
color: ${({ theme, value }) =>
|
||||
value ? theme.font.color.primary : theme.font.color.light};
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
outline: none;
|
||||
text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')};
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
}
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(2)});
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type ActivityTitleProps = {
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const ActivityTitle = ({ activityId }: ActivityTitleProps) => {
|
||||
const [activityInStore, setActivityInStore] = useRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
|
||||
const [activityTitle, setActivityTitle] = useRecoilState(
|
||||
activityTitleFamilyState({ activityId }),
|
||||
);
|
||||
|
||||
const activity = activityInStore as Activity;
|
||||
|
||||
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
|
||||
canCreateActivityState,
|
||||
);
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
handleBlur();
|
||||
},
|
||||
ActivityEditorHotkeyScope.ActivityTitle,
|
||||
);
|
||||
|
||||
const handleBlur = () => {
|
||||
goBackToPreviousHotkeyScope();
|
||||
titleInputRef.current?.blur();
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
ActivityEditorHotkeyScope.ActivityTitle,
|
||||
);
|
||||
};
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activityId,
|
||||
}),
|
||||
);
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const persistTitleDebounced = useDebouncedCallback((newTitle: string) => {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: newTitle,
|
||||
},
|
||||
});
|
||||
|
||||
if (!activityTitleHasBeenSet) {
|
||||
setActivityTitleHasBeenSet(true);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const setTitleDebounced = useDebouncedCallback((newTitle: string) => {
|
||||
setActivityInStore((currentActivity) => {
|
||||
return {
|
||||
...currentActivity,
|
||||
id: activity.id,
|
||||
title: newTitle,
|
||||
__typename: activity.__typename,
|
||||
};
|
||||
});
|
||||
|
||||
if (isNonEmptyString(newTitle) && !canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
modifyRecordFromCache({
|
||||
recordId: activity.id,
|
||||
fieldModifiers: {
|
||||
title: () => {
|
||||
return newTitle;
|
||||
},
|
||||
},
|
||||
cache: cache,
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setActivityTitle(newTitle);
|
||||
|
||||
setTitleDebounced(newTitle);
|
||||
|
||||
persistTitleDebounced(newTitle);
|
||||
};
|
||||
|
||||
const handleActivityCompletionChange = (value: boolean) => {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
completedAt: value ? new Date().toISOString() : null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const completed = isDefined(activity.completedAt);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{activity.type === 'Task' && (
|
||||
<Checkbox
|
||||
size={CheckboxSize.Large}
|
||||
shape={CheckboxShape.Rounded}
|
||||
checked={completed}
|
||||
onCheckedChange={(value) => handleActivityCompletionChange(value)}
|
||||
/>
|
||||
)}
|
||||
<StyledEditableTitleInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
ref={titleInputRef}
|
||||
placeholder={`${activity.type} title`}
|
||||
onChange={(event) => handleTitleChange(event.target.value)}
|
||||
value={activityTitle}
|
||||
completed={completed}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const ActivityTitleEffect = ({ activityId }: { activityId: string }) => {
|
||||
const [activityFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const [activityTitle, setActivityTitle] = useRecoilState(
|
||||
activityTitleFamilyState({ activityId }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
activityTitle === '' &&
|
||||
isDefined(activityFromStore) &&
|
||||
activityTitle !== activityFromStore.title
|
||||
) {
|
||||
setActivityTitle(activityFromStore.title);
|
||||
}
|
||||
}, [activityFromStore, activityTitle, setActivityTitle]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,40 +0,0 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import {
|
||||
Chip,
|
||||
ChipAccent,
|
||||
ChipSize,
|
||||
ChipVariant,
|
||||
IconCheckbox,
|
||||
IconNotes,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
|
||||
type ActivityTypeDropdownProps = {
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const ActivityTypeDropdown = ({
|
||||
activityId,
|
||||
}: ActivityTypeDropdownProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={activityInStore?.type}
|
||||
leftComponent={
|
||||
activityInStore?.type === 'Note' ? (
|
||||
<IconNotes size={theme.icon.size.md} />
|
||||
) : (
|
||||
<IconCheckbox size={theme.icon.size.md} />
|
||||
)
|
||||
}
|
||||
size={ChipSize.Large}
|
||||
accent={ChipAccent.TextSecondary}
|
||||
variant={ChipVariant.Highlighted}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -12,10 +12,8 @@ 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';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
@ -29,22 +27,31 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
import { getFileType } from '../files/utils/getFileType';
|
||||
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import '@blocknote/react/style.css';
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
|
||||
type ActivityBodyEditorProps = {
|
||||
type RichTextEditorProps = {
|
||||
activityId: string;
|
||||
fillTitleFromBody: boolean;
|
||||
activityObjectNameSingular:
|
||||
| CoreObjectNameSingular.Task
|
||||
| CoreObjectNameSingular.Note;
|
||||
};
|
||||
|
||||
export const ActivityBodyEditor = ({
|
||||
export const RichTextEditor = ({
|
||||
activityId,
|
||||
fillTitleFromBody,
|
||||
}: ActivityBodyEditorProps) => {
|
||||
activityObjectNameSingular,
|
||||
}: RichTextEditorProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
const activity = activityInStore as Activity | null;
|
||||
const activity = activityInStore as Task | Note | null;
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
@ -60,7 +67,7 @@ export const ActivityBodyEditor = ({
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const {
|
||||
@ -68,7 +75,9 @@ export const ActivityBodyEditor = ({
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
const { upsertActivity } = useUpsertActivity({
|
||||
activityObjectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const persistBodyDebounced = useDebouncedCallback((newBody: string) => {
|
||||
if (isDefined(activity)) {
|
||||
Reference in New Issue
Block a user