Skip to content

Commit d92c158

Browse files
committed
chore: allow ifthen and foreach in different branches
1 parent 0a42ced commit d92c158

File tree

5 files changed

+90
-96
lines changed

5 files changed

+90
-96
lines changed

packages/frontend/src/components/FlowStep/components/DuplicateStepButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default function DuplicateStepButton(props: DuplicateStepButtonProps) {
2323
const { flow, isMobile, onDrawerOpen, setCurrentStepId } =
2424
useContext(EditorContext)
2525

26+
// TODO: use onCreateStep from EditorContext instead
2627
const [createStep] = useMutation(CREATE_STEP, { refetchQueries: [GET_FLOW] })
2728
const onDuplicateStep = useCallback(async () => {
2829
const duplicateConfig = {
Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { IFlow, IStep } from '@plumber/types'
1+
import { IStep } from '@plumber/types'
22

33
import { useContext } from 'react'
44

55
import { BranchContext } from '@/components/FlowStepGroup/Content/IfThen/BranchContext'
66
import { NESTED_IFTHEN_FEATURE_FLAG } from '@/config/flags'
7-
import { EditorContext } from '@/contexts/Editor'
87
import { LaunchDarklyContext } from '@/contexts/LaunchDarkly'
8+
import { StepsToDisplayContext } from '@/contexts/StepsToDisplay'
99
import { TOOLBOX_ACTIONS } from '@/helpers/toolbox'
1010

1111
/**
@@ -16,68 +16,26 @@ import { TOOLBOX_ACTIONS } from '@/helpers/toolbox'
1616
*
1717
*/
1818
function isDelaySelectable({
19-
flow,
19+
hasForEach,
20+
groupedSteps,
2021
step,
2122
prevStepId,
2223
}: {
23-
flow: IFlow
24+
hasForEach: boolean
25+
groupedSteps: IStep[][]
2426
step?: IStep
2527
prevStepId?: string
2628
}) {
27-
const forEachStepPosition = flow?.steps.find(
28-
(s) => s.key === TOOLBOX_ACTIONS.ForEach,
29-
)?.position
30-
31-
let prevStepPosition = null
32-
if (step) {
33-
prevStepPosition = step.position
34-
} else if (prevStepId) {
35-
prevStepPosition = flow?.steps?.find((s) => s.id === prevStepId)?.position
36-
}
37-
38-
let isDelaySelectable: boolean
39-
if (!forEachStepPosition) {
40-
isDelaySelectable = true
41-
} else if (forEachStepPosition && prevStepPosition) {
42-
isDelaySelectable = prevStepPosition < forEachStepPosition
43-
} else {
44-
isDelaySelectable = false
29+
if (!hasForEach) {
30+
return true
4531
}
46-
47-
return isDelaySelectable
48-
}
49-
50-
/**
51-
* Helper function to check if For-each action should be selectable; supports edge
52-
* case in ChooseEvent component.
53-
*
54-
* For-each should only be selectable if:
55-
* - We're the last step.
56-
* - We are not inside a for-each action.
57-
* - We are not inside an if-then action.
58-
* - There is no if-then action in the flow,
59-
*
60-
* Using many consts as purpose of the conditions may not be immediately
61-
* apparent.
62-
*/
63-
function isForEachSelectable({
64-
flow,
65-
hasIfThen,
66-
isLastStep,
67-
}: {
68-
flow: IFlow
69-
hasIfThen: boolean
70-
isLastStep: boolean
71-
}) {
72-
const hasForEach = flow?.steps?.some(
73-
(step: IStep) => step.key === TOOLBOX_ACTIONS.ForEach,
74-
)
75-
76-
if (hasForEach || hasIfThen || !isLastStep) {
32+
const isStepWithinForEach = groupedSteps.flat().some((groupedStep) => {
33+
if (step?.id === groupedStep.id || prevStepId === groupedStep.id) {
34+
return true
35+
}
7736
return false
78-
}
79-
80-
return true
37+
})
38+
return !isStepWithinForEach
8139
}
8240

8341
/**
@@ -116,6 +74,7 @@ function isIfThenSelectable({
11674
}
11775

11876
const isNestedBranch = depth > 0
77+
11978
return !isNestedBranch
12079
}
12180

@@ -129,21 +88,31 @@ export const useIsAppSelectable = ({
12988
prevStepId?: string
13089
}): Record<string, boolean> => {
13190
const { depth } = useContext(BranchContext)
132-
const { flow, hasIfThen } = useContext(EditorContext)
91+
const { groupedSteps } = useContext(StepsToDisplayContext)
13392
const { getFlagValue } = useContext(LaunchDarklyContext)
13493

94+
const hasIfThen = groupedSteps.some((group) =>
95+
group.some((step) => step.key === TOOLBOX_ACTIONS.IfThen),
96+
)
97+
98+
const hasForEach = groupedSteps.some((group) =>
99+
group.some((step) => step.key === TOOLBOX_ACTIONS.ForEach),
100+
)
101+
135102
return {
136103
[TOOLBOX_ACTIONS.IfThen]: isIfThenSelectable({
137104
depth,
138105
hasIfThen,
139106
isLastStep,
140107
getFlagValue,
141108
}),
142-
[TOOLBOX_ACTIONS.ForEach]: isForEachSelectable({
143-
flow,
144-
hasIfThen,
145-
isLastStep,
146-
}),
147-
delay: isDelaySelectable({ flow, step, prevStepId }),
109+
/**
110+
* For-each should only be selectable if:
111+
* - We're the last step.
112+
* - There's no other for-each action
113+
* - There's no other if-then action
114+
*/
115+
[TOOLBOX_ACTIONS.ForEach]: isLastStep && !hasIfThen && !hasForEach,
116+
delay: isDelaySelectable({ hasForEach, groupedSteps, step, prevStepId }),
148117
}
149118
}

packages/frontend/src/components/FlowStepGroup/Content/IfThen/index.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import { Flex } from '@chakra-ui/react'
77
import { Button } from '@opengovsg/design-system-react'
88

99
import { EditorContext } from '@/contexts/Editor'
10+
import { MrfContext } from '@/contexts/MrfContext'
1011
import { CREATE_STEP } from '@/graphql/mutations/create-step'
1112
import { GET_FLOW } from '@/graphql/queries/get-flow'
13+
import { getMrfApprovalConfig } from '@/helpers/formsg'
1214
import { TOOLBOX_ACTIONS, TOOLBOX_APP_KEY } from '@/helpers/toolbox'
1315

1416
import Branch from './Branch'
@@ -24,6 +26,7 @@ export default function IfThen(props: IfThenProps): JSX.Element {
2426
const { groupedSteps, stepsBeforeGroup } = props
2527

2628
const { depth } = useContext(BranchContext)
29+
const { approvalBranches } = useContext(MrfContext)
2730
const {
2831
flowId,
2932
readOnly: isEditorReadOnly,
@@ -50,6 +53,11 @@ export default function IfThen(props: IfThenProps): JSX.Element {
5053
const lastGroup = groupedSteps[groupedSteps.length - 1]
5154
const lastStep = lastGroup[lastGroup.length - 1]
5255

56+
const approvalConfig = getMrfApprovalConfig({
57+
previousStep: lastStep,
58+
approvalBranches,
59+
})
60+
5361
const branchStep = await createStep({
5462
variables: {
5563
input: {
@@ -65,6 +73,9 @@ export default function IfThen(props: IfThenProps): JSX.Element {
6573
depth,
6674
branchName: `Branch ${numBranches + 1}`,
6775
},
76+
config: {
77+
approval: approvalConfig,
78+
},
6879
},
6980
},
7081
})
@@ -80,6 +91,9 @@ export default function IfThen(props: IfThenProps): JSX.Element {
8091
flow: {
8192
id: flowId,
8293
},
94+
config: {
95+
approval: approvalConfig,
96+
},
8397
},
8498
},
8599
})
@@ -92,6 +106,7 @@ export default function IfThen(props: IfThenProps): JSX.Element {
92106
createStep,
93107
flowId,
94108
depth,
109+
approvalBranches,
95110
numBranches,
96111
onDrawerOpen,
97112
setCurrentStepId,

packages/frontend/src/hooks/useReorderSteps.ts

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

33
import { useCallback } from 'react'
44
import { useMutation } from '@apollo/client'
55
import { useToast } from '@opengovsg/design-system-react'
66

77
import {
8-
StepConfigInput,
98
StepEnumType,
109
StepPositionInput,
1110
} from '@/graphql/__generated__/graphql'
@@ -33,68 +32,81 @@ const useReorderSteps = (flowId: string) => {
3332
}): StepPositionInput[] => {
3433
// we start from 2 because the first step is the trigger step and not part of the sortable list
3534
let nextPosition = 2
36-
let currentApprovalconfig: IStepApprovalConfig | null = null
35+
let currentApprovalBranch: {
36+
stepId: string
37+
branch: 'approve' | 'reject'
38+
} | null = null
3739
const allReorderedSteps: StepPositionInput[] = []
38-
reorderedSteps.forEach((step) => {
39-
const isMrfStep = mrfSteps.some((mrfStep) => mrfStep.id === step.id)
40+
reorderedSteps.forEach((reorderingStep) => {
41+
const isMrfStep = mrfSteps.some(
42+
(mrfStep) => mrfStep.id === reorderingStep.id,
43+
)
4044

4145
if (isMrfStep) {
4246
// if the previous approval config is in the approve branch
4347
// we need to add the steps in the reject branch that was hidden
44-
if (currentApprovalconfig?.branch === 'approve') {
48+
if (currentApprovalBranch?.branch === 'approve') {
4549
const stepsInRejectBranch = allSteps.filter(
4650
(step) =>
4751
step.config?.approval?.branch === 'reject' &&
48-
step.config?.approval?.stepId === currentApprovalconfig?.stepId,
52+
step.config?.approval?.stepId === currentApprovalBranch?.stepId,
4953
) as IStep[]
5054
stepsInRejectBranch.forEach((step) => {
5155
allReorderedSteps.push({
5256
id: step.id,
5357
position: nextPosition++,
5458
type: step.type as StepEnumType,
55-
config: {
56-
approval: step.config.approval,
57-
} as StepConfigInput,
5859
})
5960
})
6061
}
61-
currentApprovalconfig = null
62+
currentApprovalBranch = null
6263
}
6364

6465
// add the current step to the list
6566
allReorderedSteps.push({
66-
id: step.id,
67+
id: reorderingStep.id,
6768
position: nextPosition++,
68-
type: step.type as StepEnumType,
69-
config: {
70-
approval: currentApprovalconfig,
71-
} as StepConfigInput,
69+
type: reorderingStep.type as StepEnumType,
70+
config:
71+
currentApprovalBranch?.branch === 'reject'
72+
? {
73+
approval: currentApprovalBranch,
74+
}
75+
: { approval: null }, // this sets it to approval branch
7276
})
7377

7478
const isMrfApprovalStep = mrfApprovalSteps.some(
75-
(mrfApprovalStep) => mrfApprovalStep.id === step.id,
79+
(mrfApprovalStep) => mrfApprovalStep.id === reorderingStep.id,
7680
)
7781
if (isMrfApprovalStep) {
78-
currentApprovalconfig = {
79-
branch: approvalBranches[step.id],
80-
stepId: step.id,
82+
currentApprovalBranch = {
83+
branch: approvalBranches[reorderingStep.id],
84+
stepId: reorderingStep.id,
8185
}
8286
// if the current approval config is in the reject branch
8387
// 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[]
88+
if (currentApprovalBranch?.branch === 'reject') {
89+
const reorderingStepIndex = allSteps.findIndex(
90+
(step) => step.id === reorderingStep.id,
91+
)
92+
const stepsInApproveBranch = []
93+
// Search for non-mrf steps directly after the approval step
94+
for (let i = reorderingStepIndex + 1; i < allSteps.length; i++) {
95+
const step = allSteps[i]
96+
if (
97+
!step.config?.approval &&
98+
!mrfSteps.some((mrfStep) => mrfStep.id === step.id)
99+
) {
100+
stepsInApproveBranch.push(step)
101+
} else {
102+
break
103+
}
104+
}
90105
stepsInApproveBranch.forEach((step) => {
91106
allReorderedSteps.push({
92107
id: step.id,
93108
position: nextPosition++,
94109
type: step.type as StepEnumType,
95-
config: {
96-
approval: step.config.approval,
97-
} as StepConfigInput,
98110
})
99111
})
100112
}

packages/frontend/src/hooks/useStepMetadata.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,8 @@ export function useStepMetadata(
119119
break
120120
}
121121
}
122-
// if is approval step, return whether
122+
// this means that the step is after the trigger step
123123
if (!immediatePriorMrfStep) {
124-
console.warn(
125-
'No immediate prior mrf step found... this should not happen tho',
126-
)
127124
return null
128125
}
129126
/**

0 commit comments

Comments
 (0)