diff --git a/packages/frontend/src/components/Editor/FlowStepWithAddButton.tsx b/packages/frontend/src/components/Editor/FlowStepWithAddButton.tsx
index 2163b3ce68..f1f7256962 100644
--- a/packages/frontend/src/components/Editor/FlowStepWithAddButton.tsx
+++ b/packages/frontend/src/components/Editor/FlowStepWithAddButton.tsx
@@ -1,10 +1,6 @@
import { IStep } from '@plumber/types'
-import { useContext, useMemo } from 'react'
-
-import { EditorContext } from '@/contexts/Editor'
import { FlowStep } from '@/exports/components'
-import { TOOLBOX_ACTIONS } from '@/helpers/toolbox'
import { useStepMetadata } from '@/hooks/useStepMetadata'
import { ApproveReject } from '../FlowStep/components/ApproveReject'
@@ -15,50 +11,24 @@ export default function FlowStepWithAddButton({
step,
isLastStep,
isNested,
- stepsBeforeGroup,
- groupedSteps,
- showAddButton = true,
+ addButtonProps: {
+ isHidden = false,
+ isDisabled = false,
+ showEmptyAction = false,
+ },
}: {
step: IStep
isLastStep: boolean
isNested?: boolean
stepsBeforeGroup: IStep[]
groupedSteps: IStep[][]
- showAddButton?: boolean
+ addButtonProps: {
+ isHidden: boolean
+ isDisabled: boolean
+ showEmptyAction: boolean
+ }
}) {
- const { readOnly, allApps } = useContext(EditorContext)
-
- const { isApprovalStep } = useStepMetadata(allApps, step)
-
- const nonIfThenActionSteps = stepsBeforeGroup.filter(
- (step) => step.type === 'action' && step.key !== TOOLBOX_ACTIONS.IfThen,
- )
-
- // Disables last add step and hide in-between add step buttons
- const hasExactlyOneEmptyActionStep =
- nonIfThenActionSteps.length === 1 && !nonIfThenActionSteps[0].appKey
-
- // Disables last add step button but show empty action instead
- const hasNoActionSteps = nonIfThenActionSteps.length === 0
-
- const getAddStepButtonProps = useMemo(() => {
- const shouldShowEmptyAction = hasNoActionSteps && !groupedSteps.length
- const shouldDisableButton =
- (hasExactlyOneEmptyActionStep || hasNoActionSteps) && !groupedSteps.length
-
- return (isLastStep: boolean, stepId: string) => ({
- isHidden: readOnly,
- showEmptyAction: shouldShowEmptyAction,
- isDisabled: shouldDisableButton,
- isLastStep,
- stepId,
- })
- }, [
- readOnly,
- hasNoActionSteps,
- groupedSteps.length,
- hasExactlyOneEmptyActionStep,
- ])
+ const { isApprovalStep } = useStepMetadata(step)
return (
<>
@@ -67,13 +37,16 @@ export default function FlowStepWithAddButton({
isLastStep={isLastStep}
isNested={isNested}
// only allow reordering if there are more than 1 action steps
- allowReorder={nonIfThenActionSteps.length > 1}
+ allowReorder={true}
/>
{isApprovalStep && }
-
- {showAddButton && (
-
- )}
+
>
)
}
diff --git a/packages/frontend/src/components/Editor/index.tsx b/packages/frontend/src/components/Editor/index.tsx
index b3a7415808..19af6fcd37 100644
--- a/packages/frontend/src/components/Editor/index.tsx
+++ b/packages/frontend/src/components/Editor/index.tsx
@@ -7,6 +7,7 @@ import EditorRightDrawer from '@/components/EditorRightDrawer'
import FlowStepGroup from '@/components/FlowStepGroup'
import { SortableList } from '@/components/SortableList'
import { EditorContext } from '@/contexts/Editor'
+import { MrfContextProvider } from '@/contexts/MrfContext'
import { StepExecutionsToIncludeProvider } from '@/contexts/StepExecutionsToInclude'
import { StepEnumType } from '@/graphql/__generated__/graphql'
import { extractBranchesWithSteps, TOOLBOX_ACTIONS } from '@/helpers/toolbox'
@@ -25,7 +26,7 @@ type EditorProps = {
export default function Editor(props: EditorProps): React.ReactElement {
const { isNested } = props
- const { allApps, isDrawerOpen, isMobile, currentStepId, flow } =
+ const { allApps, isDrawerOpen, isMobile, flow, readOnly, currentStepId } =
useContext(EditorContext)
const { handleReorderUpdate } = useReorderSteps(flow.id)
@@ -43,6 +44,10 @@ export default function Editor(props: EditorProps): React.ReactElement {
[flow, rawSteps],
)
+ const currentStep = useMemo(() => {
+ return steps.find((step) => step.id === currentStepId)
+ }, [currentStepId, steps])
+
const appsWithActions: IApp[] = allApps.filter(
(app: IApp) => !!app.actions?.length,
)
@@ -61,7 +66,7 @@ export default function Editor(props: EditorProps): React.ReactElement {
)
}, [appsWithActions])
- const [triggerStep, stepsBeforeGroup, groupedSteps] = useMemo(() => {
+ const [triggerStep, actionStepsBeforeGroup, groupedSteps] = useMemo(() => {
if (!groupingActions) {
return [null, [], []]
}
@@ -92,42 +97,6 @@ export default function Editor(props: EditorProps): React.ReactElement {
: [triggerStep, steps.slice(1, groupStepIdx), branchesWithSteps]
}, [groupingActions, steps])
- const flowStepGroupIconUrl = useMemo(() => {
- if (groupedSteps.length === 0) {
- return undefined
- }
- return appsWithActions.find((app) => app.key === groupedSteps[0][0].appKey)
- ?.iconUrl
- }, [appsWithActions, groupedSteps])
-
- //
- // Compute which steps are eligible for variable extraction.
- // Mainly for if-then branches where we do not want to include steps
- // from other branches.
- //
- // Note:
- // - we include some grouped steps as there is no longer a nested editor
- // - we identify the group by checking if the current step id is in the group
- // - for-each steps are always included
- const groupStepsToInclude = useMemo(() => {
- return groupedSteps.flatMap((group) =>
- group.some((step) => step.id === currentStepId) ||
- group.some((step) => step.key === TOOLBOX_ACTIONS.ForEach)
- ? group
- : [],
- )
- }, [currentStepId, groupedSteps])
-
- const stepExecutionsToInclude = useMemo(
- () =>
- new Set([
- ...(triggerStep?.id ? [triggerStep.id] : []),
- ...stepsBeforeGroup.map((step) => step.id),
- ...groupStepsToInclude.map((s) => s.id),
- ]),
- [triggerStep, stepsBeforeGroup, groupStepsToInclude],
- )
-
const handleReorderSteps = async (reorderedSteps: IStep[]) => {
const stepPositions = reorderedSteps.map((step, index) => ({
id: step.id,
@@ -146,6 +115,21 @@ export default function Editor(props: EditorProps): React.ReactElement {
}
}
+ const nonIfThenActionSteps = actionStepsBeforeGroup.filter(
+ (step) => step.key !== TOOLBOX_ACTIONS.IfThen,
+ )
+
+ // Disables last add step and hide in-between add step buttons
+ const hasExactlyOneEmptyActionStep =
+ nonIfThenActionSteps.length === 1 && !nonIfThenActionSteps[0].appKey
+
+ // Disables last add step button but show empty action instead
+ const hasNoActionSteps = nonIfThenActionSteps.length === 0
+ const shouldShowEmptyAction = hasNoActionSteps && !groupedSteps.length
+ // for backwards compatibility where empty step is created
+ const shouldDisableAddButton =
+ (hasExactlyOneEmptyActionStep || hasNoActionSteps) && !groupedSteps.length
+
if (!appsWithActions || !groupingActions) {
return (
@@ -172,80 +156,92 @@ export default function Editor(props: EditorProps): React.ReactElement {
backgroundSize: '30px 30px',
}}
>
-
-
+
- {triggerStep && (
-
+ {triggerStep && (
+
+ )}
+
+ {
+ const { id, position } = step
+ return (
+
+
+
+
+
+ )
+ }}
/>
- )}
-
- {
- const { id, position } = step
- return (
-
-
-
-
-
- )
- }}
- />
- {groupedSteps.length > 0 && (
-
- )}
-
- {/** HACKFIX (kevinkim-ogp): to ensure that the transitions are smooth */}
-
-
- 0 && (
+
+ )}
+
+ {/** HACKFIX (kevinkim-ogp): to ensure that the transitions are smooth */}
+
-
-
+
+
+
+
+
)
}
diff --git a/packages/frontend/src/components/EditorRightDrawer/Step.tsx b/packages/frontend/src/components/EditorRightDrawer/Step.tsx
index a52df7a4eb..5677804cb3 100644
--- a/packages/frontend/src/components/EditorRightDrawer/Step.tsx
+++ b/packages/frontend/src/components/EditorRightDrawer/Step.tsx
@@ -1,14 +1,13 @@
import type { IStep } from '@plumber/types'
-import { useCallback, useContext, useMemo } from 'react'
-import { CircularProgress, Flex, useDisclosure } from '@chakra-ui/react'
+import { Fragment, useCallback, useContext, useMemo } from 'react'
+import { Flex, useDisclosure } from '@chakra-ui/react'
import ChooseConnectionSubstep from '@/components/ChooseConnectionSubstep'
import FlowSubstep from '@/components/FlowSubstep'
import Form from '@/components/Form'
import { EditorContext } from '@/contexts/Editor'
import { StepExecutionsProvider } from '@/contexts/StepExecutions'
-import { StepExecutionsToIncludeContext } from '@/contexts/StepExecutionsToInclude'
import { generateValidationSchema } from '@/helpers/editor'
import { useStepMetadata } from '@/hooks/useStepMetadata'
@@ -18,11 +17,10 @@ import LearnFromGuideInfobox from './LearnFromGuideInfobox'
type StepProps = {
step: IStep
- isLastStep: boolean
}
export default function Step(props: StepProps): React.ReactElement | null {
- const { step, isLastStep } = props
+ const { step } = props
const {
isOpen: isModalOpen,
@@ -30,23 +28,10 @@ export default function Step(props: StepProps): React.ReactElement | null {
onClose: onModalClose,
} = useDisclosure()
- const { allApps, onUpdateStep, testExecutionSteps, resetTimestamp } =
- useContext(EditorContext)
-
- // This includes all steps that run even after the current step, but within the same branch.
- const stepExecutionsToInclude = useContext(StepExecutionsToIncludeContext)
- const priorExecutionSteps = useMemo(
- () =>
- testExecutionSteps.filter(
- (stepExecution) =>
- stepExecutionsToInclude?.has(stepExecution.stepId) &&
- stepExecution.step.position < step.position,
- ),
- [step.position, stepExecutionsToInclude, testExecutionSteps],
- )
+ const { onUpdateStep, resetTimestamp } = useContext(EditorContext)
const { app, hasConnection, isTrigger, selectedActionOrTrigger, substeps } =
- useStepMetadata(allApps, step)
+ useStepMetadata(step)
const handleSubmit = useCallback(
(val: any) => {
@@ -60,14 +45,10 @@ export default function Step(props: StepProps): React.ReactElement | null {
[substeps],
)
- if (!allApps) {
- return
- }
-
return (
<>
-
+
)
diff --git a/packages/frontend/src/components/FlowStep/index.tsx b/packages/frontend/src/components/FlowStep/index.tsx
index 329ded9891..baaf45dadc 100644
--- a/packages/frontend/src/components/FlowStep/index.tsx
+++ b/packages/frontend/src/components/FlowStep/index.tsx
@@ -77,14 +77,7 @@ export default function FlowStep(
substeps,
shouldShowDragHandle,
isDeletable,
- } = useStepMetadata(
- allApps,
- step,
- readOnly,
- allowReorder,
- isMobile,
- isDrawerOpen,
- )
+ } = useStepMetadata(step, true)
const {
cancelRef,
diff --git a/packages/frontend/src/components/FlowStepGroup/Content/IfThen/HoverAddStepButton.tsx b/packages/frontend/src/components/FlowStepGroup/Content/IfThen/HoverAddStepButton.tsx
index 33efbacffd..ae905c185b 100644
--- a/packages/frontend/src/components/FlowStepGroup/Content/IfThen/HoverAddStepButton.tsx
+++ b/packages/frontend/src/components/FlowStepGroup/Content/IfThen/HoverAddStepButton.tsx
@@ -40,16 +40,8 @@ export function HoverAddStepButton(
const { isOpen, onOpen, onClose } = useDisclosure()
const [isHovered, setIsHovered] = useState(false)
- const { allApps, readOnly, isMobile, isDrawerOpen } =
- useContext(EditorContext)
- const { shouldShowDragHandle } = useStepMetadata(
- allApps,
- step,
- readOnly,
- allowReorder,
- isMobile,
- isDrawerOpen,
- )
+ const { readOnly, isDrawerOpen } = useContext(EditorContext)
+ const { shouldShowDragHandle } = useStepMetadata(step, allowReorder)
const {
cancelRef,
diff --git a/packages/frontend/src/components/FlowStepGroup/components/GroupStepWithAddButton.tsx b/packages/frontend/src/components/FlowStepGroup/components/GroupStepWithAddButton.tsx
index 658df6f339..cd1bcd1b2a 100644
--- a/packages/frontend/src/components/FlowStepGroup/components/GroupStepWithAddButton.tsx
+++ b/packages/frontend/src/components/FlowStepGroup/components/GroupStepWithAddButton.tsx
@@ -26,7 +26,6 @@ export default function GroupStepWithAddButton(
isLastStep,
isOverlay,
allowReorder,
-
showEmptyAction,
canChildStepsReorder,
} = props
diff --git a/packages/frontend/src/components/FlowStepTestController/index.tsx b/packages/frontend/src/components/FlowStepTestController/index.tsx
index 96bec1d579..63e7ce9b53 100644
--- a/packages/frontend/src/components/FlowStepTestController/index.tsx
+++ b/packages/frontend/src/components/FlowStepTestController/index.tsx
@@ -114,7 +114,7 @@ export default function FlowStepTestController(
isMrfStep,
selectedActionOrTrigger,
substeps,
- } = useStepMetadata(allApps, step)
+ } = useStepMetadata(step)
const {
isTestSuccessful,
diff --git a/packages/frontend/src/contexts/MrfContext.tsx b/packages/frontend/src/contexts/MrfContext.tsx
new file mode 100644
index 0000000000..52d5be3e33
--- /dev/null
+++ b/packages/frontend/src/contexts/MrfContext.tsx
@@ -0,0 +1,67 @@
+import { IStep } from '@plumber/types'
+
+import { createContext, useContext, useState } from 'react'
+
+import { FORMSG_APP_KEY, MRF_ACTION_KEY } from '@/helpers/formsg'
+
+type ApprovalBranch = 'approve' | 'reject'
+
+interface MrfContextReturnValue {
+ mrfSteps: IStep[]
+ approvalBranches: {
+ [stepId: string]: ApprovalBranch
+ }
+ setApprovalBranch: (stepId: string, branch: ApprovalBranch) => void
+}
+
+const MrfContext = createContext(undefined)
+
+export const useMrfContext = () => {
+ const context = useContext(MrfContext)
+ if (!context) {
+ throw new Error('useMrfContext must be used within a MrfContextProvider')
+ }
+ return context
+}
+
+interface MrfContextProviderProps {
+ children: React.ReactNode
+ steps: IStep[]
+}
+
+export const MrfContextProvider = ({
+ children,
+ steps,
+}: MrfContextProviderProps) => {
+ const mrfSteps = steps.filter(
+ (step) => step.appKey === FORMSG_APP_KEY && step.key === MRF_ACTION_KEY,
+ )
+
+ const [approvalBranches, setApprovalBranches] = useState<
+ Record
+ >({
+ ...mrfSteps.reduce((acc, step) => {
+ acc[step.id] = 'approve'
+ return acc
+ }, {} as Record),
+ })
+
+ const setApprovalBranch = (stepId: string, branch: ApprovalBranch) => {
+ setApprovalBranches((prev) => ({
+ ...prev,
+ [stepId]: branch,
+ }))
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/packages/frontend/src/contexts/StepExecutions.tsx b/packages/frontend/src/contexts/StepExecutions.tsx
index ade4847501..a84ba46627 100644
--- a/packages/frontend/src/contexts/StepExecutions.tsx
+++ b/packages/frontend/src/contexts/StepExecutions.tsx
@@ -1,20 +1,73 @@
-import type { IExecutionStep } from '@plumber/types'
+import type { IExecutionStep, IStep } from '@plumber/types'
-import * as React from 'react'
+import { createContext, useContext, useMemo } from 'react'
-export const StepExecutionsContext = React.createContext<{
+import { TOOLBOX_ACTIONS } from '@/helpers/toolbox'
+
+import { EditorContext } from './Editor'
+import { StepExecutionsToIncludeContext } from './StepExecutionsToInclude'
+
+export const StepExecutionsContext = createContext<{
priorExecutionSteps: IExecutionStep[]
}>({ priorExecutionSteps: [] })
type StepExecutionsProviderProps = {
children: React.ReactNode
- priorExecutionSteps: IExecutionStep[]
+ currentStep: IStep
}
-export const StepExecutionsProvider = (
- props: StepExecutionsProviderProps,
-): React.ReactElement => {
- const { children, priorExecutionSteps } = props
+export const StepExecutionsProvider = ({
+ currentStep,
+ children,
+}: StepExecutionsProviderProps): React.ReactElement => {
+ const { testExecutionSteps } = useContext(EditorContext)
+
+ const {
+ triggerStep,
+ actionStepsBeforeGroup: stepsBeforeGroup,
+ groupedSteps,
+ } = useContext(StepExecutionsToIncludeContext)
+
+ //
+ // Compute which steps are eligible for variable extraction.
+ // Mainly for if-then branches where we do not want to include steps
+ // from other branches.
+ //
+ // Note:
+ // - we include some grouped steps as there is no longer a nested editor
+ // - we identify the group by checking if the current step id is in the group
+ // - for-each steps are always included
+ const groupStepsToInclude = useMemo(() => {
+ return groupedSteps.flatMap((group) =>
+ group.some(
+ (step) =>
+ step.key === TOOLBOX_ACTIONS.ForEach || step.id === currentStep.id,
+ )
+ ? group
+ : [],
+ )
+ }, [currentStep?.id, groupedSteps])
+
+ const stepExecutionsToInclude = useMemo(
+ () =>
+ new Set([
+ ...(triggerStep?.id ? [triggerStep.id] : []),
+ ...stepsBeforeGroup.map((step) => step.id),
+ ...groupStepsToInclude.map((s) => s.id),
+ ]),
+ [triggerStep, stepsBeforeGroup, groupStepsToInclude],
+ )
+
+ const priorExecutionSteps = useMemo(
+ () =>
+ testExecutionSteps.filter(
+ (stepExecution) =>
+ stepExecutionsToInclude?.has(stepExecution.stepId) &&
+ stepExecution.step.position < currentStep.position,
+ ),
+ [currentStep.position, stepExecutionsToInclude, testExecutionSteps],
+ )
+
return (
{children}
diff --git a/packages/frontend/src/contexts/StepExecutionsToInclude.tsx b/packages/frontend/src/contexts/StepExecutionsToInclude.tsx
index 0f2bae6048..45fb83856b 100644
--- a/packages/frontend/src/contexts/StepExecutionsToInclude.tsx
+++ b/packages/frontend/src/contexts/StepExecutionsToInclude.tsx
@@ -3,22 +3,40 @@ import type { IStep } from '@plumber/types'
import type { ReactNode } from 'react'
import { createContext } from 'react'
-export type StepExecutionsToIncludeContextData = ReadonlySet
+export type StepExecutionsToIncludeContextData = {
+ triggerStep: IStep | null
+ actionStepsBeforeGroup: IStep[]
+ groupedSteps: IStep[][]
+}
export const StepExecutionsToIncludeContext =
- createContext(new Set())
+ createContext({
+ triggerStep: null,
+ actionStepsBeforeGroup: [],
+ groupedSteps: [],
+ })
-type StepExecutionsProviderProps = {
+interface StepExecutionsProviderProps {
children: ReactNode
- value: StepExecutionsToIncludeContextData
+ triggerStep: IStep | null
+ actionStepsBeforeGroup: IStep[]
+ groupedSteps: IStep[][]
}
-export function StepExecutionsToIncludeProvider(
- props: StepExecutionsProviderProps,
-): JSX.Element {
- const { children, value } = props
+export function StepExecutionsToIncludeProvider({
+ children,
+ triggerStep,
+ actionStepsBeforeGroup,
+ groupedSteps,
+}: StepExecutionsProviderProps): JSX.Element {
return (
-
+
{children}
)
diff --git a/packages/frontend/src/hooks/useStepMetadata.ts b/packages/frontend/src/hooks/useStepMetadata.ts
index d45aad59e7..8ec1f83fd7 100644
--- a/packages/frontend/src/hooks/useStepMetadata.ts
+++ b/packages/frontend/src/hooks/useStepMetadata.ts
@@ -1,8 +1,9 @@
import { IAction, IApp, IStep, ISubstep, ITrigger } from '@plumber/types'
-import { useMemo } from 'react'
+import { useContext, useMemo } from 'react'
import get from 'lodash/get'
+import { EditorContext } from '@/contexts/Editor'
import { FORMSG_APP_KEY, MRF_ACTION_KEY } from '@/helpers/formsg'
import getStepName from '@/helpers/getStepName'
import {
@@ -29,13 +30,12 @@ interface UseStepMetadataResult {
}
export function useStepMetadata(
- allApps: IApp[],
step: IStep | undefined,
- readOnly?: boolean,
allowReorder?: boolean,
- isMobile?: boolean,
- isDrawerOpen?: boolean,
): UseStepMetadataResult {
+ const { readOnly, isMobile, isDrawerOpen, allApps } =
+ useContext(EditorContext)
+
const isCompleted = step?.status === 'completed'
const isTrigger = step?.type === 'trigger'
const isIfThenStep = step ? checkIfThenStep(step) : false
@@ -99,9 +99,18 @@ export function useStepMetadata(
!isMobile &&
!isIfThenStep &&
allowReorder &&
- !isDrawerOpen
+ !isDrawerOpen &&
+ !isMrfStep
)
- }, [readOnly, isTrigger, isMobile, isIfThenStep, allowReorder, isDrawerOpen])
+ }, [
+ readOnly,
+ isTrigger,
+ isMobile,
+ isIfThenStep,
+ allowReorder,
+ isDrawerOpen,
+ isMrfStep,
+ ])
return {
app,