mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29:24 +00:00
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
This commit is contained in:
parent
814bade723
commit
76f411d117
@ -15,6 +15,8 @@ export interface ReviewToolStepProps<TParams = unknown> {
|
|||||||
title?: string;
|
title?: string;
|
||||||
onFileClick?: (file: File) => void;
|
onFileClick?: (file: File) => void;
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
onCollapsedClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReviewStepContent<TParams = unknown>({
|
function ReviewStepContent<TParams = unknown>({
|
||||||
@ -111,6 +113,8 @@ export function createReviewToolStep<TParams = unknown>(
|
|||||||
t("review", "Review"),
|
t("review", "Review"),
|
||||||
{
|
{
|
||||||
isVisible: props.isVisible,
|
isVisible: props.isVisible,
|
||||||
|
isCollapsed: props.isCollapsed,
|
||||||
|
onCollapsedClick: props.onCollapsedClick,
|
||||||
_excludeFromCount: true,
|
_excludeFromCount: true,
|
||||||
_noPadding: true,
|
_noPadding: true,
|
||||||
},
|
},
|
||||||
|
@ -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<SingleExpansionContextType>({
|
||||||
|
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<string | null>(initialExpandedStep);
|
||||||
|
|
||||||
|
const handleSetExpandedStep = useCallback((stepId: string | null) => {
|
||||||
|
setExpandedStep(stepId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue: SingleExpansionContextType = {
|
||||||
|
expandedStep,
|
||||||
|
setExpandedStep: handleSetExpandedStep,
|
||||||
|
enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SingleExpansionContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</SingleExpansionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -5,6 +5,8 @@ import OperationButton from './OperationButton';
|
|||||||
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
||||||
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
|
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
|
||||||
import { StirlingFile } from '../../../types/fileContext';
|
import { StirlingFile } from '../../../types/fileContext';
|
||||||
|
import { SingleExpansionProvider } from './SingleExpansionContext';
|
||||||
|
import { useSingleExpandController } from './useSingleExpandController';
|
||||||
|
|
||||||
export interface FilesStepConfig {
|
export interface FilesStepConfig {
|
||||||
selectedFiles: StirlingFile[];
|
selectedFiles: StirlingFile[];
|
||||||
@ -57,38 +59,46 @@ export interface ToolFlowConfig {
|
|||||||
executeButton?: ExecuteButtonConfig;
|
executeButton?: ExecuteButtonConfig;
|
||||||
review: ReviewStepConfig;
|
review: ReviewStepConfig;
|
||||||
forceStepNumbers?: boolean;
|
forceStepNumbers?: boolean;
|
||||||
|
maxOneExpanded?: boolean;
|
||||||
|
initialExpandedStep?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Hoist ToolFlowContent outside to make it stable across renders
|
||||||
* Creates a flexible tool flow with configurable steps and state management left to the tool.
|
function ToolFlowContent({ config }: { config: ToolFlowConfig }) {
|
||||||
* Reduces boilerplate while allowing tools to manage their own collapse/expansion logic.
|
|
||||||
*/
|
|
||||||
export function createToolFlow(config: ToolFlowConfig) {
|
|
||||||
const steps = createToolSteps();
|
const steps = createToolSteps();
|
||||||
|
const { onToggle, isCollapsed } = useSingleExpandController({
|
||||||
|
filesVisible: config.files.isVisible !== false,
|
||||||
|
stepVisibilities: config.steps.map(s => s.isVisible),
|
||||||
|
resultsVisible: config.review.isVisible,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm" p="sm" >
|
<Stack gap="sm" p="sm">
|
||||||
{/* <Stack gap="sm" p="sm" h="100%" w="100%" style={{ overflow: 'auto' }}> */}
|
|
||||||
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
||||||
{config.title && <ToolWorkflowTitle {...config.title} />}
|
{config.title && <ToolWorkflowTitle {...config.title} />}
|
||||||
|
|
||||||
{/* Files Step */}
|
{/* Files Step */}
|
||||||
{config.files.isVisible !== false && steps.createFilesStep({
|
{config.files.isVisible !== false && steps.createFilesStep({
|
||||||
selectedFiles: config.files.selectedFiles,
|
selectedFiles: config.files.selectedFiles,
|
||||||
isCollapsed: config.files.isCollapsed,
|
isCollapsed: isCollapsed('files', config.files.isCollapsed),
|
||||||
minFiles: config.files.minFiles,
|
minFiles: config.files.minFiles,
|
||||||
onCollapsedClick: config.files.onCollapsedClick
|
onCollapsedClick: () => onToggle('files', config.files.onCollapsedClick)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Middle Steps */}
|
{/* Middle Steps */}
|
||||||
{config.steps.map((stepConfig) =>
|
{config.steps.map((stepConfig, index) => {
|
||||||
steps.create(stepConfig.title, {
|
const stepId = `step-${index}`;
|
||||||
isVisible: stepConfig.isVisible,
|
return (
|
||||||
isCollapsed: stepConfig.isCollapsed,
|
<React.Fragment key={stepId}>
|
||||||
onCollapsedClick: stepConfig.onCollapsedClick,
|
{steps.create(stepConfig.title, {
|
||||||
tooltip: stepConfig.tooltip
|
isVisible: stepConfig.isVisible,
|
||||||
}, stepConfig.content)
|
isCollapsed: isCollapsed(stepId, stepConfig.isCollapsed),
|
||||||
)}
|
onCollapsedClick: () => onToggle(stepId, stepConfig.onCollapsedClick),
|
||||||
|
tooltip: stepConfig.tooltip
|
||||||
|
}, stepConfig.content)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Execute Button */}
|
{/* Execute Button */}
|
||||||
{config.executeButton && config.executeButton.isVisible !== false && (
|
{config.executeButton && config.executeButton.isVisible !== false && (
|
||||||
@ -108,9 +118,28 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
operation: config.review.operation,
|
operation: config.review.operation,
|
||||||
title: config.review.title,
|
title: config.review.title,
|
||||||
onFileClick: config.review.onFileClick,
|
onFileClick: config.review.onFileClick,
|
||||||
onUndo: config.review.onUndo
|
onUndo: config.review.onUndo,
|
||||||
|
isCollapsed: isCollapsed('review', false),
|
||||||
|
onCollapsedClick: () => onToggle('review', undefined)
|
||||||
})}
|
})}
|
||||||
</ToolStepProvider>
|
</ToolStepProvider>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolFlowProps extends ToolFlowConfig {}
|
||||||
|
|
||||||
|
export function ToolFlow(props: ToolFlowProps) {
|
||||||
|
return (
|
||||||
|
<SingleExpansionProvider
|
||||||
|
enabled={props.maxOneExpanded ?? false}
|
||||||
|
initialExpandedStep={props.initialExpandedStep ?? null}
|
||||||
|
>
|
||||||
|
<ToolFlowContent config={props} />
|
||||||
|
</SingleExpansionProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createToolFlow(config: ToolFlowConfig) {
|
||||||
|
return <ToolFlow {...config} />;
|
||||||
|
}
|
||||||
|
@ -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 };
|
||||||
|
}
|
@ -20,7 +20,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const [collapsedType, setCollapsedType] = useState(false);
|
const [collapsedType, setCollapsedType] = useState(false);
|
||||||
const [collapsedFormatting, setCollapsedFormatting] = useState(true);
|
const [collapsedFormatting, setCollapsedFormatting] = useState(true);
|
||||||
const [collapsedPageSelection, setCollapsedPageSelection] = useState(false);
|
const [collapsedPageSelection, setCollapsedPageSelection] = useState(false);
|
||||||
const [textConfirmed, setTextConfirmed] = useState(false);
|
|
||||||
const [quickPositionModeSelected, setQuickPositionModeSelected] = useState(false);
|
const [quickPositionModeSelected, setQuickPositionModeSelected] = useState(false);
|
||||||
const [customPositionModeSelected, setCustomPositionModeSelected] = useState(true);
|
const [customPositionModeSelected, setCustomPositionModeSelected] = useState(true);
|
||||||
|
|
||||||
@ -34,25 +33,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
onPreviewFile?.(null);
|
onPreviewFile?.(null);
|
||||||
}, [params.parameters]);
|
}, [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 () => {
|
const handleExecute = async () => {
|
||||||
try {
|
try {
|
||||||
@ -71,7 +51,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const getSteps = () => {
|
const getSteps = () => {
|
||||||
const steps: any[] = [];
|
const steps: any[] = [];
|
||||||
|
|
||||||
// Step 1: File settings (page selection) - auto-collapse when image is uploaded
|
// Step 1: File settings (page selection)
|
||||||
steps.push({
|
steps.push({
|
||||||
title: t("AddStampRequest.pageSelection", "Page Selection"),
|
title: t("AddStampRequest.pageSelection", "Page Selection"),
|
||||||
isCollapsed: hasResults || collapsedPageSelection,
|
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({
|
steps.push({
|
||||||
title: t("AddStampRequest.stampType", "Stamp Type"),
|
title: t("AddStampRequest.stampType", "Stamp Type"),
|
||||||
isCollapsed: hasResults ? true : collapsedType,
|
isCollapsed: hasResults ? true : collapsedType,
|
||||||
@ -122,21 +102,6 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
minRows={2}
|
minRows={2}
|
||||||
disabled={endpointLoading}
|
disabled={endpointLoading}
|
||||||
/>
|
/>
|
||||||
<Group justify="flex-start">
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
|
||||||
if ((params.parameters.stampText || '').trim().length === 0) return;
|
|
||||||
setTextConfirmed(true);
|
|
||||||
setCollapsedType(true);
|
|
||||||
setCollapsedPageSelection(true);
|
|
||||||
setCollapsedFormatting(false);
|
|
||||||
}}
|
|
||||||
disabled={(params.parameters.stampText || '').trim().length === 0}
|
|
||||||
>
|
|
||||||
{textConfirmed ? t('confirmed', 'Confirmed') : t('confirm', 'Confirm')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
<Select
|
<Select
|
||||||
label={t('AddStampRequest.alphabet', 'Alphabet')}
|
label={t('AddStampRequest.alphabet', 'Alphabet')}
|
||||||
value={params.parameters.alphabet}
|
value={params.parameters.alphabet}
|
||||||
@ -176,9 +141,16 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
{t('chooseFile', 'Choose File')}
|
{t('chooseFile', 'Choose File')}
|
||||||
</Button>
|
</Button>
|
||||||
{params.parameters.stampImage && (
|
{params.parameters.stampImage && (
|
||||||
<Text size="xs" c="dimmed">
|
<Stack gap="xs">
|
||||||
{params.parameters.stampImage.name}
|
<img
|
||||||
</Text>
|
src={URL.createObjectURL(params.parameters.stampImage)}
|
||||||
|
alt="Selected stamp image"
|
||||||
|
className="max-h-24 w-full object-contain border border-gray-200 rounded bg-gray-50"
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{params.parameters.stampImage.name}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@ -190,12 +162,7 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
steps.push({
|
steps.push({
|
||||||
title: t("AddStampRequest.positionAndFormatting", "Position & Formatting"),
|
title: t("AddStampRequest.positionAndFormatting", "Position & Formatting"),
|
||||||
isCollapsed: hasResults ? true : collapsedFormatting,
|
isCollapsed: hasResults ? true : collapsedFormatting,
|
||||||
onCollapsedClick: hasResults ? () => operation.resetResults() : () => {
|
onCollapsedClick: hasResults ? () => operation.resetResults() : () => setCollapsedFormatting(!collapsedFormatting),
|
||||||
// Prevent collapsing until text confirmed
|
|
||||||
if (params.parameters.stampType === 'text' && !textConfirmed) return;
|
|
||||||
setCollapsedFormatting(!collapsedFormatting);
|
|
||||||
if (collapsedFormatting) setCollapsedType(true);
|
|
||||||
},
|
|
||||||
isVisible: hasFiles || hasResults,
|
isVisible: hasFiles || hasResults,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap="md" justify="space-between">
|
<Stack gap="md" justify="space-between">
|
||||||
@ -394,6 +361,8 @@ const AddStamp = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
forceStepNumbers: true,
|
forceStepNumbers: true,
|
||||||
|
maxOneExpanded: true,
|
||||||
|
initialExpandedStep: "files"
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user