Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import { BiPlus } from 'react-icons/bi'
import { Box, Divider, useDisclosure } from '@chakra-ui/react'
import { IconButton, TouchableTooltip } from '@opengovsg/design-system-react'

import EmptyFlowStepHeader from '@/components/EmptyFlowStepHeader'
import FlowStepConfigurationModal from '@/components/FlowStepConfigurationModal'
import { useUnsavedChanges } from '@/hooks/useUnsavedChanges'

import EmptyFlowStepHeader from '../EmptyFlowStepHeader'
import FlowStepConfigurationModal from '../FlowStepConfigurationModal'

import UnsavedChangesAlert from './UnsavedChangesAlert'

interface AddStepButtonProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IStep } from '@plumber/types'
import { FlowStep } from '@/exports/components'
import { useStepMetadata } from '@/hooks/useStepMetadata'

import { ApproveReject } from '../FlowStep/components/ApproveReject'
import { ApproveReject } from '../../FlowStep/components/ApproveReject'

import { AddStepButton } from './AddStepButton'

Expand Down
146 changes: 146 additions & 0 deletions packages/frontend/src/components/Editor/components/StepsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { IStep } from '@plumber/types'

import { useContext } from 'react'
import { Center, Flex } from '@chakra-ui/react'

import PrimarySpinner from '@/components/PrimarySpinner'
import { SortableList } from '@/components/SortableList'
import { EditorContext } from '@/contexts/Editor'
import { StepsToDisplayContext } from '@/contexts/StepsToDisplay'
import { FlowStepGroup } from '@/exports/components'
import { StepEnumType } from '@/graphql/__generated__/graphql'
import { TOOLBOX_ACTIONS } from '@/helpers/toolbox'
import useReorderSteps from '@/hooks/useReorderSteps'

import { EDITOR_RIGHT_DRAWER_WIDTH } from '../constants'
import { editorStyles } from '../styles'

import FlowStepWithAddButton from './FlowStepWithAddButton'

interface StepsListProps {
isNested?: boolean
}

export function StepsList({ isNested }: StepsListProps) {
const {
triggerStep,
actionStepsBeforeGroup,
groupedSteps,
appsWithActions,
groupingActions,
} = useContext(StepsToDisplayContext)
const { flow, isDrawerOpen, isMobile, readOnly } = useContext(EditorContext)

const { handleReorderUpdate } = useReorderSteps(flow.id)

const handleReorderSteps = async (reorderedSteps: IStep[]) => {
const stepPositions = reorderedSteps.map((step, index) => ({
id: step.id,
position: index + 2, // trigger position is 1
type: step.type as StepEnumType,
}))

try {
await handleReorderUpdate(stepPositions)
} catch (error) {
console.error(
'Error updating step positions: ',
error,
JSON.stringify(stepPositions),
)
}
}

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 (
<Center height="100vh" position="fixed" width="full" top={0} left={0}>
<PrimarySpinner fontSize="4xl" />
</Center>
)
}
const leftStepPadding = isDrawerOpen ? (isMobile ? 0 : '5rem') : 0

return (
<Flex
{...editorStyles.stepHeaderContainer}
flex={isDrawerOpen ? (isMobile ? 0 : 1) : undefined}
px={leftStepPadding}
maxWidth={`calc(100% - ${
isDrawerOpen ? EDITOR_RIGHT_DRAWER_WIDTH : '0px'
})`}
>
{triggerStep && (
<FlowStepWithAddButton
step={triggerStep}
isLastStep={
actionStepsBeforeGroup.length === 0 && groupedSteps.length === 0
}
isNested={isNested}
stepsBeforeGroup={[]} // no reason to pass in for this
groupedSteps={groupedSteps}
addButtonProps={{
isHidden: readOnly,
isDisabled: shouldDisableAddButton,
showEmptyAction: shouldShowEmptyAction,
}}
/>
)}

<SortableList
items={actionStepsBeforeGroup}
onChange={handleReorderSteps}
renderItem={(step, isOverlay) => {
const { id, position } = step
return (
<SortableList.Item id={id}>
<Flex
key={`${id}-${position}`}
width={isDrawerOpen || isMobile ? '100%' : 'auto'}
flexDir="column"
position="relative"
>
<FlowStepWithAddButton
step={step}
isLastStep={
groupedSteps.length === 0 &&
actionStepsBeforeGroup[actionStepsBeforeGroup.length - 1]
.id === step.id
}
isNested={isNested}
stepsBeforeGroup={actionStepsBeforeGroup}
groupedSteps={groupedSteps}
addButtonProps={{
isHidden: readOnly || !!isOverlay,
isDisabled: shouldDisableAddButton,
showEmptyAction: shouldShowEmptyAction,
}}
/>
</Flex>
</SortableList.Item>
)
}}
/>
{groupedSteps.length > 0 && (
<FlowStepGroup
stepsBeforeGroup={actionStepsBeforeGroup}
groupedSteps={groupedSteps}
/>
)}
</Flex>
)
}
192 changes: 11 additions & 181 deletions packages/frontend/src/components/Editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import type { IApp, IStep } from '@plumber/types'

import { useContext, useMemo } from 'react'
import { Center, Flex } from '@chakra-ui/react'
import { Flex } from '@chakra-ui/react'

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'
import useReorderSteps from '@/hooks/useReorderSteps'

import PrimarySpinner from '../PrimarySpinner'
import { StepsToDisplayProvider } from '@/contexts/StepsToDisplay'

import { StepsList } from './components/StepsList'
import { EDITOR_RIGHT_DRAWER_WIDTH } from './constants'
import FlowStepWithAddButton from './FlowStepWithAddButton'
import { editorStyles } from './styles'

