Skip to content

Commit 09e5433

Browse files
committed
feat: reorder step across approval branches
1 parent 6c72d47 commit 09e5433

File tree

4 files changed

+181
-37
lines changed

4 files changed

+181
-37
lines changed

packages/backend/src/graphql/mutations/update-step-positions.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { IStep } from '@plumber/types'
22

3+
import { PartialModelObject, raw } from 'objection'
4+
35
import { BadUserInputError } from '@/errors/graphql-errors'
6+
import logger from '@/helpers/logger'
47
import Step from '@/models/step'
58

69
import type { MutationResolvers } from '../__generated__/types.generated'
@@ -76,9 +79,17 @@ const updateStepPositions: MutationResolvers['updateStepPositions'] = async (
7679

7780
// Patch each step individually with its new position
7881
for (const stepPosition of stepPositions) {
79-
await Step.query(trx).findById(stepPosition.id).patch({
82+
const patchData: PartialModelObject<Step> = {
8083
position: stepPosition.position,
81-
})
84+
}
85+
if (stepPosition.config) {
86+
patchData.config = stepPosition.config.approval
87+
? raw(`jsonb_set(config, '{approval}', ?::jsonb, true)`, [
88+
JSON.stringify(stepPosition.config?.approval),
89+
])
90+
: raw(`config - 'approval'`)
91+
}
92+
await Step.query(trx).findById(stepPosition.id).patch(patchData)
8293
}
8394

8495
// Update the flow's lastUpdatedAt timestamp
@@ -90,6 +101,21 @@ const updateStepPositions: MutationResolvers['updateStepPositions'] = async (
90101
.withGraphFetched('steps')
91102
.orderBy('steps.position', 'asc')
92103

104+
// sanity check that all step positions are contiguous
105+
const contiguousPositions = updatedFlow.steps.map((step) => step.position)
106+
if (
107+
!contiguousPositions.every((position, index) => position === index + 1)
108+
) {
109+
logger.error({
110+
message: 'Updated positions are no longer contiguous',
111+
stepPositions,
112+
flowId: flow.id,
113+
})
114+
throw new BadUserInputError(
115+
'Failed to update: updated positions are no longer contiguous',
116+
)
117+
}
118+
93119
return updatedFlow.steps
94120
})
95121

packages/backend/src/graphql/schema.graphql

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ input StepPositionInput {
578578
id: String!
579579
position: Int!
580580
type: StepEnumType!
581+
config: StepConfigInput
581582
}
582583

