From 36032bc16f89a4be1e9d1d8e72cc36e0d8d1a9c5 Mon Sep 17 00:00:00 2001 From: Tony Vi Date: Mon, 7 Aug 2023 23:18:13 +0300 Subject: [PATCH] refactor(CommentForm): remove external entity props --- .../CommentCreateForm/CommentCreateForm.tsx | 97 +++---- .../CommentEditForm.i18n/en.json | 3 - .../CommentEditForm.i18n/index.ts | 17 -- .../CommentEditForm.i18n/ru.json | 3 - .../CommentEditForm/CommentEditForm.tsx | 82 ------ src/components/CommentForm/CommentForm.tsx | 91 ++---- .../CommentView/CommentView.i18n/en.json | 3 +- .../CommentView/CommentView.i18n/ru.json | 3 +- src/components/CommentView/CommentView.tsx | 188 ++++++------ src/components/GoalActivity.tsx | 72 +---- src/components/GoalPreview/GoalPreview.tsx | 271 ++++++++++++------ 11 files changed, 361 insertions(+), 469 deletions(-) delete mode 100644 src/components/CommentEditForm/CommentEditForm.i18n/en.json delete mode 100644 src/components/CommentEditForm/CommentEditForm.i18n/index.ts delete mode 100644 src/components/CommentEditForm/CommentEditForm.i18n/ru.json delete mode 100644 src/components/CommentEditForm/CommentEditForm.tsx diff --git a/src/components/CommentCreateForm/CommentCreateForm.tsx b/src/components/CommentCreateForm/CommentCreateForm.tsx index ef4b8ab6c..49fe8df91 100644 --- a/src/components/CommentCreateForm/CommentCreateForm.tsx +++ b/src/components/CommentCreateForm/CommentCreateForm.tsx @@ -1,11 +1,11 @@ -import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { ArrowDownSmallIcon, ArrowUpSmallIcon, Button, Dropdown, UserPic } from '@taskany/bricks'; import { State } from '@prisma/client'; import styled from 'styled-components'; import { usePageContext } from '../../hooks/usePageContext'; -import { useCommentResource } from '../../hooks/useCommentResource'; -import { GoalCommentSchema } from '../../schema/goal'; +import { GoalCommentFormSchema } from '../../schema/goal'; +import { CommentSchema } from '../../schema/comment'; import { CommentForm } from '../CommentForm/CommentForm'; import { ActivityFeedItem } from '../ActivityFeed'; import { ColorizedMenuItem } from '../ColorizedMenuItem'; @@ -13,16 +13,12 @@ import { StateDot } from '../StateDot'; import { tr } from './CommentCreateForm.i18n'; -interface CommentCreateFormProps { - goalId: string; +interface CommentCreateFormProps extends Omit, 'actionButton'> { states?: State[]; - description?: string; stateId?: string; - onSubmit?: (id?: string) => void; - onFocus?: () => void; - onCancel?: () => void; - onChange?: (comment?: { description?: string; stateId?: string }) => void; + onSubmit: (comment: GoalCommentFormSchema) => void; + onChange?: (comment: GoalCommentFormSchema) => void; } const StyledStateUpdate = styled.div` @@ -31,10 +27,9 @@ const StyledStateUpdate = styled.div` `; const CommentCreateForm: React.FC = ({ - goalId, states, - description: currentDescription, - stateId = '', + description: currentDescription = '', + stateId, onSubmit, onFocus, onCancel, @@ -43,60 +38,48 @@ const CommentCreateForm: React.FC = ({ const statesMap = useMemo(() => { if (!states) return {}; - return states.reduce((acc, cur) => { + return states.reduce>((acc, cur) => { acc[cur.id] = cur; return acc; - }, {} as Record); + }, {}); }, [states]); const { user, themeId } = usePageContext(); - const { create } = useCommentResource(); - const [pushState, setPushState] = useState(statesMap[stateId]); - const [description, setDescription] = useState(currentDescription); + + const [pushState, setPushState] = useState(stateId ? statesMap[stateId] : undefined); + const [description, setDescription] = useState(currentDescription); const [focused, setFocused] = useState(Boolean(currentDescription)); const [busy, setBusy] = useState(false); - const [currentGoal, setCurrentGoal] = useState(goalId); - const [prevGoal, setPrevGoal] = useState(''); - - useEffect(() => { - if (!description) { - onChange?.(); - return; - } - - onChange?.({ description, stateId: pushState?.id }); - }, [pushState?.id, description, onChange]); - - useEffect(() => { - if (goalId === prevGoal) return; - - setCurrentGoal(goalId); - setPrevGoal(goalId); - setDescription(currentDescription); - setFocused(Boolean(currentDescription)); - setPushState(statesMap[stateId]); - }, [currentDescription, goalId, prevGoal, stateId, statesMap]); const onCommentFocus = useCallback(() => { setFocused(true); onFocus?.(); }, [onFocus]); - const createComment = useCallback( - async (form: GoalCommentSchema) => { + const onCommentChange = useCallback( + ({ description }: { description: string }) => { + onChange?.({ description, stateId: pushState?.id }); + }, + [onChange, pushState?.id], + ); + + const onCommentSubmit = useCallback( + async (form: CommentSchema) => { setBusy(true); setFocused(false); - await create(({ id }) => { - onSubmit?.(id); - setDescription(''); - setPushState(undefined); - })(form); + await onSubmit?.({ + ...form, + stateId: pushState?.id, + }); + + setDescription(''); + setPushState(undefined); setBusy(false); setFocused(true); }, - [create, onSubmit], + [onSubmit, pushState?.id], ); const onCancelCreate = useCallback(() => { @@ -107,22 +90,28 @@ const CommentCreateForm: React.FC = ({ onCancel?.(); }, [onCancel]); - const onStateSelect = useCallback((state: State) => { - setPushState((prev) => (state.id === prev?.id ? undefined : state)); - }, []); + const onStateSelect = useCallback( + (state: State) => { + setPushState((prev) => { + const newState = state.id === prev?.id ? undefined : state; + onChange?.({ description, stateId: newState?.id }); + + return newState; + }); + }, + [onChange, setPushState, description], + ); return ( = {}; - -keyset['ru'] = ru; -keyset['en'] = en; - -export const tr = i18n(keyset, fmt, getLang); diff --git a/src/components/CommentEditForm/CommentEditForm.i18n/ru.json b/src/components/CommentEditForm/CommentEditForm.i18n/ru.json deleted file mode 100644 index aff9ec5c6..000000000 --- a/src/components/CommentEditForm/CommentEditForm.i18n/ru.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Save": "Сохранить" -} diff --git a/src/components/CommentEditForm/CommentEditForm.tsx b/src/components/CommentEditForm/CommentEditForm.tsx deleted file mode 100644 index a020220a4..000000000 --- a/src/components/CommentEditForm/CommentEditForm.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { Button } from '@taskany/bricks'; - -import { useCommentResource } from '../../hooks/useCommentResource'; -import { CommentForm } from '../CommentForm/CommentForm'; -import { GoalCommentSchema } from '../../schema/goal'; - -import { tr } from './CommentEditForm.i18n'; - -interface CommentEditFormProps { - id: string; - description: string; - setFocus?: boolean; - - onUpdate: (comment?: { id: string; description: string }) => void; - onChange: (comment: { description: string }) => void; - onCancel: () => void; - onFocus?: () => void; -} - -const CommentEditForm: React.FC = ({ - id, - description: currentDescription, - onUpdate, - onChange, - onCancel, - onFocus, -}) => { - const { update } = useCommentResource(); - const [description, setDescription] = useState(currentDescription); - const [focused, setFocused] = useState(false); - const [busy, setBusy] = useState(false); - - const onCommentFocus = useCallback(() => { - setFocused(true); - onFocus?.(); - }, [onFocus]); - - const onCommentUpdate = useCallback( - (form: GoalCommentSchema) => { - setBusy(true); - setFocused(false); - - // optimistic update - onChange?.({ description: form.description }); - - update((comment) => { - setBusy(false); - setDescription(comment.description); - onUpdate?.(comment); - })(form); - }, - [update, onUpdate, onChange], - ); - - return ( - - } - /> - ); -}; - -export default CommentEditForm; diff --git a/src/components/CommentForm/CommentForm.tsx b/src/components/CommentForm/CommentForm.tsx index 15b88a476..e478629ac 100644 --- a/src/components/CommentForm/CommentForm.tsx +++ b/src/components/CommentForm/CommentForm.tsx @@ -1,11 +1,9 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import styled from 'styled-components'; -import { Controller, useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; import { backgroundColor, gapM, gray4 } from '@taskany/colors'; import { Button, Form, FormCard, FormAction, FormActions, nullable, useClickOutside } from '@taskany/bricks'; -import { GoalCommentSchema, goalCommentSchema } from '../../schema/goal'; +import { CommentSchema } from '../../schema/comment'; import { FormEditor } from '../FormEditor/FormEditor'; import { HelpButton } from '../HelpButton/HelpButton'; @@ -16,15 +14,10 @@ interface CommentFormProps { focused?: boolean; autoFocus?: boolean; busy?: boolean; - height?: number; - error?: React.ComponentProps['error']; - id?: string; - goalId?: string; - stateId?: string; description?: string; - onDescriptionChange: (description: string) => void; - onSubmit: (form: GoalCommentSchema) => void | Promise; + onSubmit: (form: CommentSchema) => void | Promise; + onChange?: (form: CommentSchema) => void; onFocus?: () => void; onCancel?: () => void; } @@ -60,77 +53,55 @@ const StyledCommentForm = styled(FormCard)` `; export const CommentForm: React.FC = ({ - id, - goalId, - stateId, description = '', autoFocus, focused, busy, - error, actionButton, - onDescriptionChange, + onChange, onSubmit, onFocus, onCancel, }) => { const ref = useRef(null); + const [newDescription, setNewDescription] = useState(description); - const { control, handleSubmit, register, watch } = useForm({ - resolver: zodResolver(goalCommentSchema), - mode: 'onChange', - reValidateMode: 'onChange', - shouldFocusError: true, - values: { - id, - goalId, - stateId, - description, + const onDescriptionChange = useCallback( + (descr = '') => { + setNewDescription(descr); + onChange?.({ description: descr }); }, - }); + [onChange], + ); - const descriptionWatcher = watch('description'); + const onCommentCancel = useCallback(() => { + setNewDescription(''); + onCancel?.(); + }, [onCancel]); - useEffect(() => { - onDescriptionChange(descriptionWatcher); - }, [descriptionWatcher, onDescriptionChange]); + const onCommentSubmit = useCallback(() => { + onSubmit?.({ description: newDescription }); + setNewDescription(''); + }, [onSubmit, newDescription]); useClickOutside(ref, () => { - if (!Object.values(control._fields).some((v) => v?._f.value)) { + if (newDescription === '') { onCancel?.(); } }); return ( -
- {nullable(id, () => ( - - ))} - - {nullable(goalId, () => ( - - ))} - - {nullable(stateId, () => ( - - ))} - - ( - - )} + + {nullable(focused, () => ( diff --git a/src/components/CommentView/CommentView.i18n/en.json b/src/components/CommentView/CommentView.i18n/en.json index 514d482f4..fb08e3c05 100644 --- a/src/components/CommentView/CommentView.i18n/en.json +++ b/src/components/CommentView/CommentView.i18n/en.json @@ -1,4 +1,5 @@ { "Edit": "Edit", - "Delete": "Delete" + "Delete": "Delete", + "Save": "Save" } diff --git a/src/components/CommentView/CommentView.i18n/ru.json b/src/components/CommentView/CommentView.i18n/ru.json index 13ef8ec85..f4d7a508a 100644 --- a/src/components/CommentView/CommentView.i18n/ru.json +++ b/src/components/CommentView/CommentView.i18n/ru.json @@ -1,4 +1,5 @@ { "Edit": "Изменить", - "Delete": "Удалить" + "Delete": "Удалить", + "Save": "Сохранить" } diff --git a/src/components/CommentView/CommentView.tsx b/src/components/CommentView/CommentView.tsx index 5e2e4e819..83440fefb 100644 --- a/src/components/CommentView/CommentView.tsx +++ b/src/components/CommentView/CommentView.tsx @@ -17,24 +17,26 @@ import { UserPic, nullable, PinAltIcon, + Button, } from '@taskany/bricks'; import { Reaction, State, User } from '@prisma/client'; import colorLayer from 'color-layer'; import { useReactionsResource } from '../../hooks/useReactionsResource'; -import { useCommentResource } from '../../hooks/useCommentResource'; import { usePageContext } from '../../hooks/usePageContext'; import { useLocale } from '../../hooks/useLocale'; +import { useDateViewType } from '../../hooks/useDateViewType'; import { createLocaleDate } from '../../utils/dateTime'; +import { CommentSchema } from '../../schema/comment'; import { Reactions } from '../Reactions'; import { ActivityFeedItem } from '../ActivityFeed'; import { RelativeTime } from '../RelativeTime/RelativeTime'; import { Circle, CircledIcon } from '../Circle'; +import { CommentForm } from '../CommentForm/CommentForm'; import { tr } from './CommentView.i18n'; const Md = dynamic(() => import('../Md')); -const CommentEditForm = dynamic(() => import('../CommentEditForm/CommentEditForm')); const ReactionsDropdown = dynamic(() => import('../ReactionsDropdown')); interface CommentViewProps { @@ -44,13 +46,15 @@ interface CommentViewProps { updatedAt?: Date; reactions?: Reaction[]; author?: User | null; - isNew?: boolean; - isEditable?: boolean; + highlight?: boolean; state?: State | null; - isPinned?: boolean; + pin?: boolean; onReactionToggle?: React.ComponentProps['onClick']; - onDelete?: (id: string) => void; + onSubmit?: (comment?: CommentSchema) => void; + onChange?: (comment: CommentSchema) => void; + onCancel?: () => void; + onDelete?: () => void; } const StyledCommentActions = styled.div` @@ -65,14 +69,14 @@ const StyledCommentActions = styled.div` } `; -const StyledCommentCard = styled(Card)<{ isNew?: boolean }>` +const StyledCommentCard = styled(Card)>` position: relative; min-height: 60px; transition: border-color 200ms ease-in-out; - ${({ isNew }) => - isNew && + ${({ highlight }) => + highlight && ` border-color: ${brandColor}; `} @@ -98,8 +102,8 @@ const StyledCommentCard = styled(Card)<{ isNew?: boolean }>` top: 8px; left: -6px; - ${({ isNew }) => - isNew && + ${({ highlight }) => + highlight && ` border-color: ${brandColor}; `} @@ -124,144 +128,134 @@ const StyledTimestamp = styled.div` gap: ${gapS}; `; -const renderTriggerHelper = ({ ref, onClick }: { ref: React.RefObject; onClick: () => void }) => ( - -); - -type MenuItemProps = React.ComponentProps; -type HelperItemProps = Pick & { - label: string; -}; - -const renderItemHelper = ({ item, cursor, index }: { item: HelperItemProps; cursor: number; index: number }) => ( - - {item.label} - -); - -const iconRenderCondition = (isPinned: boolean, image?: User['image'], email?: User['email']) => ( - - {isPinned ? ( - - ) : ( - - )} - -); - export const CommentView: FC = ({ id, author, description, createdAt, - isNew, - isEditable, + highlight, reactions, state, + pin, + onChange, + onCancel, + onSubmit, onDelete, onReactionToggle, - isPinned = false, }) => { const { themeId } = usePageContext(); const locale = useLocale(); - const { remove } = useCommentResource(); const [editMode, setEditMode] = useState(false); - const [commentDescription, setCommentDescription] = useState(description); + const [focused, setFocused] = useState(false); + const [busy, setBusy] = useState(false); + const [commentDescription, setCommentDescription] = useState({ description }); const { reactionsProps } = useReactionsResource(reactions); - const [isRelativeTime, setIsRelativeTime] = useState(true); + const { isRelative, onDateViewTypeChange } = useDateViewType(); - const onChangeTypeDate = (e: React.MouseEvent | undefined) => { - if (e && e.target === e.currentTarget) { - setIsRelativeTime(!isRelativeTime); + const onCommentDoubleClick = useCallback((e) => { + if (e.detail === 2) { + setEditMode(true); } - }; - - const onEditClick = useCallback(() => { - setEditMode(true); }, []); - const onDoubleCommentClick = useCallback( - (e) => { - if (isEditable && e.detail === 2) { - onEditClick(); + const onCommentSubmit = useCallback( + async (form: CommentSchema) => { + setBusy(true); + setFocused(false); + + onChange?.({ description: form.description }); + + // optimistic update + setCommentDescription({ description: form.description }); + try { + await onSubmit?.({ description: form.description }); + } catch (error) { + setCommentDescription({ description }); } - }, - [isEditable, onEditClick], - ); - const onUpdate = useCallback['onUpdate']>( - (comment) => { - setEditMode(false); - setCommentDescription(comment?.description || commentDescription); + setBusy(false); }, - [commentDescription], + [onSubmit, onChange, description], ); - const onChange = useCallback['onChange']>(({ description }) => { - setCommentDescription(description); - }, []); - - // FIXME: think twice about this - const onDeleteClick = useCallback(() => { - remove(({ id }) => { - id && onDelete?.(id); - })({ id }); - }, [id, onDelete, remove]); - const dropdownItems = useMemo( () => [ { label: tr('Edit'), icon: , - onClick: onEditClick, + onClick: () => setEditMode(true), }, { label: tr('Delete'), color: danger0, icon: , - onClick: onDeleteClick, + onClick: onDelete, }, ], - [onDeleteClick, onEditClick], + [onDelete], ); return ( - - {iconRenderCondition(isPinned, author?.image, author?.email)} + + + {pin ? ( + + ) : ( + + )} + {editMode ? ( - + } /> ) : ( - - + +
{author?.name} —{' '} - +
{nullable(!reactionsProps.limited, () => ( ))} - {nullable(isEditable, () => ( + {nullable(onSubmit, () => ( ( + + )} + renderItem={({ item, cursor, index }) => ( + + {item.label} + + )} /> ))} @@ -277,7 +271,7 @@ export const CommentView: FC = ({ ))} - {commentDescription} + {commentDescription.description} {nullable(reactions?.length, () => ( diff --git a/src/components/GoalActivity.tsx b/src/components/GoalActivity.tsx index 5c195304d..5bf4c2192 100644 --- a/src/components/GoalActivity.tsx +++ b/src/components/GoalActivity.tsx @@ -1,16 +1,11 @@ import React, { forwardRef } from 'react'; import { nullable } from '@taskany/bricks'; -import dynamic from 'next/dynamic'; -import { State } from '@prisma/client'; import { Priority } from '../types/priority'; -import { useHighlightedComment } from '../hooks/useHighlightedComment'; import { GoalByIdReturnType } from '../../trpc/inferredTypes'; import { HistoryAction } from '../types/history'; -import { useLSDraft } from '../hooks/useLSDraft'; import { ActivityFeed } from './ActivityFeed'; -import { CommentView } from './CommentView/CommentView'; import { HistoryRecord, HistoryRecordDependency, @@ -25,17 +20,12 @@ import { HistoryRecordCriteria, } from './HistoryRecord/HistoryRecord'; -const CommentCreateForm = dynamic(() => import('./CommentCreateForm/CommentCreateForm')); - interface GoalActivityProps { feed: NonNullable['_activityFeed']; - userId?: string | null; - onCommentReaction: (id: string) => (val?: string | undefined) => Promise; - onCommentPublish: (id?: string) => void; - onCommentDelete: (id?: string) => void; - goalId: string; - goalStates?: State[]; - children: React.ReactNode; + header?: React.ReactNode; + footer?: React.ReactNode; + + renderCommentItem: (item: NonNullable['comments'][number]) => React.ReactNode; } function excludeString(val: T): Exclude { @@ -43,50 +33,16 @@ function excludeString(val: T): Exclude { } export const GoalActivity = forwardRef( - ({ feed, onCommentReaction, onCommentPublish, userId, goalId, goalStates, onCommentDelete, children }, ref) => { - const { saveDraft, resolveDraft, removeDraft } = useLSDraft('draftGoalComment', {}); - const { highlightCommentId, setHighlightCommentId } = useHighlightedComment(); - - const draft = resolveDraft(goalId); - - const onPublish = (id?: string) => { - onCommentPublish(id); - setHighlightCommentId(id); - removeDraft(goalId); - }; - - const onCancel = () => { - removeDraft(goalId); - }; - - const onChange = (comment?: { stateId?: string; description?: string }) => { - if (!comment) { - removeDraft(goalId); - return; - } - saveDraft(goalId, comment); - }; - + ({ feed, header, footer, renderCommentItem }, ref) => { return ( - {children} + {header} + {feed.map((item) => nullable(item, ({ type, value }) => ( - {type === 'comment' && ( - - )} + {type === 'comment' && renderCommentItem(value)} + {type === 'history' && ( ( )), )} - + {footer} ); }, diff --git a/src/components/GoalPreview/GoalPreview.tsx b/src/components/GoalPreview/GoalPreview.tsx index 835b04192..030c42a5a 100644 --- a/src/components/GoalPreview/GoalPreview.tsx +++ b/src/components/GoalPreview/GoalPreview.tsx @@ -25,6 +25,11 @@ import { routes } from '../../hooks/router'; import { usePageContext } from '../../hooks/usePageContext'; import { useReactionsResource } from '../../hooks/useReactionsResource'; import { useCriteriaResource } from '../../hooks/useCriteriaResource'; +import { useCommentResource } from '../../hooks/useCommentResource'; +import { useHighlightedComment } from '../../hooks/useHighlightedComment'; +import { useDateViewType } from '../../hooks/useDateViewType'; +import { useLSDraft } from '../../hooks/useLSDraft'; +import { useGoalDependencyResource } from '../../hooks/useGoalDependencyResource'; import { dispatchModalEvent, ModalEvent } from '../../utils/dispatchModal'; import { GoalByIdReturnType } from '../../../trpc/inferredTypes'; import { editGoalKeys } from '../../utils/hotkeys'; @@ -39,7 +44,6 @@ import { GoalStateChangeSchema } from '../../schema/goal'; import { GoalActivity } from '../GoalActivity'; import { GoalCriteria } from '../GoalCriteria/GoalCriteria'; import { State } from '../State'; -import { useGoalDependencyResource } from '../../hooks/useGoalDependencyResource'; import { GoalDependencyAddForm } from '../GoalDependencyForm/GoalDependencyForm'; import { GoalDependencyListByKind } from '../GoalDependencyList/GoalDependencyList'; import { CommentView } from '../CommentView/CommentView'; @@ -51,6 +55,7 @@ const Md = dynamic(() => import('../Md')); const StateSwitch = dynamic(() => import('../StateSwitch')); const ModalOnEvent = dynamic(() => import('../ModalOnEvent')); const GoalEditForm = dynamic(() => import('../GoalEditForm/GoalEditForm')); +const CommentCreateForm = dynamic(() => import('../CommentCreateForm/CommentCreateForm')); interface GoalPreviewProps { shortId: string; @@ -95,14 +100,9 @@ const StyledCard = styled(Card)` export const GoalPreviewModal: React.FC = ({ shortId, onClose, onDelete, goal, defaults }) => { const { user } = usePageContext(); - const [isRelativeTime, setIsRelativeTime] = useState(true); - - const onChangeTypeDate = (e: React.MouseEvent | undefined) => { - if (e && e.target === e.currentTarget) { - setIsRelativeTime(!isRelativeTime); - } - }; + const { isRelative, onDateViewTypeChange } = useDateViewType(); + // --------------------------------------------------------------------------- goal actions const archiveMutation = trpc.goal.toggleArchive.useMutation(); const utils = trpc.useContext(); @@ -111,11 +111,13 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, }, [utils.goal.getById, shortId]); const [goalEditModalVisible, setGoalEditModalVisible] = useState(false); + const onGoalEdit = useCallback(() => { setGoalEditModalVisible(false); invalidateFn(); }, [invalidateFn]); + const onGoalEditModalShow = useCallback(() => { setGoalEditModalVisible(true); }, []); @@ -124,8 +126,6 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, setGoalEditModalVisible(false); }, []); - const { commentReaction } = useReactionsResource(goal?.reactions); - const stateChangeMutations = trpc.goal.switchState.useMutation(); const onGoalStateChange = useCallback( async (nextState: GoalStateChangeSchema['state']) => { @@ -141,18 +141,6 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, [goal, invalidateFn, stateChangeMutations], ); - const onCommentPublish = useCallback(() => { - invalidateFn(); - }, [invalidateFn]); - - const onCommentReactionToggle = useCallback( - (id: string) => commentReaction(id, () => utils.goal.getById.invalidate(shortId)), - [shortId, commentReaction, utils.goal.getById], - ); - const onCommentDelete = useCallback(() => { - invalidateFn(); - }, [invalidateFn]); - const onPreviewClose = useCallback(() => { setGoalEditModalVisible(false); onClose?.(); @@ -181,6 +169,99 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, const criteria = useCriteriaResource(invalidateFn); const dependency = useGoalDependencyResource(invalidateFn); + const { title, description, updatedAt } = goal || defaults; + const goalEditMenuItems = useMemo( + () => [ + { + label: tr('Edit'), + icon: , + onClick: dispatchModalEvent(ModalEvent.GoalEditModal), + }, + { + label: tr('Delete'), + color: danger0, + icon: , + onClick: dispatchModalEvent(ModalEvent.GoalDeleteModal), + }, + ], + [], + ); + + // --------------------------------------------------------------------------- comments actions + const { + saveDraft: saveCommentDraft, + resolveDraft: resolveCommentDraft, + removeDraft: removeCommentDraft, + } = useLSDraft('draftGoalComment', {}); + const commentDraft = resolveCommentDraft(goal?.id); + const { highlightCommentId, setHighlightCommentId } = useHighlightedComment(); + const { create: createComment, update: updateComment, remove: removeComment } = useCommentResource(); + const { commentReaction } = useReactionsResource(goal?.reactions); + + const onCommentChange = useCallback( + (comment?: { stateId?: string; description?: string }) => { + if (goal?.id) { + if (!comment?.description) { + removeCommentDraft(goal.id); + return; + } + + saveCommentDraft(goal?.id, comment); + } + }, + [goal?.id, removeCommentDraft, saveCommentDraft], + ); + + const onCommentCreate = useCallback( + async (comment?: { description: string; stateId?: string }) => { + if (comment && goal?.id) { + await createComment(({ id }) => { + invalidateFn(); + removeCommentDraft(goal.id); + setHighlightCommentId(id); + })({ + ...comment, + goalId: goal?.id, + }); + } + }, + [goal?.id, invalidateFn, removeCommentDraft, setHighlightCommentId, createComment], + ); + + const onCommentUpdate = useCallback( + (id: string) => async (comment?: { description: string }) => { + if (comment && goal?.id) { + await updateComment(() => { + invalidateFn(); + })({ + ...comment, + id, + }); + } + }, + [goal?.id, invalidateFn, updateComment], + ); + + const onCommentCancel = useCallback(() => { + if (goal?.id) { + removeCommentDraft(goal?.id); + } + }, [removeCommentDraft, goal?.id]); + + const onCommentReactionToggle = useCallback( + (id: string) => commentReaction(id, () => utils.goal.getById.invalidate(shortId)), + [shortId, commentReaction, utils.goal.getById], + ); + + const onCommentDelete = useCallback( + (id: string) => () => { + removeComment(() => { + invalidateFn(); + })({ id }); + }, + [invalidateFn, removeComment], + ); + const commentsRef = useRef(null); const contentRef = useRef(null); const headerRef = useRef(null); @@ -195,8 +276,6 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, }); }, []); - const { title, description, updatedAt } = goal || defaults; - const lastChangedStatusComment = useMemo(() => { if (!goal || goal.comments.length <= 1) { return null; @@ -204,8 +283,7 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, const foundResult = goal.comments.findLast((comment) => comment.stateId); return foundResult?.stateId === goal.stateId ? foundResult : null; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [goal?.comments, goal?.stateId]); + }, [goal]); return ( <> @@ -255,19 +333,7 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, {nullable(goal?._isEditable, () => ( , - onClick: dispatchModalEvent(ModalEvent.GoalEditModal), - }, - { - label: tr('Delete'), - color: danger0, - icon: , - onClick: dispatchModalEvent(ModalEvent.GoalDeleteModal), - }, - ]} + items={goalEditMenuItems} renderTrigger={({ ref, onClick }) => (