mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1595a1799d | ||
|
|
7d621a9966 |
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user