583584
input UpdateStepPositionsInput {
@@ -683,8 +684,8 @@ type StepTemplateConfig {
683684
}
684685

685686
type StepApprovalConfig {
686-
branch: String
687-
stepId: String
687+
branch: String!
688+
stepId: String!
688689
}
689690

690691
input StepConnectionInput {

packages/frontend/src/components/Editor/components/StepsList.tsx

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { IStep } from '@plumber/types'
22

3-
import { useContext } from 'react'
3+
import { useCallback, useContext } from 'react'
44
import { Center, Flex } from '@chakra-ui/react'
55

66
import PrimarySpinner from '@/components/PrimarySpinner'
77
import { SortableList } from '@/components/SortableList'
88
import { EditorContext } from '@/contexts/Editor'
9+
import { MrfContext } from '@/contexts/MrfContext'
910
import { StepsToDisplayContext } from '@/contexts/StepsToDisplay'
1011
import { FlowStepGroup } from '@/exports/components'
11-
import { StepEnumType } from '@/graphql/__generated__/graphql'
1212
import { TOOLBOX_ACTIONS } from '@/helpers/toolbox'
1313
import useReorderSteps from '@/hooks/useReorderSteps'
1414

@@ -30,26 +30,43 @@ export function StepsList({ isNested }: StepsListProps) {
3030
groupingActions,
3131
} = useContext(StepsToDisplayContext)
3232
const { flow, isDrawerOpen, isMobile, readOnly } = useContext(EditorContext)
33+
const { mrfSteps, mrfApprovalSteps, approvalBranches } =
34+
useContext(MrfContext)
3335

34-
const { handleReorderUpdate } = useReorderSteps(flow.id)
36+
const { calculateReorderedSteps, handleReorderUpdate } = useReorderSteps(
37+
flow.id,
38+
)
3539

36-
const handleReorderSteps = async (reorderedSteps: IStep[]) => {
37-
const stepPositions = reorderedSteps.map((step, index) => ({
38-
id: step.id,
39-
position: index + 2, // trigger position is 1
40-
type: step.type as StepEnumType,
41-
}))
40+
const handleReorderSteps = useCallback(
41+
async (reorderedSteps: IStep[]) => {
42+
const allSteps = flow.steps
43+
const allReorderedSteps = calculateReorderedSteps({
44+
reorderedSteps,
45+
allSteps,
46+
mrfSteps,
47+
mrfApprovalSteps,
48+
approvalBranches,
49+
})
4250

43-
try {
44-
await handleReorderUpdate(stepPositions)
45-
} catch (error) {
46-
console.error(
47-
'Error updating step positions: ',
48-
error,
49-
JSON.stringify(stepPositions),
50-
)
51-
}
52-
}
51+
try {
52+
await handleReorderUpdate(allReorderedSteps)
53+
} catch (error) {
54+
console.error(
55+
'Error updating step positions: ',
56+
error,
57+
JSON.stringify(allReorderedSteps),
58+
)
59+
}
60+
},
61+
[
62+
flow.steps,
63+
calculateReorderedSteps,
64+
mrfSteps,
65+
mrfApprovalSteps,
66+
approvalBranches,
67+
handleReorderUpdate,
68+
],
69+
)
5370

5471
const nonIfThenActionSteps = actionStepsBeforeGroup.filter(
5572
(step) => step.key !== TOOLBOX_ACTIONS.IfThen,

packages/frontend/src/hooks/useReorderSteps.ts

Lines changed: 114 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,114 @@
1-
import { IStep } from '@plumber/types'
1+
import { IStep, IStepApprovalBranch, IStepApprovalConfig } from '@plumber/types'
22

3+
import { useCallback } from 'react'
34
import { useMutation } from '@apollo/client'
45
import { useToast } from '@opengovsg/design-system-react'
56

6-
import { StepEnumType } from '@/graphql/__generated__/graphql'
7+
import {
8+
StepConfigInput,
9+
StepEnumType,
10+
StepPositionInput,
11+
} from '@/graphql/__generated__/graphql'
712
import { UPDATE_STEP_POSITIONS } from '@/graphql/mutations/update-step-positions'
813
import { GET_FLOW } from '@/graphql/queries/get-flow'
914

10-
interface StepPositionInput {
11-
id: string
12-
position: number
13-
type: StepEnumType
14-
}
15-
1615
const useReorderSteps = (flowId: string) => {
1716
const toast = useToast()
1817
const [updateStepPositions] = useMutation(UPDATE_STEP_POSITIONS)
1918

19+
// TODO: write unit test for this function
20+
const calculateReorderedSteps = useCallback(
21+
({
22+
reorderedSteps,
23+
allSteps,
24+
mrfSteps,
25+
mrfApprovalSteps,
26+
approvalBranches,
27+
}: {
28+
reorderedSteps: IStep[]
29+
allSteps: IStep[]
30+
mrfSteps: IStep[]
31+
mrfApprovalSteps: IStep[]
32+
approvalBranches: { [stepId: string]: IStepApprovalBranch }
33+
}): StepPositionInput[] => {
34+
// we start from 2 because the first step is the trigger step and not part of the sortable list
35+
let nextPosition = 2
36+
let currentApprovalconfig: IStepApprovalConfig | null = null
37+
const allReorderedSteps: StepPositionInput[] = []
38+
reorderedSteps.forEach((step) => {
39+
const isMrfStep = mrfSteps.some((mrfStep) => mrfStep.id === step.id)
40+
41+
if (isMrfStep) {
42+
// if the previous approval config is in the approve branch
43+
// we need to add the steps in the reject branch that was hidden
44+
if (currentApprovalconfig?.branch === 'approve') {
45+
const stepsInRejectBranch = allSteps.filter(
46+
(step) =>
47+
step.config?.approval?.branch === 'reject' &&
48+
step.config?.approval?.stepId === currentApprovalconfig?.stepId,
49+
) as IStep[]
50+
stepsInRejectBranch.forEach((step) => {
51+
allReorderedSteps.push({
52+
id: step.id,
53+
position: nextPosition++,
54+
type: step.type as StepEnumType,
55+
config: {
56+
approval: step.config.approval,
57+
} as StepConfigInput,
58+
})
59+
})
60+
}
61+
currentApprovalconfig = null
62+
}
63+
64+
// add the current step to the list
65+
allReorderedSteps.push({
66+
id: step.id,
67+
position: nextPosition++,
68+
type: step.type as StepEnumType,
69+
config: {
70+
approval: currentApprovalconfig,
71+
} as StepConfigInput,
72+
})
73+
74+
const isMrfApprovalStep = mrfApprovalSteps.some(
75+
(mrfApprovalStep) => mrfApprovalStep.id === step.id,
76+
)
77+
if (isMrfApprovalStep) {
78+
currentApprovalconfig = {
79+
branch: approvalBranches[step.id],
80+
stepId: step.id,
81+
}
82+
// if the current approval config is in the reject branch
83+
// we need to add the steps in the approve branch that was hidden
84+
if (currentApprovalconfig?.branch === 'reject') {
85+
const stepsInApproveBranch = allSteps.filter(
86+
(step) =>
87+
step.config?.approval?.branch === 'approve' &&
88+
step.config?.approval?.stepId === currentApprovalconfig?.stepId,
89+
) as IStep[]
90+
stepsInApproveBranch.forEach((step) => {
91+
allReorderedSteps.push({
92+
id: step.id,
93+
position: nextPosition++,
94+
type: step.type as StepEnumType,
95+
config: {
96+
approval: step.config.approval,
97+
} as StepConfigInput,
98+
})
99+
})
100+
}
101+
}
102+
})
103+
104+
// all later steps not in the sortable list need not be updated since
105+
// reordering of visible steps will not affect them
106+
107+
return allReorderedSteps
108+
},
109+
[],
110+
)
111+
20112
const handleReorderUpdate = async (stepPositions: StepPositionInput[]) => {
21113
try {
22114
await updateStepPositions({
@@ -25,6 +117,7 @@ const useReorderSteps = (flowId: string) => {
25117
updateStepPositions: stepPositions.map((sp) => ({
26118
id: sp.id,
27119
position: sp.position,
120+
config: sp.config ? { approval: sp.config.approval } : undefined,
28121
__typename: 'Step' as const,
29122
})),
30123
},
@@ -37,15 +130,22 @@ const useReorderSteps = (flowId: string) => {
37130

38131
if (flow) {
39132
// Create a map of step positions for quick lookup
40-
const positionMap = new Map(
41-
stepPositions.map((sp) => [sp.id, sp.position]),
133+
const updatedStepMap = new Map(
134+
stepPositions.map((sp) => [
135+
sp.id,
136+
{ position: sp.position, config: sp.config },
137+
]),
42138
)
43139

44140
// Update steps with new positions
45141
const updatedSteps = flow.steps.map((step: IStep) => {
46-
const newPosition = positionMap.get(step.id)
47-
return newPosition !== undefined
48-
? { ...step, position: newPosition }
142+
const updatedStep = updatedStepMap.get(step.id)
143+
return updatedStep !== undefined
144+
? {
145+
...step,
146+
position: updatedStep.position,
147+
config: { ...step.config, ...updatedStep.config },
148+
}
49149
: step
50150
})
51151

@@ -91,7 +191,7 @@ const useReorderSteps = (flowId: string) => {
91191
}
92192
}
93193

94-
return { handleReorderUpdate }
194+
return { handleReorderUpdate, calculateReorderedSteps }
95195
}
96196

97197
export default useReorderSteps

0 commit comments

Comments
 (0)