From 76f411d117b048bce308acb048c0046a1c943208 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Wed, 17 Sep 2025 19:37:09 +0100 Subject: [PATCH] add a prop maxOneExpanded to make the toolstep only allow one step to be expanded at a time and use it in the add stamp tool --- .../tools/shared/ReviewToolStep.tsx | 4 ++ .../tools/shared/SingleExpansionContext.tsx | 41 ++++++++++++ .../tools/shared/createToolFlow.tsx | 65 ++++++++++++++----- .../tools/shared/useSingleExpandController.ts | 65 +++++++++++++++++++ frontend/src/tools/AddStamp.tsx | 61 +++++------------ 5 files changed, 172 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/tools/shared/SingleExpansionContext.tsx create mode 100644 frontend/src/components/tools/shared/useSingleExpandController.ts diff --git a/frontend/src/components/tools/shared/ReviewToolStep.tsx b/frontend/src/components/tools/shared/ReviewToolStep.tsx index 6dcb6fc6c..243e787b5 100644 --- a/frontend/src/components/tools/shared/ReviewToolStep.tsx +++ b/frontend/src/components/tools/shared/ReviewToolStep.tsx @@ -15,6 +15,8 @@ export interface ReviewToolStepProps { title?: string; onFileClick?: (file: File) => void; onUndo: () => void; + isCollapsed?: boolean; + onCollapsedClick?: () => void; } function ReviewStepContent({ @@ -111,6 +113,8 @@ export function createReviewToolStep( t("review", "Review"), { isVisible: props.isVisible, + isCollapsed: props.isCollapsed, + onCollapsedClick: props.onCollapsedClick, _excludeFromCount: true, _noPadding: true, }, diff --git a/frontend/src/components/tools/shared/SingleExpansionContext.tsx b/frontend/src/components/tools/shared/SingleExpansionContext.tsx new file mode 100644 index 000000000..770a6205f --- /dev/null +++ b/frontend/src/components/tools/shared/SingleExpansionContext.tsx @@ -0,0 +1,41 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; + +// Context for managing single step expansion +interface SingleExpansionContextType { + expandedStep: string | null; + setExpandedStep: (stepId: string | null) => void; + enabled: boolean; +} + +const SingleExpansionContext = createContext({ + expandedStep: null, + setExpandedStep: (_: string | null) => {}, + enabled: false, +}); + +export const useSingleExpansion = () => useContext(SingleExpansionContext); + +// Provider component for single expansion mode +export const SingleExpansionProvider: React.FC<{ + children: React.ReactNode; + enabled: boolean; + initialExpandedStep?: string | null; +}> = ({ children, enabled, initialExpandedStep = null }) => { + const [expandedStep, setExpandedStep] = useState(initialExpandedStep); + + const handleSetExpandedStep = useCallback((stepId: string | null) => { + setExpandedStep(stepId); + }, []); + + const contextValue: SingleExpansionContextType = { + expandedStep, + setExpandedStep: handleSetExpandedStep, + enabled, + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index 9ea94bc4f..1c56da099 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -5,6 +5,8 @@ import OperationButton from './OperationButton'; import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation'; import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle'; import { StirlingFile } from '../../../types/fileContext'; +import { SingleExpansionProvider } from './SingleExpansionContext'; +import { useSingleExpandController } from './useSingleExpandController'; export interface FilesStepConfig { selectedFiles: StirlingFile[]; @@ -57,38 +59,46 @@ export interface ToolFlowConfig { executeButton?: ExecuteButtonConfig; review: ReviewStepConfig; forceStepNumbers?: boolean; + maxOneExpanded?: boolean; + initialExpandedStep?: string | null; } -/** - * Creates a flexible tool flow with configurable steps and state management left to the tool. - * Reduces boilerplate while allowing tools to manage their own collapse/expansion logic. - */ -export function createToolFlow(config: ToolFlowConfig) { +// Hoist ToolFlowContent outside to make it stable across renders +function ToolFlowContent({ config }: { config: ToolFlowConfig }) { const steps = createToolSteps(); + const { onToggle, isCollapsed } = useSingleExpandController({ + filesVisible: config.files.isVisible !== false, + stepVisibilities: config.steps.map(s => s.isVisible), + resultsVisible: config.review.isVisible, + }); return ( - - {/* */} + {config.title && } {/* Files Step */} {config.files.isVisible !== false && steps.createFilesStep({ selectedFiles: config.files.selectedFiles, - isCollapsed: config.files.isCollapsed, + isCollapsed: isCollapsed('files', config.files.isCollapsed), minFiles: config.files.minFiles, - onCollapsedClick: config.files.onCollapsedClick + onCollapsedClick: () => onToggle('files', config.files.onCollapsedClick) })} {/* Middle Steps */} - {config.steps.map((stepConfig) => - steps.create(stepConfig.title, { - isVisible: stepConfig.isVisible, - isCollapsed: stepConfig.isCollapsed, - onCollapsedClick: stepConfig.onCollapsedClick, - tooltip: stepConfig.tooltip - }, stepConfig.content) - )} + {config.steps.map((stepConfig, index) => { + const stepId = `step-${index}`; + return ( + + {steps.create(stepConfig.title, { + isVisible: stepConfig.isVisible, + isCollapsed: isCollapsed(stepId, stepConfig.isCollapsed), + onCollapsedClick: () => onToggle(stepId, stepConfig.onCollapsedClick), + tooltip: stepConfig.tooltip + }, stepConfig.content)} + + ); + })} {/* Execute Button */} {config.executeButton && config.executeButton.isVisible !== false && ( @@ -108,9 +118,28 @@ export function createToolFlow(config: ToolFlowConfig) { operation: config.review.operation, title: config.review.title, onFileClick: config.review.onFileClick, - onUndo: config.review.onUndo + onUndo: config.review.onUndo, + isCollapsed: isCollapsed('review', false), + onCollapsedClick: () => onToggle('review', undefined) })} ); } + +export interface ToolFlowProps extends ToolFlowConfig {} + +export function ToolFlow(props: ToolFlowProps) { + return ( + + + + ); +} + +export function createToolFlow(config: ToolFlowConfig) { + return ; +} diff --git a/frontend/src/components/tools/shared/useSingleExpandController.ts b/frontend/src/components/tools/shared/useSingleExpandController.ts new file mode 100644 index 000000000..71c43dfec --- /dev/null +++ b/frontend/src/components/tools/shared/useSingleExpandController.ts @@ -0,0 +1,65 @@ +import { useEffect, useMemo, useCallback } from 'react'; +import { useSingleExpansion } from './SingleExpansionContext'; + +export function useSingleExpandController(opts: { + filesVisible: boolean; + stepVisibilities: (boolean | undefined)[]; + resultsVisible?: boolean; +}) { + const { enabled, expandedStep, setExpandedStep } = useSingleExpansion(); + + const visibleIds = useMemo( + () => [ + ...(opts.filesVisible === false ? [] : ['files']), + ...opts.stepVisibilities.map((v, i) => (v === false ? null : `step-${i}`)).filter(Boolean) as string[], + ...(opts.resultsVisible ? ['review'] : []), + ], + [opts.filesVisible, opts.stepVisibilities, opts.resultsVisible] + ); + + // If single-expand is turned off, clear selection + useEffect(() => { + if (!enabled && expandedStep !== null) setExpandedStep(null); + }, [enabled]); // eslint-disable-line react-hooks/exhaustive-deps + + // If the selected step becomes invisible, clear it + useEffect(() => { + if (!enabled) return; + if (expandedStep && !visibleIds.includes(expandedStep)) { + setExpandedStep(null); + } + }, [enabled, expandedStep, visibleIds]); // eslint-disable-line react-hooks/exhaustive-deps + + // When results become visible, automatically expand them and collapse all others + useEffect(() => { + if (!enabled) return; + if (opts.resultsVisible && expandedStep !== 'review') { + setExpandedStep('review'); + } + }, [enabled, opts.resultsVisible, expandedStep, setExpandedStep]); + + const onToggle = useCallback((stepId: string, original?: () => void) => { + if (enabled) { + // If Files is the only visible step, don't allow it to be collapsed + if (stepId === 'files' && visibleIds.length === 1) { + return; // Don't collapse the only visible step + } + setExpandedStep(expandedStep === stepId ? null : stepId); + } + original?.(); + }, [enabled, expandedStep, setExpandedStep, visibleIds]); + + const isCollapsed = useCallback((stepId: string, original?: boolean) => { + if (!enabled) return original ?? false; + + // If Files is the only visible step, never collapse it + if (stepId === 'files' && visibleIds.length === 1) { + return false; + } + + if (expandedStep == null) return true; + return expandedStep !== stepId; + }, [enabled, expandedStep, visibleIds]); + + return { visibleIds, onToggle, isCollapsed }; +} diff --git a/frontend/src/tools/AddStamp.tsx b/frontend/src/tools/AddStamp.tsx index cf9f220e8..3ff6aa480 100644 --- a/frontend/src/tools/AddStamp.tsx +++ b/frontend/src/tools/AddStamp.tsx @@ -20,7 +20,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const [collapsedType, setCollapsedType] = useState(false); const [collapsedFormatting, setCollapsedFormatting] = useState(true); const [collapsedPageSelection, setCollapsedPageSelection] = useState(false); - const [textConfirmed, setTextConfirmed] = useState(false); const [quickPositionModeSelected, setQuickPositionModeSelected] = useState(false); const [customPositionModeSelected, setCustomPositionModeSelected] = useState(true); @@ -34,25 +33,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { onPreviewFile?.(null); }, [params.parameters]); - // Auto-collapse steps 2 and 3, and auto-expand step 4 when an image is uploaded - useEffect(() => { - if (params.parameters.stampType === 'image' && params.parameters.stampImage) { - setCollapsedType(true); - setCollapsedPageSelection(true); - setCollapsedFormatting(false); // Auto-expand step 4 (Position & Formatting) - } - }, [params.parameters.stampType, params.parameters.stampImage]); - - // Reset text confirmation when inputs change - useEffect(() => { - if (params.parameters.stampType !== 'text') { - setTextConfirmed(false); - } else { - setTextConfirmed(false); - } - }, [params.parameters.stampType, params.parameters.stampText, params.parameters.alphabet]); - - // Do not auto-collapse when switching types to avoid hiding file input prematurely const handleExecute = async () => { try { @@ -71,7 +51,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const getSteps = () => { const steps: any[] = []; - // Step 1: File settings (page selection) - auto-collapse when image is uploaded + // Step 1: File settings (page selection) steps.push({ title: t("AddStampRequest.pageSelection", "Page Selection"), isCollapsed: hasResults || collapsedPageSelection, @@ -89,7 +69,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { ), }); - // Step 2: Type & Content - auto-collapse when image is uploaded + // Step 2: Type & Content steps.push({ title: t("AddStampRequest.stampType", "Stamp Type"), isCollapsed: hasResults ? true : collapsedType, @@ -122,21 +102,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { minRows={2} disabled={endpointLoading} /> - - -