Files
twenty/front/src/modules/activities/components/ActivityEditor.tsx
Charles Bochet 35ea6b5a2f Remove activityType and Id (#1179)
* Remove activityType and Id

* Fix tests

* Fix tests
2023-08-11 17:31:54 -07:00

227 lines
6.7 KiB
TypeScript

import React, { useCallback, useRef, useState } from 'react';
import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { GET_ACTIVITIES } from '@/activities/queries';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField';
import { IconCalendar } from '@/ui/icon/index';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import {
Activity,
ActivityTarget,
ActivityType,
User,
useUpdateActivityMutation,
} from '~/generated/graphql';
import { debounce } from '~/utils/debounce';
import { ActivityAssigneeEditableField } from '../editable-fields/components/ActivityAssigneeEditableField';
import { ActivityRelationEditableField } from '../editable-fields/components/ActivityRelationEditableField';
import { ACTIVITY_UPDATE_FRAGMENT } from '../queries/update';
import { CommentForDrawer } from '../types/CommentForDrawer';
import { ActivityTitle } from './ActivityTitle';
import '@blocknote/core/style.css';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
overflow-y: auto;
`;
const StyledUpperPartContainer = styled.div`
align-items: flex-start;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
`;
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: 24px 24px 24px 48px;
`;
type OwnProps = {
activity: Pick<
Activity,
'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt'
> & {
comments?: Array<CommentForDrawer> | null;
} & {
assignee?: Pick<
User,
'id' | 'firstName' | 'lastName' | 'displayName'
> | null;
} & {
activityTargets?: Array<Pick<ActivityTarget, 'id'>> | null;
};
showComment?: boolean;
autoFillTitle?: boolean;
};
export function ActivityEditor({
activity,
showComment = true,
autoFillTitle = false,
}: OwnProps) {
const [hasUserManuallySetTitle, setHasUserManuallySetTitle] =
useState<boolean>(false);
const [title, setTitle] = useState<string | null>(activity.title ?? '');
const [completedAt, setCompletedAt] = useState<string | null>(
activity.completedAt ?? '',
);
const containerRef = useRef<HTMLDivElement>(null);
const [updateActivityMutation] = useUpdateActivityMutation();
const client = useApolloClient();
const cachedActivity = client.readFragment({
id: `Activity:${activity.id}`,
fragment: ACTIVITY_UPDATE_FRAGMENT,
});
const updateTitle = useCallback(
(newTitle: string) => {
updateActivityMutation({
variables: {
where: {
id: activity.id,
},
data: {
title: newTitle ?? '',
},
},
optimisticResponse: {
__typename: 'Mutation',
updateOneActivity: {
__typename: 'Activity',
...cachedActivity,
title: newTitle,
},
},
});
},
[activity.id, cachedActivity, updateActivityMutation],
);
const handleActivityCompletionChange = useCallback(
(value: boolean) => {
updateActivityMutation({
variables: {
where: {
id: activity.id,
},
data: {
completedAt: value ? new Date().toISOString() : null,
},
},
optimisticResponse: {
__typename: 'Mutation',
updateOneActivity: {
__typename: 'Activity',
...cachedActivity,
completedAt: value ? new Date().toISOString() : null,
},
},
refetchQueries: [getOperationName(GET_ACTIVITIES) ?? ''],
});
setCompletedAt(value ? new Date().toISOString() : null);
},
[activity.id, cachedActivity, updateActivityMutation],
);
const debouncedUpdateTitle = debounce(updateTitle, 200);
function updateTitleFromBody(body: string) {
const parsedTitle = JSON.parse(body)[0]?.content[0]?.text;
if (!hasUserManuallySetTitle && autoFillTitle) {
setTitle(parsedTitle);
debouncedUpdateTitle(parsedTitle);
}
}
if (!activity) {
return <></>;
}
return (
<StyledContainer ref={containerRef}>
<StyledUpperPartContainer>
<StyledTopContainer>
<ActivityTypeDropdown activity={activity} />
<ActivityTitle
title={title ?? ''}
completed={!!completedAt}
type={activity.type}
onTitleChange={(newTitle) => {
setTitle(newTitle);
setHasUserManuallySetTitle(true);
debouncedUpdateTitle(newTitle);
}}
onCompletionChange={handleActivityCompletionChange}
/>
<PropertyBox>
{activity.type === ActivityType.Task && (
<>
<DateEditableField
value={activity.dueAt}
icon={<IconCalendar />}
label="Due date"
onSubmit={(newDate) => {
updateActivityMutation({
variables: {
where: {
id: activity.id,
},
data: {
dueAt: newDate,
},
},
refetchQueries: [getOperationName(GET_ACTIVITIES) ?? ''],
});
}}
/>
<ActivityAssigneeEditableField activity={activity} />
</>
)}
<ActivityRelationEditableField activity={activity} />
</PropertyBox>
</StyledTopContainer>
<ActivityBodyEditor
activity={activity}
onChange={updateTitleFromBody}
/>
</StyledUpperPartContainer>
{showComment && (
<ActivityComments
activity={{
id: activity.id,
comments: activity.comments ?? [],
}}
scrollableContainerRef={containerRef}
/>
)}
</StyledContainer>
);
}