mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Refactor to fix circular imports (#4700)
# Description of Changes Refactors code to avoid circular imports everywhere and adds linting for circular imports to ensure it doesn't happen again. Most changes are around the tool registry, making it a provider, and splitting into tool types to make it easier for things like Automate to only have access to tools excluding itself.
This commit is contained in:
parent
3e23dc59b6
commit
c9eee00d66
@ -1,9 +1,10 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import globals from "globals";
|
import globals from 'globals';
|
||||||
import { defineConfig } from 'eslint/config';
|
import { defineConfig } from 'eslint/config';
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
|
||||||
const srcGlobs = [
|
const srcGlobs = [
|
||||||
'src/**/*.{js,mjs,jsx,ts,tsx}',
|
'src/**/*.{js,mjs,jsx,ts,tsx}',
|
||||||
@ -14,35 +15,37 @@ const nodeGlobs = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default defineConfig(
|
export default defineConfig(
|
||||||
|
{
|
||||||
|
// Everything that contains 3rd party code that we don't want to lint
|
||||||
|
ignores: [
|
||||||
|
'dist',
|
||||||
|
'node_modules',
|
||||||
|
'public',
|
||||||
|
],
|
||||||
|
},
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
"dist", // Contains 3rd party code
|
|
||||||
"public", // Contains 3rd party code
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-empty-object-type": [
|
'@typescript-eslint/no-empty-object-type': [
|
||||||
"error",
|
'error',
|
||||||
{
|
{
|
||||||
// Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future
|
// Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future
|
||||||
allowInterfaces: 'with-single-extends',
|
allowInterfaces: 'with-single-extends',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant
|
'@typescript-eslint/no-explicit-any': 'off', // Temporarily disabled until codebase conformant
|
||||||
"@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant
|
'@typescript-eslint/no-require-imports': 'off', // Temporarily disabled until codebase conformant
|
||||||
"@typescript-eslint/no-unused-vars": [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
"error",
|
'error',
|
||||||
{
|
{
|
||||||
"args": "all", // All function args must be used (or explicitly ignored)
|
'args': 'all', // All function args must be used (or explicitly ignored)
|
||||||
"argsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
'argsIgnorePattern': '^_', // Allow unused variables beginning with an underscore
|
||||||
"caughtErrors": "all", // Caught errors must be used (or explicitly ignored)
|
'caughtErrors': 'all', // Caught errors must be used (or explicitly ignored)
|
||||||
"caughtErrorsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
'caughtErrorsIgnorePattern': '^_', // Allow unused variables beginning with an underscore
|
||||||
"destructuredArrayIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
'destructuredArrayIgnorePattern': '^_', // Allow unused variables beginning with an underscore
|
||||||
"varsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
'varsIgnorePattern': '^_', // Allow unused variables beginning with an underscore
|
||||||
"ignoreRestSiblings": true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky)
|
'ignoreRestSiblings': true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -65,4 +68,21 @@ export default defineConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Config for import plugin
|
||||||
|
{
|
||||||
|
...importPlugin.flatConfigs.recommended,
|
||||||
|
...importPlugin.flatConfigs.typescript,
|
||||||
|
rules: {
|
||||||
|
// ...importPlugin.flatConfigs.recommended.rules, // Temporarily disabled until codebase conformant
|
||||||
|
...importPlugin.flatConfigs.typescript.rules,
|
||||||
|
'import/no-cycle': 'error',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
2083
frontend/package-lock.json
generated
2083
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -121,6 +121,8 @@
|
|||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.36.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"madge": "^8.0.0",
|
"madge": "^8.0.0",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Suspense } from "react";
|
|||||||
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
|
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
|
||||||
import { FileContextProvider } from "./contexts/FileContext";
|
import { FileContextProvider } from "./contexts/FileContext";
|
||||||
import { NavigationProvider } from "./contexts/NavigationContext";
|
import { NavigationProvider } from "./contexts/NavigationContext";
|
||||||
|
import { ToolRegistryProvider } from "./contexts/ToolRegistryProvider";
|
||||||
import { FilesModalProvider } from "./contexts/FilesModalContext";
|
import { FilesModalProvider } from "./contexts/FilesModalContext";
|
||||||
import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext";
|
import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext";
|
||||||
import { HotkeyProvider } from "./contexts/HotkeyContext";
|
import { HotkeyProvider } from "./contexts/HotkeyContext";
|
||||||
@ -48,26 +49,28 @@ export default function App() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<OnboardingProvider>
|
<OnboardingProvider>
|
||||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||||
<NavigationProvider>
|
<ToolRegistryProvider>
|
||||||
<FilesModalProvider>
|
<NavigationProvider>
|
||||||
<ToolWorkflowProvider>
|
<FilesModalProvider>
|
||||||
<HotkeyProvider>
|
<ToolWorkflowProvider>
|
||||||
<SidebarProvider>
|
<HotkeyProvider>
|
||||||
<ViewerProvider>
|
<SidebarProvider>
|
||||||
<SignatureProvider>
|
<ViewerProvider>
|
||||||
<RightRailProvider>
|
<SignatureProvider>
|
||||||
<TourOrchestrationProvider>
|
<RightRailProvider>
|
||||||
<HomePage />
|
<TourOrchestrationProvider>
|
||||||
<OnboardingTour />
|
<HomePage />
|
||||||
</TourOrchestrationProvider>
|
<OnboardingTour />
|
||||||
</RightRailProvider>
|
</TourOrchestrationProvider>
|
||||||
</SignatureProvider>
|
</RightRailProvider>
|
||||||
</ViewerProvider>
|
</SignatureProvider>
|
||||||
</SidebarProvider>
|
</ViewerProvider>
|
||||||
</HotkeyProvider>
|
</SidebarProvider>
|
||||||
</ToolWorkflowProvider>
|
</HotkeyProvider>
|
||||||
</FilesModalProvider>
|
</ToolWorkflowProvider>
|
||||||
</NavigationProvider>
|
</FilesModalProvider>
|
||||||
|
</NavigationProvider>
|
||||||
|
</ToolRegistryProvider>
|
||||||
</FileContextProvider>
|
</FileContextProvider>
|
||||||
</OnboardingProvider>
|
</OnboardingProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ interface AutomationCreationProps {
|
|||||||
existingAutomation?: AutomationConfig;
|
existingAutomation?: AutomationConfig;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onComplete: (automation: AutomationConfig) => void;
|
onComplete: (automation: AutomationConfig) => void;
|
||||||
toolRegistry: ToolRegistry;
|
toolRegistry: Partial<ToolRegistry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {
|
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
|||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
import { Tooltip } from '../../shared/Tooltip';
|
import { Tooltip } from '../../shared/Tooltip';
|
||||||
import { ToolIcon } from '../../shared/ToolIcon';
|
import { ToolIcon } from '../../shared/ToolIcon';
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||||
import { ToolId } from 'src/types/toolId';
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
interface AutomationEntryProps {
|
interface AutomationEntryProps {
|
||||||
@ -32,7 +32,7 @@ interface AutomationEntryProps {
|
|||||||
/** Copy handler (for suggested automations) */
|
/** Copy handler (for suggested automations) */
|
||||||
onCopy?: () => void;
|
onCopy?: () => void;
|
||||||
/** Tool registry to resolve operation names */
|
/** Tool registry to resolve operation names */
|
||||||
toolRegistry?: Record<ToolId, ToolRegistryEntry>;
|
toolRegistry?: Partial<ToolRegistry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationEntry({
|
export default function AutomationEntry({
|
||||||
@ -56,8 +56,9 @@ export default function AutomationEntry({
|
|||||||
|
|
||||||
// Helper function to resolve tool display names
|
// Helper function to resolve tool display names
|
||||||
const getToolDisplayName = (operation: string): string => {
|
const getToolDisplayName = (operation: string): string => {
|
||||||
if (toolRegistry?.[operation as ToolId]?.name) {
|
const entry = toolRegistry?.[operation as ToolId];
|
||||||
return toolRegistry[operation as ToolId].name;
|
if (entry?.name) {
|
||||||
|
return entry.name;
|
||||||
}
|
}
|
||||||
// Fallback to translation or operation key
|
// Fallback to translation or operation key
|
||||||
return t(`${operation}.title`, operation);
|
return t(`${operation}.title`, operation);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core";
|
|||||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
import CheckIcon from "@mui/icons-material/Check";
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import { useFileSelection } from "../../../contexts/FileContext";
|
import { useFileSelection } from "../../../contexts/FileContext";
|
||||||
import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry";
|
import { useToolRegistry } from "../../../contexts/ToolRegistryContext";
|
||||||
import { AutomationConfig, ExecutionStep } from "../../../types/automation";
|
import { AutomationConfig, ExecutionStep } from "../../../types/automation";
|
||||||
import { AUTOMATION_CONSTANTS, EXECUTION_STATUS } from "../../../constants/automation";
|
import { AUTOMATION_CONSTANTS, EXECUTION_STATUS } from "../../../constants/automation";
|
||||||
import { useResourceCleanup } from "../../../utils/resourceManager";
|
import { useResourceCleanup } from "../../../utils/resourceManager";
|
||||||
@ -18,7 +18,8 @@ interface AutomationRunProps {
|
|||||||
export default function AutomationRun({ automation, onComplete, automateOperation }: AutomationRunProps) {
|
export default function AutomationRun({ automation, onComplete, automateOperation }: AutomationRunProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const { regularTools } = useToolRegistry();
|
||||||
|
const toolRegistry = regularTools;
|
||||||
const cleanup = useResourceCleanup();
|
const cleanup = useResourceCleanup();
|
||||||
|
|
||||||
// Progress tracking state
|
// Progress tracking state
|
||||||
|
|||||||
@ -6,8 +6,7 @@ import AutomationEntry from "./AutomationEntry";
|
|||||||
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
|
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
|
||||||
import { AutomationConfig, SuggestedAutomation } from "../../../types/automation";
|
import { AutomationConfig, SuggestedAutomation } from "../../../types/automation";
|
||||||
import { iconMap } from './iconMap';
|
import { iconMap } from './iconMap';
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||||
import { ToolId } from '../../../types/toolId';
|
|
||||||
|
|
||||||
interface AutomationSelectionProps {
|
interface AutomationSelectionProps {
|
||||||
savedAutomations: AutomationConfig[];
|
savedAutomations: AutomationConfig[];
|
||||||
@ -16,7 +15,7 @@ interface AutomationSelectionProps {
|
|||||||
onEdit: (automation: AutomationConfig) => void;
|
onEdit: (automation: AutomationConfig) => void;
|
||||||
onDelete: (automation: AutomationConfig) => void;
|
onDelete: (automation: AutomationConfig) => void;
|
||||||
onCopyFromSuggested: (automation: SuggestedAutomation) => void;
|
onCopyFromSuggested: (automation: SuggestedAutomation) => void;
|
||||||
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
toolRegistry: Partial<ToolRegistry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationSelection({
|
export default function AutomationSelection({
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import CheckIcon from '@mui/icons-material/Check';
|
|||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||||
|
import { ToolId } from '../../../types/toolId';
|
||||||
import { getAvailableToExtensions } from '../../../utils/convertUtils';
|
import { getAvailableToExtensions } from '../../../utils/convertUtils';
|
||||||
interface ToolConfigurationModalProps {
|
interface ToolConfigurationModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@ -26,7 +27,7 @@ interface ToolConfigurationModalProps {
|
|||||||
};
|
};
|
||||||
onSave: (parameters: any) => void;
|
onSave: (parameters: any) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
toolRegistry: ToolRegistry;
|
toolRegistry: Partial<ToolRegistry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, toolRegistry }: ToolConfigurationModalProps) {
|
export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, toolRegistry }: ToolConfigurationModalProps) {
|
||||||
@ -35,7 +36,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
|
|||||||
const [parameters, setParameters] = useState<any>({});
|
const [parameters, setParameters] = useState<any>({});
|
||||||
|
|
||||||
// Get tool info from registry
|
// Get tool info from registry
|
||||||
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
|
const toolInfo = toolRegistry[tool.operation as ToolId];
|
||||||
const SettingsComponent = toolInfo?.automationSettings;
|
const SettingsComponent = toolInfo?.automationSettings;
|
||||||
|
|
||||||
// Initialize parameters from tool (which should contain defaults from registry)
|
// Initialize parameters from tool (which should contain defaults from registry)
|
||||||
|
|||||||
@ -5,14 +5,14 @@ import SettingsIcon from "@mui/icons-material/Settings";
|
|||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
||||||
import { AutomationTool } from "../../../types/automation";
|
import { AutomationTool } from "../../../types/automation";
|
||||||
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
|
import { ToolRegistry } from "../../../data/toolsTaxonomy";
|
||||||
import { ToolId } from "../../../types/toolId";
|
import { ToolId } from "../../../types/toolId";
|
||||||
import ToolSelector from "./ToolSelector";
|
import ToolSelector from "./ToolSelector";
|
||||||
import AutomationEntry from "./AutomationEntry";
|
import AutomationEntry from "./AutomationEntry";
|
||||||
|
|
||||||
interface ToolListProps {
|
interface ToolListProps {
|
||||||
tools: AutomationTool[];
|
tools: AutomationTool[];
|
||||||
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
toolRegistry: Partial<ToolRegistry>;
|
||||||
onToolUpdate: (index: number, updates: Partial<AutomationTool>) => void;
|
onToolUpdate: (index: number, updates: Partial<AutomationTool>) => void;
|
||||||
onToolRemove: (index: number) => void;
|
onToolRemove: (index: number) => void;
|
||||||
onToolConfigure: (index: number) => void;
|
onToolConfigure: (index: number) => void;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Stack, Text, ScrollArea } from '@mantine/core';
|
import { Stack, Text, ScrollArea } from '@mantine/core';
|
||||||
import { ToolRegistryEntry, getToolSupportsAutomate } from '../../../data/toolsTaxonomy';
|
import { ToolRegistryEntry, ToolRegistry, getToolSupportsAutomate } from '../../../data/toolsTaxonomy';
|
||||||
import { useToolSections } from '../../../hooks/useToolSections';
|
import { useToolSections } from '../../../hooks/useToolSections';
|
||||||
import { renderToolButtons } from '../shared/renderToolButtons';
|
import { renderToolButtons } from '../shared/renderToolButtons';
|
||||||
import ToolSearch from '../toolPicker/ToolSearch';
|
import ToolSearch from '../toolPicker/ToolSearch';
|
||||||
@ -11,7 +11,7 @@ import { ToolId } from '../../../types/toolId';
|
|||||||
interface ToolSelectorProps {
|
interface ToolSelectorProps {
|
||||||
onSelect: (toolKey: string) => void;
|
onSelect: (toolKey: string) => void;
|
||||||
excludeTools?: string[];
|
excludeTools?: string[];
|
||||||
toolRegistry: Record<ToolId, ToolRegistryEntry>; // Pass registry as prop to break circular dependency
|
toolRegistry: Partial<ToolRegistry>; // Pass registry as prop to break circular dependency
|
||||||
selectedValue?: string; // For showing current selection when editing existing tool
|
selectedValue?: string; // For showing current selection when editing existing tool
|
||||||
placeholder?: string; // Custom placeholder text
|
placeholder?: string; // Custom placeholder text
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ export default function ToolSelector({
|
|||||||
|
|
||||||
// Create filtered tool registry for ToolSearch
|
// Create filtered tool registry for ToolSearch
|
||||||
const filteredToolRegistry = useMemo(() => {
|
const filteredToolRegistry = useMemo(() => {
|
||||||
const registry: Record<ToolId, ToolRegistryEntry> = {} as Record<ToolId, ToolRegistryEntry>;
|
const registry: Partial<ToolRegistry> = {};
|
||||||
baseFilteredTools.forEach(([key, tool]) => {
|
baseFilteredTools.forEach(([key, tool]) => {
|
||||||
registry[key as ToolId] = tool;
|
registry[key as ToolId] = tool;
|
||||||
});
|
});
|
||||||
@ -142,10 +142,10 @@ export default function ToolSelector({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get display value for selected tool
|
// Get display value for selected tool
|
||||||
|
const selectedTool = selectedValue ? toolRegistry[selectedValue as ToolId] : undefined;
|
||||||
|
|
||||||
const getDisplayValue = () => {
|
const getDisplayValue = () => {
|
||||||
if (selectedValue && toolRegistry[selectedValue as ToolId]) {
|
if (selectedTool) return selectedTool.name;
|
||||||
return toolRegistry[selectedValue as ToolId].name;
|
|
||||||
}
|
|
||||||
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
|
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -153,12 +153,18 @@ export default function ToolSelector({
|
|||||||
<div ref={containerRef} className='rounded-xl'>
|
<div ref={containerRef} className='rounded-xl'>
|
||||||
{/* Always show the target - either selected tool or search input */}
|
{/* Always show the target - either selected tool or search input */}
|
||||||
|
|
||||||
{selectedValue && toolRegistry[selectedValue as ToolId] && !opened ? (
|
{selectedTool && !opened ? (
|
||||||
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
|
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
|
||||||
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
||||||
borderRadius: "var(--mantine-radius-lg)" }}>
|
borderRadius: "var(--mantine-radius-lg)" }}>
|
||||||
<ToolButton id={'tool' as ToolId} tool={toolRegistry[selectedValue as ToolId]} isSelected={false}
|
<ToolButton
|
||||||
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
|
id={'tool' as ToolId}
|
||||||
|
tool={selectedTool}
|
||||||
|
isSelected={false}
|
||||||
|
onSelect={()=>{}}
|
||||||
|
rounded={true}
|
||||||
|
disableNavigation={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Show search input when no tool selected OR when dropdown is opened
|
// Show search input when no tool selected OR when dropdown is opened
|
||||||
|
|||||||
@ -3,13 +3,7 @@ import { useHistoryCapability } from '@embedpdf/plugin-history/react';
|
|||||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||||
import { useSignature } from '../../contexts/SignatureContext';
|
import { useSignature } from '../../contexts/SignatureContext';
|
||||||
import { uuidV4 } from '@embedpdf/models';
|
import { uuidV4 } from '@embedpdf/models';
|
||||||
|
import type { HistoryAPI } from './viewerTypes';
|
||||||
export interface HistoryAPI {
|
|
||||||
undo: () => void;
|
|
||||||
redo: () => void;
|
|
||||||
canUndo: () => boolean;
|
|
||||||
canRedo: () => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge(_, ref) {
|
export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge(_, ref) {
|
||||||
const { provides: historyApi } = useHistoryCapability();
|
const { provides: historyApi } = useHistoryCapability();
|
||||||
|
|||||||
@ -34,8 +34,9 @@ import { SpreadAPIBridge } from './SpreadAPIBridge';
|
|||||||
import { SearchAPIBridge } from './SearchAPIBridge';
|
import { SearchAPIBridge } from './SearchAPIBridge';
|
||||||
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
||||||
import { RotateAPIBridge } from './RotateAPIBridge';
|
import { RotateAPIBridge } from './RotateAPIBridge';
|
||||||
import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge';
|
import { SignatureAPIBridge } from './SignatureAPIBridge';
|
||||||
import { HistoryAPIBridge, HistoryAPI } from './HistoryAPIBridge';
|
import { HistoryAPIBridge } from './HistoryAPIBridge';
|
||||||
|
import type { SignatureAPI, HistoryAPI } from './viewerTypes';
|
||||||
import { ExportAPIBridge } from './ExportAPIBridge';
|
import { ExportAPIBridge } from './ExportAPIBridge';
|
||||||
|
|
||||||
interface LocalEmbedPDFProps {
|
interface LocalEmbedPDFProps {
|
||||||
|
|||||||
@ -2,17 +2,7 @@ import { useImperativeHandle, forwardRef, useEffect } from 'react';
|
|||||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||||
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
|
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
|
||||||
import { useSignature } from '../../contexts/SignatureContext';
|
import { useSignature } from '../../contexts/SignatureContext';
|
||||||
|
import type { SignatureAPI } from './viewerTypes';
|
||||||
export interface SignatureAPI {
|
|
||||||
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void;
|
|
||||||
activateDrawMode: () => void;
|
|
||||||
activateSignaturePlacementMode: () => void;
|
|
||||||
activateDeleteMode: () => void;
|
|
||||||
deleteAnnotation: (annotationId: string, pageIndex: number) => void;
|
|
||||||
updateDrawSettings: (color: string, size: number) => void;
|
|
||||||
deactivateTools: () => void;
|
|
||||||
getPageAnnotations: (pageIndex: number) => Promise<any[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
|
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
|
||||||
const { provides: annotationApi } = useAnnotationCapability();
|
const { provides: annotationApi } = useAnnotationCapability();
|
||||||
|
|||||||
24
frontend/src/components/viewer/viewerTypes.ts
Normal file
24
frontend/src/components/viewer/viewerTypes.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export interface SignatureAPI {
|
||||||
|
addImageSignature: (
|
||||||
|
signatureData: string,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
pageIndex: number
|
||||||
|
) => void;
|
||||||
|
activateDrawMode: () => void;
|
||||||
|
activateSignaturePlacementMode: () => void;
|
||||||
|
activateDeleteMode: () => void;
|
||||||
|
deleteAnnotation: (annotationId: string, pageIndex: number) => void;
|
||||||
|
updateDrawSettings: (color: string, size: number) => void;
|
||||||
|
deactivateTools: () => void;
|
||||||
|
getPageAnnotations: (pageIndex: number) => Promise<any[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HistoryAPI {
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
canUndo: () => boolean;
|
||||||
|
canRedo: () => boolean;
|
||||||
|
}
|
||||||
@ -1,10 +1,3 @@
|
|||||||
import { useAppConfig } from '../hooks/useAppConfig';
|
|
||||||
|
|
||||||
// Get base URL from app config with fallback
|
|
||||||
export const getBaseUrl = (): string => {
|
|
||||||
const { config } = useAppConfig();
|
|
||||||
return config?.baseUrl || 'https://stirling.com';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Base path from Vite config - build-time constant, normalized (no trailing slash)
|
// Base path from Vite config - build-time constant, normalized (no trailing slash)
|
||||||
// When no subpath, use empty string instead of '.' to avoid relative path issues
|
// When no subpath, use empty string instead of '.' to avoid relative path issues
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback } from 'react';
|
import React, { createContext, useContext, useReducer, useCallback } from 'react';
|
||||||
import { WorkbenchType, getDefaultWorkbench } from '../types/workbench';
|
import { WorkbenchType, getDefaultWorkbench } from '../types/workbench';
|
||||||
import { ToolId, isValidToolId } from '../types/toolId';
|
import { ToolId, isValidToolId } from '../types/toolId';
|
||||||
import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry';
|
import { useToolRegistry } from './ToolRegistryContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NavigationContext - Complete navigation management system
|
* NavigationContext - Complete navigation management system
|
||||||
@ -107,7 +107,7 @@ export const NavigationProvider: React.FC<{
|
|||||||
enableUrlSync?: boolean;
|
enableUrlSync?: boolean;
|
||||||
}> = ({ children }) => {
|
}> = ({ children }) => {
|
||||||
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const { allTools: toolRegistry } = useToolRegistry();
|
||||||
const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
|
const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
|
||||||
|
|
||||||
const actions: NavigationContextActions = {
|
const actions: NavigationContextActions = {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react';
|
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react';
|
||||||
import { SignParameters } from '../hooks/tools/sign/useSignParameters';
|
import { SignParameters } from '../hooks/tools/sign/useSignParameters';
|
||||||
import { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
|
import type { SignatureAPI, HistoryAPI } from '../components/viewer/viewerTypes';
|
||||||
import { HistoryAPI } from '../components/viewer/HistoryAPIBridge';
|
|
||||||
|
|
||||||
// Signature state interface
|
// Signature state interface
|
||||||
interface SignatureState {
|
interface SignatureState {
|
||||||
|
|||||||
30
frontend/src/contexts/ToolRegistryContext.tsx
Normal file
30
frontend/src/contexts/ToolRegistryContext.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToolRegistryEntry,
|
||||||
|
ToolRegistry,
|
||||||
|
RegularToolRegistry,
|
||||||
|
SuperToolRegistry,
|
||||||
|
LinkToolRegistry,
|
||||||
|
} from '../data/toolsTaxonomy';
|
||||||
|
import type { ToolId } from '../types/toolId';
|
||||||
|
|
||||||
|
export interface ToolRegistryCatalog {
|
||||||
|
regularTools: RegularToolRegistry;
|
||||||
|
superTools: SuperToolRegistry;
|
||||||
|
linkTools: LinkToolRegistry;
|
||||||
|
allTools: ToolRegistry;
|
||||||
|
getToolById: (toolId: ToolId | null) => ToolRegistryEntry | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolRegistryContext = createContext<ToolRegistryCatalog | null>(null);
|
||||||
|
|
||||||
|
export const useToolRegistry = (): ToolRegistryCatalog => {
|
||||||
|
const context = useContext(ToolRegistryContext);
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error('useToolRegistry must be used within a ToolRegistryProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolRegistryContext;
|
||||||
44
frontend/src/contexts/ToolRegistryProvider.tsx
Normal file
44
frontend/src/contexts/ToolRegistryProvider.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import type { ToolId } from '../types/toolId';
|
||||||
|
import type { ToolRegistry } from '../data/toolsTaxonomy';
|
||||||
|
import ToolRegistryContext, { ToolRegistryCatalog } from './ToolRegistryContext';
|
||||||
|
import { useTranslatedToolCatalog } from '../data/useTranslatedToolRegistry';
|
||||||
|
|
||||||
|
interface ToolRegistryProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolRegistryProvider: React.FC<ToolRegistryProviderProps> = ({ children }) => {
|
||||||
|
const catalog = useTranslatedToolCatalog();
|
||||||
|
|
||||||
|
const contextValue = useMemo<ToolRegistryCatalog>(() => {
|
||||||
|
const { regularTools, superTools, linkTools } = catalog;
|
||||||
|
const allTools: ToolRegistry = {
|
||||||
|
...regularTools,
|
||||||
|
...superTools,
|
||||||
|
...linkTools,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getToolById = (toolId: ToolId | null) => {
|
||||||
|
if (!toolId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return allTools[toolId] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
regularTools,
|
||||||
|
superTools,
|
||||||
|
linkTools,
|
||||||
|
allTools,
|
||||||
|
getToolById,
|
||||||
|
};
|
||||||
|
}, [catalog]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolRegistryContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</ToolRegistryContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -20,6 +20,7 @@ import {
|
|||||||
} from './toolWorkflow/toolWorkflowState';
|
} from './toolWorkflow/toolWorkflowState';
|
||||||
import type { ToolPanelMode } from '../constants/toolPanel';
|
import type { ToolPanelMode } from '../constants/toolPanel';
|
||||||
import { usePreferences } from './PreferencesContext';
|
import { usePreferences } from './PreferencesContext';
|
||||||
|
import { useToolRegistry } from './ToolRegistryContext';
|
||||||
|
|
||||||
// State interface
|
// State interface
|
||||||
// Types and reducer/state moved to './toolWorkflow/state'
|
// Types and reducer/state moved to './toolWorkflow/state'
|
||||||
@ -111,10 +112,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
const navigationState = useNavigationState();
|
const navigationState = useNavigationState();
|
||||||
|
|
||||||
// Tool management hook
|
// Tool management hook
|
||||||
const {
|
const { toolRegistry, getSelectedTool } = useToolManagement();
|
||||||
toolRegistry,
|
const { allTools } = useToolRegistry();
|
||||||
getSelectedTool,
|
|
||||||
} = useToolManagement();
|
|
||||||
|
|
||||||
// Tool history hook
|
// Tool history hook
|
||||||
const {
|
const {
|
||||||
@ -315,7 +314,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
// Filter tools based on search query with fuzzy matching (name, description, id, synonyms)
|
// Filter tools based on search query with fuzzy matching (name, description, id, synonyms)
|
||||||
const filteredTools = useMemo(() => {
|
const filteredTools = useMemo(() => {
|
||||||
if (!toolRegistry) return [];
|
if (!toolRegistry) return [];
|
||||||
return filterToolRegistryByQuery(toolRegistry as ToolRegistry, state.searchQuery);
|
return filterToolRegistryByQuery(toolRegistry, state.searchQuery);
|
||||||
}, [toolRegistry, state.searchQuery]);
|
}, [toolRegistry, state.searchQuery]);
|
||||||
|
|
||||||
const isPanelVisible = useMemo(() =>
|
const isPanelVisible = useMemo(() =>
|
||||||
@ -327,7 +326,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
navigationState.selectedTool,
|
navigationState.selectedTool,
|
||||||
handleToolSelect,
|
handleToolSelect,
|
||||||
handleBackToTools,
|
handleBackToTools,
|
||||||
toolRegistry as ToolRegistry,
|
allTools,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
|
import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
|
||||||
import { BaseToolProps } from '../types/tool';
|
import { BaseToolProps } from '../types/tool';
|
||||||
import { WorkbenchType } from '../types/workbench';
|
import { WorkbenchType } from '../types/workbench';
|
||||||
import { ToolId } from '../types/toolId';
|
import { LinkToolId, RegularToolId, SuperToolId, ToolId, ToolKind } from '../types/toolId';
|
||||||
import DrawRoundedIcon from '@mui/icons-material/DrawRounded';
|
import DrawRoundedIcon from '@mui/icons-material/DrawRounded';
|
||||||
import SecurityRoundedIcon from '@mui/icons-material/SecurityRounded';
|
import SecurityRoundedIcon from '@mui/icons-material/SecurityRounded';
|
||||||
import VerifiedUserRoundedIcon from '@mui/icons-material/VerifiedUserRounded';
|
import VerifiedUserRoundedIcon from '@mui/icons-material/VerifiedUserRounded';
|
||||||
@ -47,7 +47,7 @@ export type ToolRegistryEntry = {
|
|||||||
supportedFormats?: string[];
|
supportedFormats?: string[];
|
||||||
endpoints?: string[];
|
endpoints?: string[];
|
||||||
link?: string;
|
link?: string;
|
||||||
type?: string;
|
kind?: ToolKind;
|
||||||
// Workbench type for navigation
|
// Workbench type for navigation
|
||||||
workbench?: WorkbenchType;
|
workbench?: WorkbenchType;
|
||||||
// Operation configuration for automation
|
// Operation configuration for automation
|
||||||
@ -60,6 +60,9 @@ export type ToolRegistryEntry = {
|
|||||||
synonyms?: string[];
|
synonyms?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RegularToolRegistry = Record<RegularToolId, ToolRegistryEntry>;
|
||||||
|
export type SuperToolRegistry = Record<SuperToolId, ToolRegistryEntry>;
|
||||||
|
export type LinkToolRegistry = Record<LinkToolId, ToolRegistryEntry>;
|
||||||
export type ToolRegistry = Record<ToolId, ToolRegistryEntry>;
|
export type ToolRegistry = Record<ToolId, ToolRegistryEntry>;
|
||||||
|
|
||||||
export const SUBCATEGORY_ORDER: SubcategoryId[] = [
|
export const SUBCATEGORY_ORDER: SubcategoryId[] = [
|
||||||
|
|||||||
@ -13,7 +13,15 @@ import RemovePages from "../tools/RemovePages";
|
|||||||
import ReorganizePages from "../tools/ReorganizePages";
|
import ReorganizePages from "../tools/ReorganizePages";
|
||||||
import { reorganizePagesOperationConfig } from "../hooks/tools/reorganizePages/useReorganizePagesOperation";
|
import { reorganizePagesOperationConfig } from "../hooks/tools/reorganizePages/useReorganizePagesOperation";
|
||||||
import RemovePassword from "../tools/RemovePassword";
|
import RemovePassword from "../tools/RemovePassword";
|
||||||
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
|
import {
|
||||||
|
SubcategoryId,
|
||||||
|
ToolCategoryId,
|
||||||
|
ToolRegistry,
|
||||||
|
RegularToolRegistry,
|
||||||
|
SuperToolRegistry,
|
||||||
|
LinkToolRegistry,
|
||||||
|
} from "./toolsTaxonomy";
|
||||||
|
import { isSuperToolId, isLinkToolId } from '../types/toolId';
|
||||||
import AdjustContrast from "../tools/AdjustContrast";
|
import AdjustContrast from "../tools/AdjustContrast";
|
||||||
import AdjustContrastSingleStepSettings from "../components/tools/adjustContrast/AdjustContrastSingleStepSettings";
|
import AdjustContrastSingleStepSettings from "../components/tools/adjustContrast/AdjustContrastSingleStepSettings";
|
||||||
import { adjustContrastOperationConfig } from "../hooks/tools/adjustContrast/useAdjustContrastOperation";
|
import { adjustContrastOperationConfig } from "../hooks/tools/adjustContrast/useAdjustContrastOperation";
|
||||||
@ -109,14 +117,18 @@ import RemoveBlanksSettings from "../components/tools/removeBlanks/RemoveBlanksS
|
|||||||
import AddPageNumbersAutomationSettings from "../components/tools/addPageNumbers/AddPageNumbersAutomationSettings";
|
import AddPageNumbersAutomationSettings from "../components/tools/addPageNumbers/AddPageNumbersAutomationSettings";
|
||||||
import OverlayPdfsSettings from "../components/tools/overlayPdfs/OverlayPdfsSettings";
|
import OverlayPdfsSettings from "../components/tools/overlayPdfs/OverlayPdfsSettings";
|
||||||
import ValidateSignature from "../tools/ValidateSignature";
|
import ValidateSignature from "../tools/ValidateSignature";
|
||||||
|
import Automate from "../tools/Automate";
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
|
||||||
|
|
||||||
// Convert tool supported file formats
|
|
||||||
import { CONVERT_SUPPORTED_FORMATS } from "../constants/convertSupportedFornats";
|
import { CONVERT_SUPPORTED_FORMATS } from "../constants/convertSupportedFornats";
|
||||||
|
|
||||||
|
export interface TranslatedToolCatalog {
|
||||||
|
allTools: ToolRegistry;
|
||||||
|
regularTools: RegularToolRegistry;
|
||||||
|
superTools: SuperToolRegistry;
|
||||||
|
linkTools: LinkToolRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
// Hook to get the translated tool registry
|
// Hook to get the translated tool registry
|
||||||
export function useFlatToolRegistry(): ToolRegistry {
|
export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
@ -564,7 +576,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
automate: {
|
automate: {
|
||||||
icon: <LocalIcon icon="automation-outline" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="automation-outline" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.automate.title", "Automate"),
|
name: t("home.automate.title", "Automate"),
|
||||||
component: React.lazy(() => import("../tools/Automate")),
|
component: Automate,
|
||||||
description: t(
|
description: t(
|
||||||
"home.automate.desc",
|
"home.automate.desc",
|
||||||
"Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
|
"Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
|
||||||
@ -829,15 +841,26 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (showPlaceholderTools) {
|
const regularTools = {} as RegularToolRegistry;
|
||||||
return allTools;
|
const superTools = {} as SuperToolRegistry;
|
||||||
}
|
const linkTools = {} as LinkToolRegistry;
|
||||||
const filteredTools = Object.keys(allTools)
|
|
||||||
.filter((key) => allTools[key as ToolId].component !== null || allTools[key as ToolId].link)
|
Object.entries(allTools).forEach(([key, entry]) => {
|
||||||
.reduce((obj, key) => {
|
const toolId = key as ToolId;
|
||||||
obj[key as ToolId] = allTools[key as ToolId];
|
if (isSuperToolId(toolId)) {
|
||||||
return obj;
|
superTools[toolId] = entry;
|
||||||
}, {} as ToolRegistry);
|
} else if (isLinkToolId(toolId)) {
|
||||||
return filteredTools;
|
linkTools[toolId] = entry;
|
||||||
|
} else {
|
||||||
|
regularTools[toolId] = entry;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allTools,
|
||||||
|
regularTools,
|
||||||
|
superTools,
|
||||||
|
linkTools,
|
||||||
|
};
|
||||||
}, [t]); // Only re-compute when translations change
|
}, [t]); // Only re-compute when translations change
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { executeAutomationSequence } from '../../../utils/automationExecutor';
|
import { executeAutomationSequence } from '../../../utils/automationExecutor';
|
||||||
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
import { useToolRegistry } from '../../../contexts/ToolRegistryContext';
|
||||||
import { AutomateParameters } from '../../../types/automation';
|
import { AutomateParameters } from '../../../types/automation';
|
||||||
|
|
||||||
export function useAutomateOperation() {
|
export function useAutomateOperation() {
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const { allTools } = useToolRegistry();
|
||||||
|
const toolRegistry = allTools;
|
||||||
|
|
||||||
const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => {
|
const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => {
|
||||||
console.log('🚀 Starting automation execution via customProcessor', { params, files });
|
console.log('🚀 Starting automation execution via customProcessor', { params, files });
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { ToolId } from 'src/types/toolId';
|
|||||||
interface UseAutomationFormProps {
|
interface UseAutomationFormProps {
|
||||||
mode: AutomationMode;
|
mode: AutomationMode;
|
||||||
existingAutomation?: AutomationConfig;
|
existingAutomation?: AutomationConfig;
|
||||||
toolRegistry: ToolRegistry;
|
toolRegistry: Partial<ToolRegistry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
|
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
|
||||||
@ -21,12 +21,12 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
|||||||
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
||||||
|
|
||||||
const getToolName = useCallback((operation: string) => {
|
const getToolName = useCallback((operation: string) => {
|
||||||
const tool = toolRegistry?.[operation as keyof ToolRegistry] as any;
|
const tool = toolRegistry?.[operation as ToolId] as any;
|
||||||
return tool?.name || t(`tools.${operation}.name`, operation);
|
return tool?.name || t(`tools.${operation}.name`, operation);
|
||||||
}, [toolRegistry, t]);
|
}, [toolRegistry, t]);
|
||||||
|
|
||||||
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
|
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
|
||||||
const config = toolRegistry[operation as keyof ToolRegistry]?.operationConfig;
|
const config = toolRegistry[operation as ToolId]?.operationConfig;
|
||||||
if (config?.defaultParameters) {
|
if (config?.defaultParameters) {
|
||||||
return { ...config.defaultParameters };
|
return { ...config.defaultParameters };
|
||||||
}
|
}
|
||||||
|
|||||||
6
frontend/src/hooks/useBaseUrl.ts
Normal file
6
frontend/src/hooks/useBaseUrl.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useAppConfig } from './useAppConfig';
|
||||||
|
|
||||||
|
export const useBaseUrl = (): string => {
|
||||||
|
const { config } = useAppConfig();
|
||||||
|
return config?.baseUrl || 'https://demo.stirlingpdf.com';
|
||||||
|
};
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { useState, useCallback, useMemo } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useToolRegistry } from "../contexts/ToolRegistryContext";
|
||||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
|
||||||
import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "../data/toolsTaxonomy";
|
import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "../data/toolsTaxonomy";
|
||||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
@ -15,19 +14,19 @@ interface ToolManagementResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useToolManagement = (): ToolManagementResult => {
|
export const useToolManagement = (): ToolManagementResult => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<FileId[]>([]);
|
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<FileId[]>([]);
|
||||||
|
|
||||||
// Build endpoints list from registry entries with fallback to legacy mapping
|
// Build endpoints list from registry entries with fallback to legacy mapping
|
||||||
const baseRegistry = useFlatToolRegistry();
|
const { allTools } = useToolRegistry();
|
||||||
|
const baseRegistry = allTools;
|
||||||
|
|
||||||
const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]);
|
const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]);
|
||||||
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
||||||
|
|
||||||
const isToolAvailable = useCallback((toolKey: string): boolean => {
|
const isToolAvailable = useCallback((toolKey: string): boolean => {
|
||||||
if (endpointsLoading) return true;
|
if (endpointsLoading) return true;
|
||||||
const endpoints = baseRegistry[toolKey as keyof typeof baseRegistry]?.endpoints || [];
|
const tool = baseRegistry[toolKey as ToolId];
|
||||||
|
const endpoints = tool?.endpoints || [];
|
||||||
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
|
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
|
||||||
}, [endpointsLoading, endpointStatus, baseRegistry]);
|
}, [endpointsLoading, endpointStatus, baseRegistry]);
|
||||||
|
|
||||||
@ -35,16 +34,18 @@ export const useToolManagement = (): ToolManagementResult => {
|
|||||||
const availableToolRegistry: Partial<ToolRegistry> = {};
|
const availableToolRegistry: Partial<ToolRegistry> = {};
|
||||||
(Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
|
(Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
|
||||||
if (isToolAvailable(toolKey)) {
|
if (isToolAvailable(toolKey)) {
|
||||||
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
|
const baseTool = baseRegistry[toolKey];
|
||||||
availableToolRegistry[toolKey as ToolId] = {
|
if (baseTool) {
|
||||||
...baseTool,
|
availableToolRegistry[toolKey] = {
|
||||||
name: baseTool.name,
|
...baseTool,
|
||||||
description: baseTool.description,
|
name: baseTool.name,
|
||||||
};
|
description: baseTool.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return availableToolRegistry;
|
return availableToolRegistry;
|
||||||
}, [isToolAvailable, t, baseRegistry]);
|
}, [isToolAvailable, baseRegistry]);
|
||||||
|
|
||||||
const getSelectedTool = useCallback((toolKey: ToolId | null): ToolRegistryEntry | null => {
|
const getSelectedTool = useCallback((toolKey: ToolId | null): ToolRegistryEntry | null => {
|
||||||
return toolKey ? toolRegistry[toolKey] || null : null;
|
return toolKey ? toolRegistry[toolKey] || null : null;
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
|||||||
import { Group, useMantineColorScheme } from "@mantine/core";
|
import { Group, useMantineColorScheme } from "@mantine/core";
|
||||||
import { useSidebarContext } from "../contexts/SidebarContext";
|
import { useSidebarContext } from "../contexts/SidebarContext";
|
||||||
import { useDocumentMeta } from "../hooks/useDocumentMeta";
|
import { useDocumentMeta } from "../hooks/useDocumentMeta";
|
||||||
import { BASE_PATH, getBaseUrl } from "../constants/app";
|
import { BASE_PATH } from "../constants/app";
|
||||||
|
import { useBaseUrl } from "../hooks/useBaseUrl";
|
||||||
import { useMediaQuery } from "@mantine/hooks";
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
import AppsIcon from '@mui/icons-material/AppsRounded';
|
import AppsIcon from '@mui/icons-material/AppsRounded';
|
||||||
|
|
||||||
@ -135,7 +136,7 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
}, [isMobile, activeMobileView, selectedTool, setLeftPanelView]);
|
}, [isMobile, activeMobileView, selectedTool, setLeftPanelView]);
|
||||||
|
|
||||||
const baseUrl = getBaseUrl();
|
const baseUrl = useBaseUrl();
|
||||||
|
|
||||||
// Update document meta when tool changes
|
// Update document meta when tool changes
|
||||||
useDocumentMeta({
|
useDocumentMeta({
|
||||||
|
|||||||
@ -119,5 +119,49 @@ Object.defineProperty(window, 'matchMedia', {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Provide a minimal DOMMatrix implementation for pdf.js in the test environment
|
||||||
|
if (typeof globalThis.DOMMatrix === 'undefined') {
|
||||||
|
class DOMMatrixStub {
|
||||||
|
a = 1;
|
||||||
|
b = 0;
|
||||||
|
c = 0;
|
||||||
|
d = 1;
|
||||||
|
e = 0;
|
||||||
|
f = 0;
|
||||||
|
|
||||||
|
constructor(init?: string | number[]) {
|
||||||
|
if (Array.isArray(init) && init.length === 6) {
|
||||||
|
[this.a, this.b, this.c, this.d, this.e, this.f] = init as [number, number, number, number, number, number];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplySelf(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
translateSelf(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSelf(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateSelf(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inverse(): this {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'DOMMatrix', {
|
||||||
|
value: DOMMatrixStub,
|
||||||
|
writable: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Set global test timeout to prevent hangs
|
// Set global test timeout to prevent hangs
|
||||||
vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 });
|
vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 });
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import AutomationRun from "../components/tools/automate/AutomationRun";
|
|||||||
|
|
||||||
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
|
import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperation";
|
||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps } from "../types/tool";
|
||||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
import { useToolRegistry } from "../contexts/ToolRegistryContext";
|
||||||
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
||||||
import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "../types/automation";
|
import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "../types/automation";
|
||||||
import { AUTOMATION_STEPS } from "../constants/automation";
|
import { AUTOMATION_STEPS } from "../constants/automation";
|
||||||
@ -27,7 +27,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const [stepData, setStepData] = useState<AutomationStepData>({ step: AUTOMATION_STEPS.SELECTION });
|
const [stepData, setStepData] = useState<AutomationStepData>({ step: AUTOMATION_STEPS.SELECTION });
|
||||||
|
|
||||||
const automateOperation = useAutomateOperation();
|
const automateOperation = useAutomateOperation();
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const { regularTools: toolRegistry } = useToolRegistry();
|
||||||
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
|
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
|
||||||
const { savedAutomations, deleteAutomation, refreshAutomations, copyFromSuggested } = useSavedAutomations();
|
const { savedAutomations, deleteAutomation, refreshAutomations, copyFromSuggested } = useSavedAutomations();
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// Define all possible tool IDs as source of truth
|
export type ToolKind = 'regular' | 'super' | 'link';
|
||||||
export const TOOL_IDS = [
|
|
||||||
|
export const REGULAR_TOOL_IDS = [
|
||||||
'certSign',
|
'certSign',
|
||||||
'sign',
|
'sign',
|
||||||
'addPassword',
|
'addPassword',
|
||||||
@ -26,7 +27,6 @@ export const TOOL_IDS = [
|
|||||||
'adjustContrast',
|
'adjustContrast',
|
||||||
'crop',
|
'crop',
|
||||||
'pdfToSinglePage',
|
'pdfToSinglePage',
|
||||||
'multiTool',
|
|
||||||
'repair',
|
'repair',
|
||||||
'compare',
|
'compare',
|
||||||
'addPageNumbers',
|
'addPageNumbers',
|
||||||
@ -44,21 +44,52 @@ export const TOOL_IDS = [
|
|||||||
'overlayPdfs',
|
'overlayPdfs',
|
||||||
'getPdfInfo',
|
'getPdfInfo',
|
||||||
'validateSignature',
|
'validateSignature',
|
||||||
'read',
|
|
||||||
'automate',
|
|
||||||
'replaceColor',
|
'replaceColor',
|
||||||
'showJS',
|
'showJS',
|
||||||
|
'bookletImposition',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SUPER_TOOL_IDS = [
|
||||||
|
'multiTool',
|
||||||
|
'read',
|
||||||
|
'automate',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const LINK_TOOL_IDS = [
|
||||||
'devApi',
|
'devApi',
|
||||||
'devFolderScanning',
|
'devFolderScanning',
|
||||||
'devSsoGuide',
|
'devSsoGuide',
|
||||||
'devAirgapped',
|
'devAirgapped',
|
||||||
'bookletImposition',
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const TOOL_IDS = [
|
||||||
|
...REGULAR_TOOL_IDS,
|
||||||
|
...SUPER_TOOL_IDS,
|
||||||
|
...LINK_TOOL_IDS,
|
||||||
|
];
|
||||||
|
|
||||||
// Tool identity - what PDF operation we're performing (type-safe)
|
// Tool identity - what PDF operation we're performing (type-safe)
|
||||||
export type ToolId = typeof TOOL_IDS[number];
|
export type ToolId = typeof TOOL_IDS[number];
|
||||||
|
export const isValidToolId = (value: string): value is ToolId =>
|
||||||
|
TOOL_IDS.includes(value as ToolId);
|
||||||
|
|
||||||
|
export type RegularToolId = typeof REGULAR_TOOL_IDS[number];
|
||||||
|
export const isRegularToolId = (toolId: ToolId): toolId is RegularToolId =>
|
||||||
|
REGULAR_TOOL_IDS.includes(toolId as RegularToolId);
|
||||||
|
|
||||||
|
export type SuperToolId = typeof SUPER_TOOL_IDS[number];
|
||||||
|
export const isSuperToolId = (toolId: ToolId): toolId is SuperToolId =>
|
||||||
|
SUPER_TOOL_IDS.includes(toolId as SuperToolId);
|
||||||
|
|
||||||
|
export type LinkToolId = typeof LINK_TOOL_IDS[number];
|
||||||
|
export const isLinkToolId = (toolId: ToolId): toolId is LinkToolId =>
|
||||||
|
LINK_TOOL_IDS.includes(toolId as LinkToolId);
|
||||||
|
|
||||||
|
|
||||||
|
type Assert<A extends true> = A;
|
||||||
|
type Disjoint<A, B> = [A & B] extends [never] ? true : false;
|
||||||
|
|
||||||
|
type _Check1 = Assert<Disjoint<RegularToolId, SuperToolId>>;
|
||||||
|
type _Check2 = Assert<Disjoint<RegularToolId, LinkToolId>>;
|
||||||
|
type _Check3 = Assert<Disjoint<SuperToolId, LinkToolId>>;
|
||||||
|
|
||||||
// Type guard using the same source of truth
|
|
||||||
export const isValidToolId = (value: string): value is ToolId => {
|
|
||||||
return TOOL_IDS.includes(value as ToolId);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ToolRegistry } from '../data/toolsTaxonomy';
|
import { ToolRegistry } from '../data/toolsTaxonomy';
|
||||||
|
import { ToolId } from '../types/toolId';
|
||||||
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
import { AUTOMATION_CONSTANTS } from '../constants/automation';
|
||||||
import { AutomationFileProcessor } from './automationFileProcessor';
|
import { AutomationFileProcessor } from './automationFileProcessor';
|
||||||
import { ToolType } from '../hooks/tools/shared/useToolOperation';
|
import { ToolType } from '../hooks/tools/shared/useToolOperation';
|
||||||
@ -149,7 +150,7 @@ export const executeToolOperationWithPrefix = async (
|
|||||||
toolRegistry: ToolRegistry,
|
toolRegistry: ToolRegistry,
|
||||||
filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX
|
filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
): Promise<File[]> => {
|
): Promise<File[]> => {
|
||||||
const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
|
const config = toolRegistry[operationName as ToolId]?.operationConfig;
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error(`Tool operation not supported: ${operationName}`);
|
throw new Error(`Tool operation not supported: ${operationName}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { PDFDocument, rgb } from 'pdf-lib';
|
|||||||
import { generateThumbnailWithMetadata } from './thumbnailUtils';
|
import { generateThumbnailWithMetadata } from './thumbnailUtils';
|
||||||
import { createProcessedFile, createChildStub } from '../contexts/file/fileActions';
|
import { createProcessedFile, createChildStub } from '../contexts/file/fileActions';
|
||||||
import { createStirlingFile, StirlingFile, FileId, StirlingFileStub } from '../types/fileContext';
|
import { createStirlingFile, StirlingFile, FileId, StirlingFileStub } from '../types/fileContext';
|
||||||
import type { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
|
import type { SignatureAPI } from '../components/viewer/viewerTypes';
|
||||||
|
|
||||||
interface MinimalFileContextSelectors {
|
interface MinimalFileContextSelectors {
|
||||||
getAllFileIds: () => FileId[];
|
getAllFileIds: () => FileId[];
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ToolId } from "src/types/toolId";
|
import { ToolId } from "src/types/toolId";
|
||||||
import { ToolRegistryEntry } from "../data/toolsTaxonomy";
|
import { ToolRegistryEntry, ToolRegistry } from "../data/toolsTaxonomy";
|
||||||
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
|
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
|
||||||
|
|
||||||
export interface RankedToolItem {
|
export interface RankedToolItem {
|
||||||
@ -8,7 +8,7 @@ export interface RankedToolItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function filterToolRegistryByQuery(
|
export function filterToolRegistryByQuery(
|
||||||
toolRegistry: Record<ToolId, ToolRegistryEntry>,
|
toolRegistry: Partial<ToolRegistry>,
|
||||||
query: string
|
query: string
|
||||||
): RankedToolItem[] {
|
): RankedToolItem[] {
|
||||||
const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
|
const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
|
||||||
@ -96,5 +96,3 @@ export function filterToolRegistryByQuery(
|
|||||||
return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
|
return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -115,4 +115,3 @@ export function getToolDisplayName(toolId: ToolId, registry: ToolRegistry): stri
|
|||||||
const tool = registry[toolId];
|
const tool = registry[toolId];
|
||||||
return tool ? tool.name : toolId;
|
return tool ? tool.name : toolId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user