Skip to content

Commit 7669bff

Browse files
committed
enable for each deletion
1 parent e5003e8 commit 7669bff

File tree

7 files changed

+203
-12
lines changed

7 files changed

+203
-12
lines changed

packages/frontend/src/components/FlowStep/utils.ts

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

3-
import { isIfThenStep } from '@/helpers/toolbox'
3+
import { isForEachStep, isIfThenStep } from '@/helpers/toolbox'
44

55
function findAdjacentSteps(
66
steps: IStep[],
@@ -17,14 +17,35 @@ function findAdjacentSteps(
1717

1818
/**
1919
* NOTE: this function checks if we should create an empty step.
20+
* This applies to if-then and for-each groups.
2021
* The conditions are:
2122
* 1. The previous step is an if-then step and the next step is also an if-then step,
2223
* which means that the step being deleted is the 'last' step in the if-then group
2324
* 2. The previous step is an if-then step and there is no next step,
2425
* which means that the step being deleted is the 'last' step in the group
26+
* 3. The previous step is a for-each step and there is no next step
2527
*/
2628
function shouldCreateEmptyStep(prev?: IStep, next?: IStep): boolean {
27-
return !!prev && isIfThenStep(prev) && (!next || isIfThenStep(next))
29+
if (!prev) {
30+
return false
31+
}
32+
33+
// Condition 1: Previous is if-then AND next is if-then
34+
if (isIfThenStep(prev) && next && isIfThenStep(next)) {
35+
return true
36+
}
37+
38+
// Condition 2: Previous is if-then AND no next step
39+
if (isIfThenStep(prev) && !next) {
40+
return true
41+
}
42+
43+
// Condition 3: Previous is for-each AND no next step
44+
if (isForEachStep(prev) && !next) {
45+
return true
46+
}
47+
48+
return false
2849
}
2950

3051
export { findAdjacentSteps, shouldCreateEmptyStep }

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { IconButton } from '@opengovsg/design-system-react'
2727

2828
import FlowStep from '@/components/FlowStep'
2929
import { EditorContext } from '@/contexts/Editor'
30+
import { CREATE_STEP } from '@/graphql/mutations/create-step'
3031
import { DELETE_STEP } from '@/graphql/mutations/delete-step'
3132
import { GET_FLOW } from '@/graphql/queries/get-flow'
3233

@@ -38,12 +39,15 @@ import { branchStyles } from './styles'
3839
interface BranchProps {
3940
branchSteps: IStep[]
4041
stepsBeforeGroup: IStep[]
42+
groupedSteps: IStep[][]
4143
}
4244

4345
export default function Branch(props: BranchProps) {
44-
const { branchSteps, stepsBeforeGroup } = props
46+
const { branchSteps, stepsBeforeGroup, groupedSteps } = props
4547

4648
const {
49+
flow,
50+
hasForEach,
4751
isDrawerOpen,
4852
isMobile,
4953
readOnly: isEditorReadOnly,
@@ -58,6 +62,10 @@ export default function Branch(props: BranchProps) {
5862
onClose: closeDeleteConfirmation,
5963
} = useDisclosure()
6064
const cancelDeleteButton = useRef<HTMLButtonElement>(null)
65+
const [createStep, { loading: isCreatingStep }] = useMutation(CREATE_STEP, {
66+
fetchPolicy: 'no-cache',
67+
refetchQueries: [GET_FLOW],
68+
})
6169
const [deleteStep, { loading: isDeletingBranch }] = useMutation(DELETE_STEP, {
6270
refetchQueries: [GET_FLOW],
6371
})
@@ -74,15 +82,37 @@ export default function Branch(props: BranchProps) {
7482
variables: { input: { ids: idsToDelete } },
7583
})
7684

85+
// EDGE CASE: if the branch is the last step in a for-each action,
86+
// we should create an empty step after the for-each set up step
87+
if (
88+
hasForEach &&
89+
groupedSteps.length === 1 &&
90+
stepsBeforeGroup.length === 1
91+
) {
92+
await createStep({
93+
variables: {
94+
input: {
95+
previousStep: { id: stepsBeforeGroup[0]?.id },
96+
flow: { id: flow.id },
97+
},
98+
},
99+
})
100+
}
101+
77102
setCurrentStepId(null)
78103
closeDeleteConfirmation()
79104
onDrawerClose()
80105
}, [
81106
branchSteps,
82107
deleteStep,
83-
onDrawerClose,
84-
closeDeleteConfirmation,
108+
hasForEach,
109+
groupedSteps.length,
110+
stepsBeforeGroup,
85111
setCurrentStepId,
112+
closeDeleteConfirmation,
113+
onDrawerClose,
114+
createStep,
115+
flow.id,
86116
])
87117

88118
const canAddStep = useMemo(() => allowAddStep(branchSteps), [branchSteps])
@@ -121,8 +151,8 @@ export default function Branch(props: BranchProps) {
121151
aria-label="Delete branch"
122152
colorScheme="secondary"
123153
icon={<BiTrash />}
124-
isLoading={isDeletingBranch}
125-
isDisabled={isDeletingBranch}
154+
isLoading={isDeletingBranch || isCreatingStep}
155+
isDisabled={isDeletingBranch || isCreatingStep}
126156
/>
127157
</Flex>
128158
)}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export default function IfThen(props: IfThenProps): JSX.Element {
106106
key={branchSteps[0].id}
107107
branchSteps={branchSteps}
108108
stepsBeforeGroup={stepsBeforeGroup}
109+
groupedSteps={groupedSteps}
109110
/>
110111
)
111112
})}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { RefObject } from 'react'
2+
import {
3+
AlertDialog,
4+
AlertDialogBody,
5+
AlertDialogContent,
6+
AlertDialogFooter,
7+
AlertDialogHeader,
8+
AlertDialogOverlay,
9+
} from '@chakra-ui/react'
10+
import { Button } from '@opengovsg/design-system-react'
11+
12+
interface DeleteConfirmationDialogProps {
13+
cancelRef: RefObject<HTMLButtonElement>
14+
name: string
15+
isOpen: boolean
16+
onClose: () => void
17+
onCancel: () => void
18+
onDelete: () => void
19+
}
20+
21+
export default function DeleteConfirmationDialog(
22+
props: DeleteConfirmationDialogProps,
23+
) {
24+
const { cancelRef, name, isOpen, onClose, onDelete, onCancel } = props
25+
return (
26+
<AlertDialog
27+
isOpen={isOpen}
28+
leastDestructiveRef={cancelRef}
29+
onClose={onClose}
30+
>
31+
<AlertDialogOverlay>
32+
<AlertDialogContent>
33+
<AlertDialogHeader>Delete {name}</AlertDialogHeader>
34+
<AlertDialogBody>
35+
Are you sure you want to delete {name}? This action cannot be
36+
undone.
37+
</AlertDialogBody>
38+
<AlertDialogFooter>
39+
<Button
40+
colorScheme="neutral"
41+
variant="clear"
42+
ref={cancelRef}
43+
onClick={onCancel}
44+
>
45+
Cancel
46+
</Button>
47+
<Button colorScheme="critical" onClick={onDelete} ml={3}>
48+
Yes, delete {name}
49+
</Button>
50+
</AlertDialogFooter>
51+
</AlertDialogContent>
52+
</AlertDialogOverlay>
53+
</AlertDialog>
54+
)
55+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { IStep } from '@plumber/types'
2+
3+
import { MouseEventHandler, useCallback, useRef } from 'react'
4+
import { useMutation } from '@apollo/client'
5+
import { useDisclosure } from '@chakra-ui/react'
6+
7+
import { DELETE_STEP } from '@/graphql/mutations/delete-step'
8+
import { GET_FLOW } from '@/graphql/queries/get-flow'
9+
import { TOOLBOX_ACTIONS } from '@/helpers/toolbox'
10+
11+
const useDeleteConfirmation = (type: string, groupedSteps: IStep[][]) => {
12+
const { isOpen, onOpen, onClose } = useDisclosure()
13+
const cancelRef = useRef<HTMLButtonElement>(null)
14+
15+
const [deleteStep, { loading: isDeletingBranch }] = useMutation(DELETE_STEP, {
16+
refetchQueries: [GET_FLOW],
17+
})
18+
19+
const openDeleteConfirmation = useCallback<MouseEventHandler>(
20+
(e) => {
21+
e.stopPropagation()
22+
onOpen()
23+
},
24+
[onOpen],
25+
)
26+
27+
const onDelete = useCallback(async () => {
28+
if (type === TOOLBOX_ACTIONS.ForEach) {
29+
/**
30+
* deleting the entire for-each deletes the entire for-each loop
31+
* and all the steps inside it.
32+
*/
33+
const flatSteps = groupedSteps.flat()
34+
const idsToDelete = flatSteps.map((step) => step.id)
35+
await deleteStep({
36+
variables: { input: { ids: idsToDelete } },
37+
})
38+
onClose()
39+
} else {
40+
// TODO: refactor branch deletion
41+
}
42+
onClose()
43+
}, [deleteStep, groupedSteps, onClose, type])
44+
45+
return {
46+
cancelRef,
47+
isDeletingBranch,
48+
isOpen,
49+
onDelete,
50+
onOpen,
51+
onClose,
52+
openDeleteConfirmation,
53+
}
54+
}
55+
56+
export default useDeleteConfirmation

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

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

3-
import { useContext, useMemo } from 'react'
3+
import { useCallback, useContext, useMemo } from 'react'
44
import { BiTrash } from 'react-icons/bi'
55
import { Box, Flex, Icon, Text } from '@chakra-ui/react'
66
import { IconButton } from '@opengovsg/design-system-react'
@@ -11,9 +11,11 @@ import { TOOLBOX_ACTIONS } from '@/helpers/toolbox'
1111

1212
import { MIN_FLOW_STEP_WIDTH } from '../Editor/constants'
1313

14+
import DeleteConfirmationDialog from './components/DeleteConfirmationDialog'
1415
import Error from './Content/Error'
1516
import ForEach from './Content/ForEach'
1617
import IfThen from './Content/IfThen'
18+
import useDeleteConfirmation from './hooks/useDeleteConfirmation'
1719
import { flowStepGroupStyles } from './styles'
1820

1921
interface FlowStepGroupProps {
@@ -23,7 +25,7 @@ interface FlowStepGroupProps {
2325

2426
export default function FlowStepGroup(props: FlowStepGroupProps) {
2527
const { groupedSteps, stepsBeforeGroup } = props
26-
const { isDrawerOpen, isMobile } = useContext(EditorContext)
28+
const { isDrawerOpen, isMobile, onDrawerClose } = useContext(EditorContext)
2729

2830
const { stepGroupType, stepGroupCaption } = useMemo(() => {
2931
let stepGroupType: string | null = null
@@ -47,6 +49,19 @@ export default function FlowStepGroup(props: FlowStepGroupProps) {
4749
return { stepGroupType, stepGroupCaption }
4850
}, [groupedSteps])
4951

52+
const {
53+
isOpen: isDeleteConfirmationOpen,
54+
onOpen: openDeleteConfirmation,
55+
onClose: closeDeleteConfirmation,
56+
onDelete: deleteForEach,
57+
cancelRef,
58+
} = useDeleteConfirmation(stepGroupType ?? '', groupedSteps)
59+
60+
const handleForEachDelete = useCallback(async () => {
61+
await deleteForEach()
62+
onDrawerClose()
63+
}, [deleteForEach, onDrawerClose])
64+
5065
return (
5166
<Flex
5267
w="100%"
@@ -94,6 +109,7 @@ export default function FlowStepGroup(props: FlowStepGroupProps) {
94109
aria-label="Delete for each action"
95110
icon={<BiTrash />}
96111
colorScheme="secondary"
112+
onClick={openDeleteConfirmation}
97113
/>
98114
</Flex>
99115
)}
@@ -110,6 +126,14 @@ export default function FlowStepGroup(props: FlowStepGroupProps) {
110126
groupedSteps={groupedSteps}
111127
stepsBeforeGroup={stepsBeforeGroup}
112128
/>
129+
<DeleteConfirmationDialog
130+
name="For each"
131+
cancelRef={cancelRef}
132+
isOpen={isDeleteConfirmationOpen}
133+
onClose={closeDeleteConfirmation}
134+
onDelete={handleForEachDelete}
135+
onCancel={closeDeleteConfirmation}
136+
/>
113137
</>
114138
) : (
115139
<Error />

packages/frontend/src/contexts/Editor.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { GET_APPS } from '@/graphql/queries/get-apps'
2828
import { GET_FLOW } from '@/graphql/queries/get-flow'
2929
import { GET_TEST_EXECUTION_STEPS } from '@/graphql/queries/get-test-execution-steps'
3030
import {
31+
isForEachStep,
32+
isIfThenStep,
3133
TOOLBOX_ACTIONS,
3234
TOOLBOX_APP_KEY,
3335
useForEachInitializer,
@@ -44,6 +46,7 @@ interface IEditorContextValue {
4446
testExecutionSteps: IExecutionStep[]
4547
currentStepId: string | null
4648
currentStepIndex: number | null
49+
hasForEach: boolean
4750
hasIfThen: boolean
4851
currentTestExecutionStep: IExecutionStep | null
4952
isDrawerOpen: boolean
@@ -76,6 +79,7 @@ export const EditorContext = createContext<IEditorContextValue>({
7679
flowId: '',
7780
currentStepId: null,
7881
currentStepIndex: null,
82+
hasForEach: false,
7983
hasIfThen: false,
8084
currentTestExecutionStep: null,
8185
isDrawerOpen: false,
@@ -174,9 +178,8 @@ export const EditorProvider = ({
174178
const isEmptyPipe =
175179
steps.length <= 2 && steps.every((s) => s.key === null && s.appKey === null)
176180

177-
const hasIfThen = flow?.steps.some(
178-
(step: IStep) => step.key === TOOLBOX_ACTIONS.IfThen,
179-
)
181+
const hasForEach = flow?.steps.some((step) => isForEachStep(step))
182+
const hasIfThen = flow?.steps.some((step: IStep) => isIfThenStep(step))
180183

181184
const allApps = getAppsData?.getApps ?? []
182185

@@ -392,6 +395,7 @@ export const EditorProvider = ({
392395
allApps,
393396
currentStepId,
394397
currentStepIndex,
398+
hasForEach,
395399
hasIfThen,
396400
isDrawerOpen,
397401
isMobile,

0 commit comments

Comments
 (0)