Compare commits

...
Author SHA1 Message Date
Anthony Stirling 1595a1799d url and close button 2026-01-27 21:53:18 +00:00
Anthony Stirling 7d621a9966 fix 2026-01-25 01:04:47 +00:00
8 changed files with 128 additions and 23 deletions
@@ -3,7 +3,7 @@ import { Box } from '@mantine/core';
import { useRainbowThemeContext } from '@app/components/shared/RainbowThemeProvider';
import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext';
import { useFileHandler } from '@app/hooks/useFileHandler';
import { useFileState } from '@app/contexts/FileContext';
import { useFileState, useFileActions } from '@app/contexts/FileContext';
import { useNavigationState, useNavigationActions, useNavigationGuard } from '@app/contexts/NavigationContext';
import { isBaseWorkbench } from '@app/types/workbench';
import { useViewer } from '@app/contexts/ViewerContext';
@@ -26,6 +26,7 @@ export default function Workbench() {
// Use context-based hooks to eliminate all prop drilling
const { selectors } = useFileState();
const { actions: fileActions } = useFileActions();
const { workbench: currentView } = useNavigationState();
const { actions: navActions } = useNavigationActions();
const setCurrentView = navActions.setWorkbench;
@@ -61,13 +62,35 @@ export default function Workbench() {
const handleFileSelect = useCallback((index: number) => {
// Don't do anything if selecting the same file
if (index === activeFileIndex) return;
// requestNavigation handles the unsaved changes check internally
requestNavigation(() => {
setActiveFileIndex(index);
});
}, [activeFileIndex, requestNavigation, setActiveFileIndex]);
// Handle file removal from dropdown
const handleFileRemove = useCallback(async (fileId: string, index: number) => {
// Remove the file from FileContext (handles memory cleanup)
await fileActions.removeFiles([fileId]);
// Adjust activeFileIndex if needed
if (activeFiles.length > 1) {
if (index === activeFileIndex) {
// Removing the active file - switch to the next file (or previous if it's the last)
const newIndex = index >= activeFiles.length - 1 ? Math.max(0, index - 1) : index;
setActiveFileIndex(newIndex);
} else if (index < activeFileIndex) {
// Removing a file before the active one - decrement active index
setActiveFileIndex(activeFileIndex - 1);
}
// If removing a file after the active one, no index adjustment needed
} else {
// Last file removed - reset to index 0 (will show landing page)
setActiveFileIndex(0);
}
}, [activeFiles.length, activeFileIndex, fileActions, setActiveFileIndex]);
const handlePreviewClose = () => {
setPreviewFile(null);
const previousMode = sessionStorage.getItem('previousMode');
@@ -199,6 +222,7 @@ export default function Workbench() {
})}
currentFileIndex={activeFileIndex}
onFileSelect={handleFileSelect}
onFileRemove={handleFileRemove}
/>
)}
@@ -1,15 +1,18 @@
import React from 'react';
import { Menu, Loader, Group, Text } from '@mantine/core';
import { Menu, Loader, Group, Text, ActionIcon, Tooltip } from '@mantine/core';
import VisibilityIcon from '@mui/icons-material/Visibility';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import CloseIcon from '@mui/icons-material/Close';
import FitText from '@app/components/shared/FitText';
import { PrivateContent } from '@app/components/shared/PrivateContent';
import { useTranslation } from 'react-i18next';
interface FileDropdownMenuProps {
displayName: string;
activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>;
currentFileIndex: number;
onFileSelect?: (index: number) => void;
onFileRemove?: (fileId: string, index: number) => void;
switchingTo?: string | null;
viewOptionStyle: React.CSSProperties;
pillRef?: React.RefObject<HTMLDivElement>;
@@ -20,9 +23,12 @@ export const FileDropdownMenu: React.FC<FileDropdownMenuProps> = ({
activeFiles,
currentFileIndex,
onFileSelect,
onFileRemove,
switchingTo,
viewOptionStyle,
}) => {
const { t } = useTranslation();
return (
<Menu trigger="click" position="bottom" width="30rem">
<Menu.Target>
@@ -68,11 +74,39 @@ export const FileDropdownMenu: React.FC<FileDropdownMenuProps> = ({
<FitText text={itemName} fontSize={14} minimumFontScale={0.7} />
</PrivateContent>
</div>
{file.versionNumber && file.versionNumber > 1 && (
<Text size="xs" c="dimmed">
v{file.versionNumber}
</Text>
)}
<Group gap="xs" style={{ flexShrink: 0 }}>
{file.versionNumber && file.versionNumber > 1 && (
<Text size="xs" c="dimmed">
v{file.versionNumber}
</Text>
)}
{onFileRemove && (
<Tooltip label={t('close', 'Close')} position="top" withArrow>
<ActionIcon
size="xs"
variant="subtle"
color="gray"
onClick={(e) => {
e.stopPropagation();
onFileRemove(file.fileId, index);
}}
style={{
opacity: 0.6,
transition: 'opacity 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = '1';
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = '0.6';
}}
aria-label={t('close', 'Close')}
>
<CloseIcon style={{ fontSize: 14 }} />
</ActionIcon>
</Tooltip>
)}
</Group>
</Group>
</Menu.Item>
);
@@ -29,6 +29,7 @@ const createViewOptions = (
activeFiles: Array<{ fileId: string; name: string; versionNumber?: number }>,
currentFileIndex: number,
onFileSelect?: (index: number) => void,
onFileRemove?: (fileId: string, index: number) => void,
pageEditorState?: PageEditorDropdownState,
customViews?: CustomWorkbenchViewInstance[]
) => {
@@ -47,6 +48,7 @@ const createViewOptions = (
activeFiles={activeFiles}
currentFileIndex={currentFileIndex}
onFileSelect={onFileSelect}
onFileRemove={onFileRemove}
switchingTo={switchingTo}
viewOptionStyle={viewOptionStyle}
/>
@@ -132,6 +134,7 @@ interface TopControlsProps {
activeFiles?: Array<{ fileId: string; name: string; versionNumber?: number }>;
currentFileIndex?: number;
onFileSelect?: (index: number) => void;
onFileRemove?: (fileId: string, index: number) => void;
}
const TopControls = ({
@@ -141,6 +144,7 @@ const TopControls = ({
activeFiles = [],
currentFileIndex = 0,
onFileSelect,
onFileRemove,
}: TopControlsProps) => {
const { isRainbowMode } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
@@ -176,9 +180,10 @@ const TopControls = ({
activeFiles,
currentFileIndex,
onFileSelect,
onFileRemove,
pageEditorState,
customViews
), [currentView, switchingTo, activeFiles, currentFileIndex, onFileSelect, pageEditorState, customViews]);
), [currentView, switchingTo, activeFiles, currentFileIndex, onFileSelect, onFileRemove, pageEditorState, customViews]);
return (
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
@@ -123,7 +123,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
{t('settings.general.enableFeatures.benefit', 'Enables user roles, team collaboration, admin controls, and enterprise features.')}
</Text>
<Anchor
href="https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security"
href="https://docs.stirlingpdf.com/Configuration/System%20and%20Security/"
target="_blank"
size="sm"
style={{ color: 'var(--mantine-color-blue-6)' }}
@@ -223,7 +223,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
createdAt: existingAutomation?.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString()
};
downloadFolderScanningConfig(tempAutomation);
downloadFolderScanningConfig(tempAutomation, toolRegistry);
}}
disabled={!canSaveAutomation()}
variant="light"
@@ -59,7 +59,7 @@ export default function AutomationSelection({
onClick={() => onRun(automation)}
showMenu={true}
onEdit={() => onEdit(automation)}
onExport={() => downloadFolderScanningConfig(automation)}
onExport={() => downloadFolderScanningConfig(automation, toolRegistry)}
onDelete={() => onDelete(automation)}
toolRegistry={toolRegistry}
/>
@@ -811,7 +811,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
link: "https://docs.stirlingpdf.com/Configuration/Folder%20Scanning/",
synonyms: getSynonyms(t, "devFolderScanning"),
supportsAutomate: false,
automationSettings: null
@@ -823,7 +823,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
description: t("home.devSsoGuide.desc", "Link to SSO guide"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.DEVELOPER_TOOLS,
link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
link: "https://docs.stirlingpdf.com/Configuration/Single%20Sign-On%20Configuration/",
synonyms: getSynonyms(t, "devSsoGuide"),
supportsAutomate: false,
automationSettings: null
+51 -9
View File
@@ -3,6 +3,8 @@
*/
import { AutomationConfig } from '@app/types/automation';
import { ToolRegistry } from '@app/data/toolsTaxonomy';
import { ToolId } from '@app/types/toolId';
/**
* Pipeline configuration format used by folder scanning
@@ -24,18 +26,54 @@ interface FolderScanningPipeline {
/**
* Converts an AutomationConfig to a folder scanning pipeline configuration
* @param automation The automation configuration to convert
* @param toolRegistry The tool registry to map operation types to endpoints
* @returns Folder scanning pipeline configuration
*/
export function convertToFolderScanningConfig(automation: AutomationConfig): FolderScanningPipeline {
export function convertToFolderScanningConfig(
automation: AutomationConfig,
toolRegistry: Partial<ToolRegistry>
): FolderScanningPipeline {
return {
name: automation.name,
pipeline: automation.operations.map(op => ({
operation: op.operation,
parameters: {
...op.parameters,
fileInput: "automated"
pipeline: automation.operations.map(op => {
// Map operationType to full API endpoint path
const toolId = op.operation as ToolId;
const toolEntry = toolRegistry[toolId];
const endpointConfig = toolEntry?.operationConfig?.endpoint;
let endpoint: string | undefined;
if (typeof endpointConfig === 'string') {
endpoint = endpointConfig;
} else if (typeof endpointConfig === 'function') {
// For dynamic endpoints, call with the saved parameters
try {
endpoint = endpointConfig(op.parameters);
} catch (error) {
console.warn(
`Failed to resolve dynamic endpoint for operation "${op.operation}". ` +
`This may happen if the tool requires specific parameters. ` +
`Error: ${error}`
);
}
}
})),
if (!endpoint) {
console.warn(
`No endpoint found for operation "${op.operation}". ` +
`This operation may fail in folder scanning. ` +
`Using operation type as fallback.`
);
}
return {
operation: endpoint || op.operation,
parameters: {
...op.parameters,
fileInput: "automated"
}
};
}),
_examples: {
outputDir: "{outputFolder}/{folderName}",
outputFileName: "{filename}-{pipelineName}-{date}-{time}"
@@ -48,9 +86,13 @@ export function convertToFolderScanningConfig(automation: AutomationConfig): Fol
/**
* Downloads a folder scanning configuration as a JSON file
* @param automation The automation configuration to export
* @param toolRegistry The tool registry to map operation types to endpoints
*/
export function downloadFolderScanningConfig(automation: AutomationConfig): void {
const config = convertToFolderScanningConfig(automation);
export function downloadFolderScanningConfig(
automation: AutomationConfig,
toolRegistry: Partial<ToolRegistry>
): void {
const config = convertToFolderScanningConfig(automation, toolRegistry);
const json = JSON.stringify(config, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);