type EditorProps = {
Expand All @@ -25,108 +16,13 @@ type EditorProps = {

export default function Editor(props: EditorProps): React.ReactElement {
const { isNested } = props

const { allApps, isDrawerOpen, isMobile, flow, readOnly, currentStepId } =
useContext(EditorContext)

const { handleReorderUpdate } = useReorderSteps(flow.id)
const steps = flow.steps

const { currentStepId, flow } = useContext(EditorContext)
const currentStep = useMemo(() => {
return steps.find((step) => step.id === currentStepId)
}, [currentStepId, steps])

const appsWithActions: IApp[] = allApps.filter(
(app: IApp) => !!app.actions?.length,
)

const groupingActions = useMemo(() => {
if (!appsWithActions) {
return null
}

return new Set(
appsWithActions?.flatMap((app) =>
app.actions
?.filter((action) => action.groupsLaterSteps)
?.map((action) => `${app.key}-${action.key}`),
) ?? [],
)
}, [appsWithActions])

const [triggerStep, actionStepsBeforeGroup, groupedSteps] = useMemo(() => {
if (!groupingActions) {
return [null, [], []]
}

const groupStepIdx = steps.findIndex((step, index) => {
if (
// We ignore the 1st step because it's either a trigger, or a
// step-grouping action that is using a nested Editor to edit steps in
// its group.
index === 0 ||
!step.appKey ||
!step.key
) {
return false
}
return groupingActions.has(`${step.appKey}-${step.key}`)
})

let branchesWithSteps: IStep[][] = []
if (groupStepIdx !== -1) {
branchesWithSteps = extractBranchesWithSteps(steps.slice(groupStepIdx), 0)
}
return flow.steps.find((step) => step.id === currentStepId)
}, [currentStepId, flow])

const triggerStep = steps[0]
const { isDrawerOpen, isMobile } = useContext(EditorContext)

return groupStepIdx === -1
? [triggerStep, steps.slice(1), []]
: [triggerStep, steps.slice(1, groupStepIdx), branchesWithSteps]
}, [groupingActions, steps])

const handleReorderSteps = async (reorderedSteps: IStep[]) => {
const stepPositions = reorderedSteps.map((step, index) => ({
id: step.id,
position: index + 2, // trigger position is 1
type: step.type as StepEnumType,
}))

try {
await handleReorderUpdate(stepPositions)
} catch (error) {
console.error(
'Error updating step positions: ',
error,
JSON.stringify(stepPositions),
)
}
}

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 (
<Center height="100vh" position="fixed" width="full" top={0} left={0}>
<PrimarySpinner fontSize="4xl" />
</Center>
)
}

const leftStepPadding = isDrawerOpen ? (isMobile ? 0 : '5rem') : 0
const rightDrawerTransform = isDrawerOpen
? 'translateX(0)'
: 'translateX(100%)'
Expand All @@ -144,75 +40,9 @@ export default function Editor(props: EditorProps): React.ReactElement {
backgroundSize: '30px 30px',
}}
>
<MrfContextProvider steps={steps}>
<StepExecutionsToIncludeProvider
groupedSteps={groupedSteps}
triggerStep={triggerStep}
actionStepsBeforeGroup={actionStepsBeforeGroup}
>
<Flex
{...editorStyles.stepHeaderContainer}
flex={isDrawerOpen ? (isMobile ? 0 : 1) : undefined}
px={leftStepPadding}
maxWidth={`calc(100% - ${
isDrawerOpen ? EDITOR_RIGHT_DRAWER_WIDTH : '0px'
})`}
>
{triggerStep && (
<FlowStepWithAddButton
step={triggerStep}
isLastStep={
actionStepsBeforeGroup.length === 0 &&
groupedSteps.length === 0
}
isNested={isNested}
stepsBeforeGroup={[]} // no reason to pass in for this
groupedSteps={groupedSteps}
addButtonProps={{
isHidden: readOnly,
isDisabled: shouldDisableAddButton,
showEmptyAction: shouldShowEmptyAction,
}}
/>
)}

<SortableList
items={actionStepsBeforeGroup}
onChange={handleReorderSteps}
renderItem={(step, isOverlay) => {
const { id, position } = step
return (
<SortableList.Item id={id}>
<Flex
key={`${id}-${position}`}
width={isDrawerOpen || isMobile ? '100%' : 'auto'}
flexDir="column"
position="relative"
>
<FlowStepWithAddButton
step={step}
isLastStep={position === steps.length}
isNested={isNested}
stepsBeforeGroup={actionStepsBeforeGroup}
groupedSteps={groupedSteps}
addButtonProps={{
isHidden: readOnly || !!isOverlay,
isDisabled: shouldDisableAddButton,
showEmptyAction: shouldShowEmptyAction,
}}
/>
</Flex>
</SortableList.Item>
)
}}
/>
{groupedSteps.length > 0 && (
<FlowStepGroup
stepsBeforeGroup={actionStepsBeforeGroup}
groupedSteps={groupedSteps}
/>
)}
</Flex>
<MrfContextProvider>
<StepsToDisplayProvider>
<StepsList isNested={isNested} />
{/** HACKFIX (kevinkim-ogp): to ensure that the transitions are smooth */}
<Flex
{...editorStyles.dummyRightContainer}
Expand All @@ -228,7 +58,7 @@ export default function Editor(props: EditorProps): React.ReactElement {
>
<EditorRightDrawer step={currentStep} />
</Flex>
</StepExecutionsToIncludeProvider>
</StepsToDisplayProvider>
</MrfContextProvider>
</Flex>
)
Expand Down
Loading