From 6afa940b6568fe9af6a4976508aa1316851e0fdb Mon Sep 17 00:00:00 2001 From: ogp-weeloong Date: Mon, 25 May 2026 16:17:11 +0800 Subject: [PATCH] feat: setup front end extensions for app We want to allow apps to extend our front end (e.g. with tooltips) This creates a new "app-extensions" system in front end to enable this. The idea is to allow apps to provide react components that we will render in appropriate parts of the frontend. This kicks it off by extending the "check step" button Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/frontend/src/app-extensions/index.ts | 19 ++++ packages/frontend/src/app-extensions/types.ts | 22 ++++ .../CheckAgainButton.tsx | 22 ++-- .../FlowStepTestController/index.tsx | 107 +++++++++++------- 4 files changed, 122 insertions(+), 48 deletions(-) create mode 100644 packages/frontend/src/app-extensions/index.ts create mode 100644 packages/frontend/src/app-extensions/types.ts diff --git a/packages/frontend/src/app-extensions/index.ts b/packages/frontend/src/app-extensions/index.ts new file mode 100644 index 0000000000..53942d2aba --- /dev/null +++ b/packages/frontend/src/app-extensions/index.ts @@ -0,0 +1,19 @@ +import type { FrontEndAppExtension } from './types' + +// Nothing for now +const APP_EXTENSIONS: Record = {} + +export function getExtension( + appKey?: string, + stepKey?: string, +): FrontEndAppExtension | null { + if (!appKey || !stepKey) { + return null + } + return APP_EXTENSIONS[`${appKey}-${stepKey}`] ?? null +} + +export type { + CheckStepButtonExtensionProps, + FrontEndAppExtension, +} from './types' diff --git a/packages/frontend/src/app-extensions/types.ts b/packages/frontend/src/app-extensions/types.ts new file mode 100644 index 0000000000..d783c3e5ef --- /dev/null +++ b/packages/frontend/src/app-extensions/types.ts @@ -0,0 +1,22 @@ +import type { IStep } from '@plumber/types' + +import type { ComponentType, ReactNode } from 'react' + +export interface CheckStepButtonExtensionProps { + step: IStep + /** The Check Step / Check Step Again button itself. */ + children: ReactNode +} + +export interface FrontEndAppExtension { + /** + * Wraps the Check Step / Check Step Again button. + * + * Mounted ONLY WHEN the button is enabled. + * TBD: Allow extending when button is disabled (for showing custom failure + * tooltip etc) + **/ + CheckStepButton?: ComponentType + + // Room to grow: ResultPanelWrapper, SubstepWrapper, etc. +} diff --git a/packages/frontend/src/components/FlowStepTestController/CheckAgainButton.tsx b/packages/frontend/src/components/FlowStepTestController/CheckAgainButton.tsx index 18e6f5d3d5..081b22715b 100644 --- a/packages/frontend/src/components/FlowStepTestController/CheckAgainButton.tsx +++ b/packages/frontend/src/components/FlowStepTestController/CheckAgainButton.tsx @@ -1,6 +1,6 @@ import { IExecutionStepMetadata, IStep } from '@plumber/types' -import { useCallback, useMemo } from 'react' +import { forwardRef, useCallback, useMemo } from 'react' import { IconType } from 'react-icons' import { BiChevronDown } from 'react-icons/bi' import { PiRobot, PiUser } from 'react-icons/pi' @@ -30,7 +30,10 @@ interface CheckAgainButtonProps { executionStepMetadata?: IExecutionStepMetadata } -export function CheckAgainButton(props: CheckAgainButtonProps) { +export const CheckAgainButton = forwardRef< + HTMLButtonElement, + CheckAgainButtonProps +>((props, ref) => { const { isUnstyledInfobox, onClick, isLoading, isDisabled, step } = props const isFormSgTrigger = step.appKey === FORMSG_APP_KEY && step.key === FORMSG_TRIGGER_KEY @@ -38,13 +41,14 @@ export function CheckAgainButton(props: CheckAgainButtonProps) { step.appKey === FORMSG_APP_KEY && step.key === MRF_ACTION_KEY if (isFormSgTrigger) { - return + return } if (isFormSgAction) { - return + return } return ( ) -} +}) /** * For UX reasons, we need to have a different button for FormSG. @@ -104,7 +108,10 @@ function FormSGMenuItem({ ) } -function FormSGCheckAgainButton(props: CheckAgainButtonProps) { +const FormSGCheckAgainButton = forwardRef< + HTMLButtonElement, + CheckAgainButtonProps +>((props, ref) => { const { isUnstyledInfobox: isTransparentInfobox, onClick, @@ -155,6 +162,7 @@ function FormSGCheckAgainButton(props: CheckAgainButtonProps) { colorScheme={isTransparentInfobox ? 'primary' : 'black'} > )} - + + + @@ -379,20 +402,22 @@ export default function FlowStepTestController( {isDirty ? 'Save' : 'Saved'} )} - - - + + + )}