diff --git a/packages/backend/src/graphql/mutations/create-step.ts b/packages/backend/src/graphql/mutations/create-step.ts index 732ba60b6e..db38d4a4ce 100644 --- a/packages/backend/src/graphql/mutations/create-step.ts +++ b/packages/backend/src/graphql/mutations/create-step.ts @@ -34,7 +34,7 @@ const createStep: MutationResolvers['createStep'] = async ( throw new BadUserInputError('No such trigger or action') } - if ('hiddenFromUser' in triggerOrAction && triggerOrAction.hiddenFromUser) { + if (triggerOrAction?.hiddenFromUser) { throw new BadUserInputError('Action can only be created by system') } } diff --git a/packages/backend/src/models/app.ts b/packages/backend/src/models/app.ts index 86b5e0c915..3cfad3c5c8 100644 --- a/packages/backend/src/models/app.ts +++ b/packages/backend/src/models/app.ts @@ -41,7 +41,7 @@ class App { static async findTriggerOrActionByKey( appKey: string, key: string, - ): Promise { + ): Promise { try { const app = await this.findOneByKey(appKey) return ( diff --git a/packages/frontend/src/components/ChooseConnectionSubstep/index.tsx b/packages/frontend/src/components/ChooseConnectionSubstep/index.tsx index 16beeb6f65..0dae5d76d8 100644 --- a/packages/frontend/src/components/ChooseConnectionSubstep/index.tsx +++ b/packages/frontend/src/components/ChooseConnectionSubstep/index.tsx @@ -8,6 +8,7 @@ import { Button, Link } from '@opengovsg/design-system-react' import { EditorContext } from '@/contexts/Editor' import { TEST_CONNECTION } from '@/graphql/queries/test-connection' +import { FORMSG_APP_KEY } from '@/helpers/formsg' import useAuthentication from '@/hooks/useAuthentication' import { NON_EDITABLE_APP_CONNECTIONS } from '../Editor/constants' @@ -118,7 +119,7 @@ function ChooseConnectionSubstep( const connectionOption = optionGenerator(connection, application.key) let connectionLink: ConnectionLink | undefined - if (application.key === 'formsg') { + if (application.key === FORMSG_APP_KEY) { connectionLink = { url: formLinkGenerator(connectionOption), text: '(View form)', diff --git a/packages/frontend/src/components/Editor/FlowStepWithAddButton.tsx b/packages/frontend/src/components/Editor/FlowStepWithAddButton.tsx index 96458b5220..2163b3ce68 100644 --- a/packages/frontend/src/components/Editor/FlowStepWithAddButton.tsx +++ b/packages/frontend/src/components/Editor/FlowStepWithAddButton.tsx @@ -5,6 +5,9 @@ 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' import { AddStepButton } from './AddStepButton' @@ -23,7 +26,9 @@ export default function FlowStepWithAddButton({ groupedSteps: IStep[][] showAddButton?: boolean }) { - const { readOnly } = useContext(EditorContext) + const { readOnly, allApps } = useContext(EditorContext) + + const { isApprovalStep } = useStepMetadata(allApps, step) const nonIfThenActionSteps = stepsBeforeGroup.filter( (step) => step.type === 'action' && step.key !== TOOLBOX_ACTIONS.IfThen, @@ -59,12 +64,13 @@ export default function FlowStepWithAddButton({ <> 1} /> + {isApprovalStep && } + {showAddButton && ( )} diff --git a/packages/frontend/src/components/FlowStep/components/ApproveReject.tsx b/packages/frontend/src/components/FlowStep/components/ApproveReject.tsx new file mode 100644 index 0000000000..fd167e395a --- /dev/null +++ b/packages/frontend/src/components/FlowStep/components/ApproveReject.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react' +import { Flex, Tab, TabList, TabProps, Tabs } from '@chakra-ui/react' + +const tabStyle = (primaryColor: string): TabProps => { + return { + _selected: { + color: 'white', + bg: primaryColor, + _hover: { + color: 'white', + }, + }, + _hover: { + color: primaryColor, + }, + letterSpacing: '0', + fontWeight: 'medium', + textTransform: 'none', + px: 4, + fontSize: 'medium', + } +} + +export function ApproveReject() { + const [isApprovedSelected, setIsApprovedSelected] = useState(true) + + return ( + + setIsApprovedSelected(index === 0)} + > + + If approved + If rejected + + + + ) +} diff --git a/packages/frontend/src/components/FlowStep/index.tsx b/packages/frontend/src/components/FlowStep/index.tsx index e95902fed1..329ded9891 100644 --- a/packages/frontend/src/components/FlowStep/index.tsx +++ b/packages/frontend/src/components/FlowStep/index.tsx @@ -6,7 +6,6 @@ import { Box, CircularProgress, Flex, useDisclosure } from '@chakra-ui/react' import { Infobox } from '@opengovsg/design-system-react' import { EditorContext } from '@/contexts/Editor' -import { StepDisplayOverridesContext } from '@/contexts/StepDisplayOverrides' import { MarkdownRenderer } from '@/exports/components' import { getFlowStepHeaderWidth } from '@/helpers/editor' import { replacePlaceholdersForHelpMessage } from '@/helpers/flow-templates' @@ -31,7 +30,6 @@ import { flowStepStyles } from './styles' type FlowStepProps = { step: IStep - isDeletable?: boolean isLastStep: boolean isNested?: boolean allowReorder?: boolean @@ -70,7 +68,6 @@ export default function FlowStep( setCurrentStepId, setShouldWarnOnLeave, } = useContext(EditorContext) - const displayOverrides = useContext(StepDisplayOverridesContext)?.[step.id] const { app, caption, @@ -79,6 +76,7 @@ export default function FlowStep( selectedActionOrTrigger, substeps, shouldShowDragHandle, + isDeletable, } = useStepMetadata( allApps, step, @@ -99,11 +97,6 @@ export default function FlowStep( onProceed: onModalOpen, }) - const isDeletable = - displayOverrides?.disableDelete === true - ? false - : !readOnly && props.isDeletable - const { shouldTestStepAgain, isTestSuccessful } = useMemo( () => validateStepParams(step, testExecutionSteps, substeps), [step, substeps, testExecutionSteps], diff --git a/packages/frontend/src/components/FlowStepGroup/components/GroupStepWithAddButton.tsx b/packages/frontend/src/components/FlowStepGroup/components/GroupStepWithAddButton.tsx index 668eab3bf1..658df6f339 100644 --- a/packages/frontend/src/components/FlowStepGroup/components/GroupStepWithAddButton.tsx +++ b/packages/frontend/src/components/FlowStepGroup/components/GroupStepWithAddButton.tsx @@ -4,7 +4,6 @@ import { useContext } from 'react' import { EditorContext } from '@/contexts/Editor' import { FlowStep } from '@/exports/components' -import { TOOLBOX_ACTIONS } from '@/helpers/toolbox' import { HoverAddStepButton } from '../Content/IfThen/HoverAddStepButton' @@ -27,20 +26,16 @@ export default function GroupStepWithAddButton( isLastStep, isOverlay, allowReorder, + showEmptyAction, canChildStepsReorder, } = props const { isDrawerOpen, readOnly } = useContext(EditorContext) - // cannot delete the condition step - const isDeletable = - step.key !== TOOLBOX_ACTIONS.IfThen && step.key !== TOOLBOX_ACTIONS.ForEach - return ( <> ) => void @@ -27,9 +33,9 @@ interface CheckAgainButtonProps { export function CheckAgainButton(props: CheckAgainButtonProps) { const { isUnstyledInfobox, onClick, isLoading, isDisabled, step } = props const isFormSgTrigger = - step.appKey === 'formsg' && step.key === 'newSubmission' + step.appKey === FORMSG_APP_KEY && step.key === FORMSG_TRIGGER_KEY const isFormSgAction = - step.appKey === 'formsg' && step.key === 'mrfSubmission' + step.appKey === FORMSG_APP_KEY && step.key === MRF_ACTION_KEY if (isFormSgTrigger) { return diff --git a/packages/frontend/src/components/FlowSubstep/index.tsx b/packages/frontend/src/components/FlowSubstep/index.tsx index dbe4b2ec19..0381ef601b 100644 --- a/packages/frontend/src/components/FlowSubstep/index.tsx +++ b/packages/frontend/src/components/FlowSubstep/index.tsx @@ -125,11 +125,7 @@ function FlowSubstep(props: FlowSubstepProps): JSX.Element { const handleSaveAndTest = useCallback( async (testRunMetadata?: Record) => { try { - if ( - !selectedActionOrTrigger || - !('hiddenFromUser' in selectedActionOrTrigger) || - selectedActionOrTrigger.hiddenFromUser !== true - ) { + if (!selectedActionOrTrigger?.hiddenFromUser) { await saveStep() } await executeTestStep({ testRunMetadata }) diff --git a/packages/frontend/src/contexts/StepDisplayOverrides.tsx b/packages/frontend/src/contexts/StepDisplayOverrides.tsx deleted file mode 100644 index f2aa9be4e2..0000000000 --- a/packages/frontend/src/contexts/StepDisplayOverrides.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { IStep } from '@plumber/types' - -import type { ReactNode } from 'react' -import { createContext } from 'react' - -// -// FIXME (ogp-weeloong): This is hella hackfix... think of something better and -// refactor. -// - -interface StepDisplayOverrides { - hintAboveCaption?: string - caption?: string - disableActionChanges?: boolean - disableDelete?: boolean -} - -// Using record instead of Map because most of the time we will only have 1-2 overrides. -export type StepDisplayOverridesContextData = Record< - IStep['id'], - StepDisplayOverrides -> - -export const StepDisplayOverridesContext = - createContext({}) - -type StepExecutionsProviderProps = { - children: ReactNode - value: StepDisplayOverridesContextData -} - -export function StepDisplayOverridesProvider( - props: StepExecutionsProviderProps, -): JSX.Element { - const { children, value } = props - return ( - - {children} - - ) -} diff --git a/packages/frontend/src/helpers/formsg.ts b/packages/frontend/src/helpers/formsg.ts new file mode 100644 index 0000000000..26367a044a --- /dev/null +++ b/packages/frontend/src/helpers/formsg.ts @@ -0,0 +1,3 @@ +export const MRF_ACTION_KEY = 'mrfSubmission' +export const FORMSG_APP_KEY = 'formsg' +export const FORMSG_TRIGGER_KEY = 'newSubmission' diff --git a/packages/frontend/src/hooks/useStepMetadata.ts b/packages/frontend/src/hooks/useStepMetadata.ts index 728a143caf..d45aad59e7 100644 --- a/packages/frontend/src/hooks/useStepMetadata.ts +++ b/packages/frontend/src/hooks/useStepMetadata.ts @@ -1,9 +1,14 @@ import { IAction, IApp, IStep, ISubstep, ITrigger } from '@plumber/types' import { useMemo } from 'react' +import get from 'lodash/get' +import { FORMSG_APP_KEY, MRF_ACTION_KEY } from '@/helpers/formsg' import getStepName from '@/helpers/getStepName' -import { isIfThenStep as checkIfThenStep } from '@/helpers/toolbox' +import { + isIfThenStep as checkIfThenStep, + TOOLBOX_ACTIONS, +} from '@/helpers/toolbox' interface UseStepMetadataResult { app: IApp | undefined @@ -18,7 +23,9 @@ interface UseStepMetadataResult { stepName: string substeps: ISubstep[] shouldShowDragHandle?: boolean + isDeletable: boolean isMrfStep: boolean + isApprovalStep: boolean } export function useStepMetadata( @@ -32,7 +39,6 @@ export function useStepMetadata( const isCompleted = step?.status === 'completed' const isTrigger = step?.type === 'trigger' const isIfThenStep = step ? checkIfThenStep(step) : false - const isMrfStep = step?.key === 'mrfSubmission' const apps: IApp[] = allApps?.filter((app: IApp) => isTrigger ? !!app.triggers?.length : !!app.actions?.length, @@ -60,6 +66,21 @@ export function useStepMetadata( (substep: ISubstep) => substep.key === 'chooseConnection', ) + const isDeletable = useMemo( + () => + !readOnly && + step?.key !== TOOLBOX_ACTIONS.IfThen && + step?.key !== TOOLBOX_ACTIONS.ForEach && + !selectedActionOrTrigger?.hiddenFromUser, + [readOnly, selectedActionOrTrigger, step?.key], + ) + + const isMrfStep = + step?.appKey === FORMSG_APP_KEY && step?.key === MRF_ACTION_KEY + + const isApprovalStep = + isMrfStep && !!get(step?.parameters, 'mrf.approvalField', false) + /** * NOTE: there are various conditions that determine whether the drag handle * should be shown. @@ -90,7 +111,6 @@ export function useStepMetadata( hasConnection, isCompleted, isIfThenStep, - isMrfStep, isTrigger, position: step?.position ?? 0, stepName: step?.config?.stepName @@ -98,5 +118,8 @@ export function useStepMetadata( : defaultCaption ?? '', substeps, shouldShowDragHandle, + isDeletable, + isMrfStep, + isApprovalStep, } } diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 71527239d1..557d109e6f 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -712,6 +712,7 @@ export interface IBaseTrigger { description: string isNew?: boolean webhookTriggerInstructions?: ITriggerInstructions + hiddenFromUser?: boolean getInterval?(parameters: IStep['parameters']): string run?($: IGlobalVariable): Promise testRun?(