From f876d7e27247d13db241da087bf5964beaf49d27 Mon Sep 17 00:00:00 2001 From: Tony Vi Date: Tue, 8 Aug 2023 15:26:23 +0300 Subject: [PATCH] refactor: useGoalCommentsActions for GoalPreview and GoalPage --- src/components/GoalPage/GoalPage.tsx | 183 +++++++++++++-------- src/components/GoalPreview/GoalPreview.tsx | 119 +++----------- src/hooks/useGoalCommentsActions.ts | 121 ++++++++++++++ 3 files changed, 262 insertions(+), 161 deletions(-) create mode 100644 src/hooks/useGoalCommentsActions.ts diff --git a/src/components/GoalPage/GoalPage.tsx b/src/components/GoalPage/GoalPage.tsx index 5a9ee6901..d2ef5b8f5 100644 --- a/src/components/GoalPage/GoalPage.tsx +++ b/src/components/GoalPage/GoalPage.tsx @@ -27,11 +27,13 @@ import { IssueStats } from '../IssueStats/IssueStats'; import { IssueParent } from '../IssueParent'; import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useWillUnmount } from '../../hooks/useWillUnmount'; -import { useReactionsResource } from '../../hooks/useReactionsResource'; import { WatchButton } from '../WatchButton/WatchButton'; import { useGoalResource } from '../../hooks/useGoalResource'; -import { StarButton } from '../StarButton/StarButton'; +import { useGoalCommentsActions } from '../../hooks/useGoalCommentsActions'; +import { useCriteriaResource } from '../../hooks/useCriteriaResource'; +import { useGoalDependencyResource } from '../../hooks/useGoalDependencyResource'; import { useRouter } from '../../hooks/router'; +import { StarButton } from '../StarButton/StarButton'; import { GoalDeleteModal } from '../GoalDeleteModal/GoalDeleteModal'; import { trpc } from '../../utils/trpcClient'; import { GoalStateChangeSchema } from '../../schema/goal'; @@ -40,8 +42,6 @@ import { notifyPromise } from '../../utils/notifyPromise'; import { ActivityByIdReturnType, GoalAchiveCriteria, GoalDependencyItem } from '../../../trpc/inferredTypes'; import { GoalActivity } from '../GoalActivity'; import { GoalCriteria } from '../GoalCriteria/GoalCriteria'; -import { useCriteriaResource } from '../../hooks/useCriteriaResource'; -import { useGoalDependencyResource } from '../../hooks/useGoalDependencyResource'; import { RelativeTime } from '../RelativeTime/RelativeTime'; import { IssueMeta } from '../IssueMeta'; import { UserBadge } from '../UserBadge'; @@ -52,6 +52,8 @@ import { GoalParentComboBox } from '../GoalParentComboBox'; import { GoalDependencyListByKind } from '../GoalDependencyList/GoalDependencyList'; import { GoalDependencyAddForm } from '../GoalDependencyForm/GoalDependencyForm'; import { useGoalPreview } from '../GoalPreview/GoalPreviewProvider'; +import CommentCreateForm from '../CommentCreateForm/CommentCreateForm'; +import { CommentView } from '../CommentView/CommentView'; import { tr } from './GoalPage.i18n'; @@ -122,8 +124,6 @@ export const GoalPage = ({ user, ssrTime, params: { id } }: ExternalPageProps<{ const { toggleGoalWatching, toggleGoalStar } = useGoalResource(goal?.id, goal?._shortId); - const { commentReaction } = useReactionsResource(goal?.reactions); - const invalidateFn = useCallback(() => { return utils.goal.getById.invalidate(id); }, [id, utils.goal.getById]); @@ -164,18 +164,6 @@ export const GoalPage = ({ user, ssrTime, params: { id } }: ExternalPageProps<{ [goal, invalidateFn, removeParticipantMutation], ); - const onCommentPublish = useCallback(() => { - invalidateFn(); - }, [invalidateFn]); - - const onCommentReactionToggle = useCallback( - (id: string) => commentReaction(id, () => utils.goal.getById.invalidate(id)), - [commentReaction, utils.goal.getById], - ); - const onCommentDelete = useCallback(() => { - invalidateFn(); - }, [invalidateFn]); - const onGoalEdit = useCallback(() => { dispatchModalEvent(ModalEvent.GoalEditModal)(); invalidateFn(); @@ -220,11 +208,6 @@ export const GoalPage = ({ user, ssrTime, params: { id } }: ExternalPageProps<{ }) .join(''); - const commentsRef = useRef(null); - const onCommentsClick = useCallback(() => { - commentsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }, []); - const criteria = useCriteriaResource(invalidateFn); const dependency = useGoalDependencyResource(invalidateFn); @@ -259,6 +242,31 @@ export const GoalPage = ({ user, ssrTime, params: { id } }: ExternalPageProps<{ [setPreview], ); + const { + highlightCommentId, + lastStateComment, + resolveCommentDraft, + onCommentChange, + onCommentCreate, + onCommentUpdate, + onCommentDelete, + onCommentCancel, + onCommentReactionToggle, + } = useGoalCommentsActions({ + id: goal?.id, + shortId: goal?._shortId, + stateId: goal?.stateId, + reactions: goal?.reactions, + comments: goal?.comments, + cb: invalidateFn, + }); + + const commentDraft = resolveCommentDraft(); + const commentsRef = useRef(null); + const onCommentsClick = useCallback(() => { + commentsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, []); + if (!goal || !owner || !issuer) return null; const participantsFilter = goal.participants.map(({ id }) => id).concat([owner.id, issuer.id]); @@ -341,53 +349,94 @@ export const GoalPage = ({ user, ssrTime, params: { id } }: ExternalPageProps<{ {nullable(goal, ({ _activityFeed, id, goalAchiveCriteria, _relations, _isEditable }) => ( - {nullable(goalAchiveCriteria.length || _isEditable, () => ( - - ))} - - {_relations.map((deps, depIdx) => - nullable(deps.goals.length || _isEditable, () => ( - - {nullable(_isEditable, () => ( - + {nullable(lastStateComment, (value) => ( + + ))} + {nullable(goalAchiveCriteria.length || _isEditable, () => ( + + ))} + + {_relations.map((deps, depIdx) => + nullable(deps.goals.length || _isEditable, () => ( + - ))} - - )), + key={deps.kind} + kind={deps.kind} + items={deps.goals} + canEdit={_isEditable} + onRemove={dependency.onRemoveHandler} + onClick={onGoalDependencyClick} + > + {nullable(_isEditable, () => ( + + ))} + + )), + )} + + } + footer={ + + } + renderCommentItem={(value) => ( + )} - + /> ))} diff --git a/src/components/GoalPreview/GoalPreview.tsx b/src/components/GoalPreview/GoalPreview.tsx index 030c42a5a..3e876a3a1 100644 --- a/src/components/GoalPreview/GoalPreview.tsx +++ b/src/components/GoalPreview/GoalPreview.tsx @@ -23,13 +23,10 @@ import { 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 { useClickSwitch } from '../../hooks/useClickSwitch'; import { useGoalDependencyResource } from '../../hooks/useGoalDependencyResource'; +import { useGoalCommentsActions } from '../../hooks/useGoalCommentsActions'; import { dispatchModalEvent, ModalEvent } from '../../utils/dispatchModal'; import { GoalByIdReturnType } from '../../../trpc/inferredTypes'; import { editGoalKeys } from '../../utils/hotkeys'; @@ -98,11 +95,10 @@ const StyledCard = styled(Card)` min-height: 60px; `; -export const GoalPreviewModal: React.FC = ({ shortId, onClose, onDelete, goal, defaults }) => { +export const GoalPreviewModal: React.FC = ({ shortId, goal, defaults, onClose, onDelete }) => { const { user } = usePageContext(); - const { isRelative, onDateViewTypeChange } = useDateViewType(); + const [isRelative, onDateViewTypeChange] = useClickSwitch(); - // --------------------------------------------------------------------------- goal actions const archiveMutation = trpc.goal.toggleArchive.useMutation(); const utils = trpc.useContext(); @@ -187,85 +183,29 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, [], ); - // --------------------------------------------------------------------------- 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], - ); - + highlightCommentId, + lastStateComment, + resolveCommentDraft, + onCommentChange, + onCommentCreate, + onCommentUpdate, + onCommentDelete, + onCommentCancel, + onCommentReactionToggle, + } = useGoalCommentsActions({ + id: goal?.id, + shortId: goal?._shortId, + stateId: goal?.stateId, + reactions: goal?.reactions, + comments: goal?.comments, + cb: invalidateFn, + }); + + const commentDraft = resolveCommentDraft(); const commentsRef = useRef(null); const contentRef = useRef(null); const headerRef = useRef(null); - const onCommentsClick = useCallback(() => { commentsRef.current && contentRef.current && @@ -276,15 +216,6 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, }); }, []); - const lastChangedStatusComment = useMemo(() => { - if (!goal || goal.comments.length <= 1) { - return null; - } - - const foundResult = goal.comments.findLast((comment) => comment.stateId); - return foundResult?.stateId === goal.stateId ? foundResult : null; - }, [goal]); - return ( <> @@ -384,7 +315,7 @@ export const GoalPreviewModal: React.FC = ({ shortId, onClose, feed={_activityFeed} header={ <> - {nullable(lastChangedStatusComment, (value) => ( + {nullable(lastStateComment, (value) => ( = ({ shortId, onClose, onDelete={onCommentDelete(value.id)} /> )} - > + /> ))} diff --git a/src/hooks/useGoalCommentsActions.ts b/src/hooks/useGoalCommentsActions.ts new file mode 100644 index 000000000..f36e81f45 --- /dev/null +++ b/src/hooks/useGoalCommentsActions.ts @@ -0,0 +1,121 @@ +import { useCallback, useMemo } from 'react'; + +import { trpc } from '../utils/trpcClient'; +import { GoalByIdReturnType } from '../../trpc/inferredTypes'; + +import { useLSDraft } from './useLSDraft'; +import { useHighlightedComment } from './useHighlightedComment'; +import { useCommentResource } from './useCommentResource'; +import { useReactionsResource } from './useReactionsResource'; + +export const useGoalCommentsActions = ({ + id, + shortId, + stateId, + reactions, + comments, + cb, +}: { + id?: string; + shortId?: string; + stateId?: string | null; + reactions?: NonNullable['reactions']; + comments?: NonNullable['comments']; + cb: () => void; +}) => { + const { + saveDraft: saveCommentDraft, + resolveDraft: resolveCommentDraft, + removeDraft: removeCommentDraft, + } = useLSDraft('draftGoalComment', {}); + const { highlightCommentId, setHighlightCommentId } = useHighlightedComment(); + const { create: createComment, update: updateComment, remove: removeComment } = useCommentResource(); + const { commentReaction } = useReactionsResource(reactions); + + const utils = trpc.useContext(); + + const onCommentChange = useCallback( + (comment?: { stateId?: string; description?: string }) => { + if (id) { + if (!comment?.description) { + removeCommentDraft(id); + return; + } + + saveCommentDraft(id, comment); + } + }, + [id, removeCommentDraft, saveCommentDraft], + ); + + const onCommentCreate = useCallback( + async (comment?: { description: string; stateId?: string }) => { + if (comment && id) { + await createComment(({ id }) => { + cb(); + removeCommentDraft(id); + setHighlightCommentId(id); + })({ + ...comment, + goalId: id, + }); + } + }, + [id, cb, removeCommentDraft, setHighlightCommentId, createComment], + ); + + const onCommentUpdate = useCallback( + (commentId: string) => async (comment?: { description: string }) => { + if (comment && commentId) { + await updateComment(() => { + cb(); + })({ + ...comment, + id: commentId, + }); + } + }, + [cb, updateComment], + ); + + const onCommentCancel = useCallback(() => { + if (id) { + removeCommentDraft(id); + } + }, [removeCommentDraft, id]); + + const onCommentReactionToggle = useCallback( + (id: string) => commentReaction(id, () => utils.goal.getById.invalidate(shortId)), + [shortId, commentReaction, utils.goal.getById], + ); + + const onCommentDelete = useCallback( + (id: string) => () => { + removeComment(() => { + cb(); + })({ id }); + }, + [cb, removeComment], + ); + + const lastStateComment = useMemo(() => { + if ((comments?.length ?? 0) <= 1) { + return null; + } + + const foundResult = comments?.findLast((comment) => comment.stateId); + return foundResult?.stateId === stateId ? foundResult : null; + }, [comments, stateId]); + + return { + highlightCommentId, + lastStateComment, + resolveCommentDraft, + onCommentChange, + onCommentCreate, + onCommentUpdate, + onCommentDelete, + onCommentCancel, + onCommentReactionToggle, + }; +};