From d81dc71766c9cd4bc993676f7b48c9d6bd9c25dc Mon Sep 17 00:00:00 2001 From: Maksim Sviridov Date: Thu, 25 May 2023 17:21:16 +0300 Subject: [PATCH] feat(GoalHistory): recordings changes feat(GoalHistory): for tags, title, desc, project, owner, priority --- .../GoalCreateForm/GoalCreateForm.tsx | 3 +- src/components/GoalEditForm/GoalEditForm.tsx | 6 +- src/components/GoalForm/GoalForm.tsx | 15 ++- src/components/GoalPage/GoalPage.tsx | 1 - src/components/GoalPreview/GoalPreview.tsx | 1 - .../IssueParticipantsForm.tsx | 4 +- src/schema/goal.ts | 93 ++++++--------- src/schema/goalHistory.ts | 11 -- trpc/router/goal.ts | 112 ++++++++++++++---- 9 files changed, 148 insertions(+), 98 deletions(-) delete mode 100644 src/schema/goalHistory.ts diff --git a/src/components/GoalCreateForm/GoalCreateForm.tsx b/src/components/GoalCreateForm/GoalCreateForm.tsx index 5516de108..6c05bdcc1 100644 --- a/src/components/GoalCreateForm/GoalCreateForm.tsx +++ b/src/components/GoalCreateForm/GoalCreateForm.tsx @@ -11,7 +11,7 @@ import { Tip } from '../Tip'; import { Keyboard } from '../Keyboard'; import { GoalForm } from '../GoalForm/GoalForm'; import { trpc } from '../../utils/trpcClient'; -import { GoalCommon } from '../../schema/goal'; +import { GoalCommon, goalCommonSchema } from '../../schema/goal'; import { ActivityByIdReturnType } from '../../../trpc/inferredTypes'; import { notifyPromise } from '../../utils/notifyPromise'; @@ -61,6 +61,7 @@ const GoalCreateForm: React.FC = () => { return ( = ({ goal, onSubmit }) => { const [busy, setBusy] = useState(false); const update = useGoalUpdate(goal.id); - const updateGoal = async (form: GoalCommon) => { + const updateGoal = async (form: GoalUpdate) => { setBusy(true); await update(form); @@ -28,6 +28,8 @@ const GoalEditForm: React.FC = ({ goal, onSubmit }) => { // FIXME: nullable values are conflicting with undefined return ( void; + onSumbit: (fields: z.infer) => void; } const StyledTagsContainer = styled.div<{ focused?: boolean }>` @@ -65,6 +67,7 @@ const StyledTagsContainer = styled.div<{ focused?: boolean }>` export const GoalForm: React.FC = ({ formTitle, actionBtnText, + id, title, description, owner, @@ -74,6 +77,7 @@ export const GoalForm: React.FC = ({ priority, estimate, busy, + validityScheme, children, onSumbit, }) => { @@ -96,8 +100,8 @@ export const GoalForm: React.FC = ({ setFocus, setValue, formState: { errors, isValid, isSubmitted }, - } = useForm({ - resolver: zodResolver(goalCommonSchema), + } = useForm>({ + resolver: zodResolver(validityScheme), mode: 'onChange', reValidateMode: 'onChange', shouldFocusError: false, @@ -110,11 +114,12 @@ export const GoalForm: React.FC = ({ priority, estimate, tags, + id, }, }); const parentWatcher = watch('parent'); - const tagsWatcher = watch('tags'); + const tagsWatcher: TagModel[] = watch('tags'); const errorsResolver = errorsProvider(errors, isSubmitted); useEffect(() => { diff --git a/src/components/GoalPage/GoalPage.tsx b/src/components/GoalPage/GoalPage.tsx index 62af8c60b..3d7924a9b 100644 --- a/src/components/GoalPage/GoalPage.tsx +++ b/src/components/GoalPage/GoalPage.tsx @@ -37,7 +37,6 @@ import { IssueParent } from '../IssueParent'; import { IssueTags } from '../IssueTags'; import { getPriorityText } from '../PriorityText/PriorityText'; import { useHighlightedComment } from '../../hooks/useHighlightedComment'; -import { useGoalUpdate } from '../../hooks/useGoalUpdate'; import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useWillUnmount } from '../../hooks/useWillUnmount'; import { ActivityFeed } from '../ActivityFeed'; diff --git a/src/components/GoalPreview/GoalPreview.tsx b/src/components/GoalPreview/GoalPreview.tsx index 77de0aee7..c9a91aa0e 100644 --- a/src/components/GoalPreview/GoalPreview.tsx +++ b/src/components/GoalPreview/GoalPreview.tsx @@ -24,7 +24,6 @@ import { import { refreshInterval } from '../../utils/config'; import { formatEstimate } from '../../utils/dateTime'; import { useHighlightedComment } from '../../hooks/useHighlightedComment'; -import { useGoalUpdate } from '../../hooks/useGoalUpdate'; import { routes } from '../../hooks/router'; import { usePageContext } from '../../hooks/usePageContext'; import { useReactionsResource } from '../../hooks/useReactionsResource'; diff --git a/src/components/IssueParticipantsForm/IssueParticipantsForm.tsx b/src/components/IssueParticipantsForm/IssueParticipantsForm.tsx index 867b0df7b..b7ca62bfa 100644 --- a/src/components/IssueParticipantsForm/IssueParticipantsForm.tsx +++ b/src/components/IssueParticipantsForm/IssueParticipantsForm.tsx @@ -44,7 +44,7 @@ export const IssueParticipantsForm: React.FC = ({ pa (id: string) => { activities.delete(id); - onChange?.(Array.from(activities).map(([_, p]) => p)); + onChange?.(Array.from(activities.values())); }, [activities, onChange], ); @@ -58,7 +58,7 @@ export const IssueParticipantsForm: React.FC = ({ pa setQuery(''); - onChange?.(Array.from(activities).map(([_, p]) => p)); + onChange?.(Array.from(activities.values())); }, [activities, onChange], ); diff --git a/src/schema/goal.ts b/src/schema/goal.ts index c9c5650e2..9ac84b4f0 100644 --- a/src/schema/goal.ts +++ b/src/schema/goal.ts @@ -99,62 +99,47 @@ export const goalUpdateSchema = z.object({ }) .min(10, { message: tr("Goal's description must be longer than 10 symbols"), - }) - .optional(), - description: z - .string({ - required_error: tr("Goal's description is required"), - invalid_type_error: tr("Goal's description must be a string"), - }) - .optional(), - owner: z - .object({ + }), + description: z.string({ + required_error: tr("Goal's description is required"), + invalid_type_error: tr("Goal's description must be a string"), + }), + owner: z.object({ + id: z.string(), + user: z.object({ + nickname: z.string().nullable(), + name: z.string().nullable(), + email: z.string(), + }), + }), + parent: z.object( + { id: z.string(), - }) - .optional(), - parent: z - .object( - { - id: z.string(), - title: z.string(), - flowId: z.string(), - }, - { - invalid_type_error: tr("Goal's project or team are required"), - required_error: tr("Goal's project or team are required"), - }, - ) - .optional(), - state: z - .object({ + title: z.string(), + flowId: z.string(), + }, + { + invalid_type_error: tr("Goal's project or team are required"), + required_error: tr("Goal's project or team are required"), + }, + ), + state: z.object({ + id: z.string(), + hue: z.number().optional(), + title: z.string().optional(), + }), + priority: z.string().nullable(), + estimate: z.object({ + date: z.string(), + q: z.string(), + y: z.string(), + }), + tags: z.array( + z.object({ id: z.string(), - hue: z.number().optional(), - title: z.string().optional(), - }) - .optional(), - priority: z.string().nullable().optional(), - estimate: z - .object({ - date: z.string(), - q: z.string(), - y: z.string(), - }) - .optional(), - tags: z - .array( - z.object({ - id: z.string(), - title: z.string(), - }), - ) - .optional(), - participants: z - .array( - z.object({ - id: z.string(), - }), - ) - .optional(), + title: z.string(), + }), + ), }); export type GoalUpdate = z.infer; diff --git a/src/schema/goalHistory.ts b/src/schema/goalHistory.ts deleted file mode 100644 index 336b4ab6c..000000000 --- a/src/schema/goalHistory.ts +++ /dev/null @@ -1,11 +0,0 @@ -import z from 'zod'; - -export const goalHistorySchema = z.object({ - goalId: z.string(), - action: z.enum(['edit', 'remove', 'add', 'delete', 'archive', 'change']), - subject: z.enum(['title', 'description', 'participants', 'state', 'tags', 'dependencies']), - nextValue: z.string(), - previousValue: z.string().optional(), -}); - -export type GoalHistory = z.infer; diff --git a/trpc/router/goal.ts b/trpc/router/goal.ts index a45e50b4f..85e023cc2 100644 --- a/trpc/router/goal.ts +++ b/trpc/router/goal.ts @@ -1,5 +1,6 @@ import z from 'zod'; import { TRPCError } from '@trpc/server'; +import { Tag } from '@prisma/client'; import { prisma } from '../../src/utils/prisma'; import { protectedProcedure, router } from '../trpcBackend'; @@ -10,6 +11,7 @@ import { goalParticipantsSchema, goalStateChangeSchema, goalUpdateSchema, + GoalUpdate, toogleGoalArchiveSchema, toogleGoalDependencySchema, userGoalsSchema, @@ -290,22 +292,93 @@ export const goal = router({ update: protectedProcedure.input(goalUpdateSchema).mutation(async ({ ctx, input }) => { const actualGoal = await prisma.goal.findUnique({ where: { id: input.id }, - include: { participants: true, project: true, tags: true }, + include: { + participants: true, + project: true, + tags: true, + owner: { + include: { + user: true, + }, + }, + }, }); if (!actualGoal) return null; - // FIXME: move out to separated mutations - let participantsToDisconnect: Array<{ id: string }> = []; - let tagsToDisconnect: Array<{ id: string }> = []; + const tagsToDisconnect: Array = + actualGoal.tags.filter((t) => !input.tags.some((tag) => tag.id === t.id)) || []; + const tagsToConnect: GoalUpdate['tags'] = + input.tags.filter((t) => !actualGoal.tags.some((tag) => tag.id === t.id)) || []; + + const history = []; + + if (actualGoal.title !== input.title) { + history.push({ + subject: 'title', + action: 'change', + previousValue: actualGoal.title, + nextValue: input.title, + }); + } + + if (actualGoal.description !== input.description) { + history.push({ + subject: 'description', + action: 'change', + previousValue: actualGoal.description, + nextValue: input.description, + }); + } + + if (tagsToDisconnect.length) { + history.push({ + subject: 'tags', + action: 'remove', + nextValue: tagsToDisconnect.map(({ title }) => title).join(', '), + }); + } + + if (tagsToConnect.length) { + history.push({ + subject: 'tags', + action: 'add', + nextValue: tagsToConnect.map(({ title }) => title).join(', '), + }); + } - if (input.participants?.length) { - participantsToDisconnect = - actualGoal.participants?.filter((p) => !input.participants?.some((pa) => pa.id === p.id)) || []; + if (actualGoal.priority !== input.priority) { + history.push({ + subject: 'priority', + action: 'change', + previousValue: actualGoal.priority, + nextValue: input.priority, + }); } - if (input.tags?.length) { - tagsToDisconnect = actualGoal.tags?.filter((t) => !input.tags?.some((tag) => tag.id === t.id)) || []; + // TODO: after FIXME statement below + // if (actualGoal.estimate) + + if (actualGoal.ownerId !== input.owner.id) { + history.push({ + subject: 'owner', + action: 'change', + previousValue: + actualGoal.owner?.user?.nickname ?? + actualGoal.owner?.user?.name ?? + actualGoal.owner?.user?.email ?? + '', + nextValue: input.owner?.user?.nickname ?? input.owner?.user?.name ?? input.owner?.user?.email ?? '', + }); + } + + if (actualGoal.projectId !== input.parent.id) { + history.push({ + subject: 'project', + action: 'change', + previuosValue: actualGoal.project?.id, + nextValue: input.parent.id, + }); } try { @@ -327,18 +400,15 @@ export const goal = router({ }, } : undefined, - tags: input.tags?.length - ? { - connect: input.tags.map((t) => ({ id: t.id })), - disconnect: tagsToDisconnect, - } - : undefined, - participants: input.participants?.length - ? { - connect: input.participants.map((p) => ({ id: p.id })), - disconnect: participantsToDisconnect, - } - : undefined, + tags: { + connect: tagsToConnect.map(({ id }) => ({ id })), + disconnect: tagsToDisconnect.map(({ id }) => ({ id })), + }, + history: { + createMany: { + data: history.map((record) => ({ ...record, activityId: ctx.session.user.activityId })), + }, + }, }, });