This commit is contained in:
Connor Yoh 2025-08-28 15:25:33 +01:00
parent c0caf3bc6e
commit 6e553b23f6
20 changed files with 101 additions and 72 deletions

View File

@ -5,7 +5,7 @@ import rainbowStyles from '../../styles/rainbow.module.css';
import VisibilityIcon from "@mui/icons-material/Visibility";
import EditNoteIcon from "@mui/icons-material/EditNote";
import FolderIcon from "@mui/icons-material/Folder";
import { WorkbenchType, isValidWorkbench } from '../../types/navigation';
import { WorkbenchType, isValidWorkbench } from '../../types/workbench';
import { Tooltip } from "./Tooltip";
const viewOptionStyle = {

View File

@ -11,7 +11,7 @@ import {
Modal
} from '@mantine/core';
import CheckIcon from '@mui/icons-material/Check';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
import ToolConfigurationModal from './ToolConfigurationModal';
import ToolList from './ToolList';
import IconSelector from './IconSelector';
@ -24,7 +24,7 @@ interface AutomationCreationProps {
existingAutomation?: AutomationConfig;
onBack: () => void;
onComplete: (automation: AutomationConfig) => void;
toolRegistry: Record<string, ToolRegistryEntry>;
toolRegistry: ToolRegistry;
}
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {

View File

@ -33,7 +33,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
React.useEffect(() => {
if (automation?.operations) {
const steps = automation.operations.map((op: any, index: number) => {
const tool = toolRegistry[op.operation];
const tool = toolRegistry[op.operation as keyof typeof toolRegistry];
return {
id: `${op.operation}-${index}`,
operation: op.operation,

View File

@ -35,7 +35,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
const [isValid, setIsValid] = useState(true);
// Get tool info from registry
const toolInfo = toolRegistry[tool.operation];
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
const SettingsComponent = toolInfo?.settingsComponent;
// Initialize parameters from tool (which should contain defaults from registry)

View File

@ -1,5 +1,6 @@
import React, { createContext, useContext, useReducer, useCallback } from 'react';
import { WorkbenchType, ToolId, getDefaultWorkbench } from '../types/navigation';
import { WorkbenchType, getDefaultWorkbench } from '../types/workbench';
import { ToolId, isValidToolId } from '../types/toolId';
import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry';
/**
@ -167,10 +168,13 @@ export const NavigationProvider: React.FC<{
}
// Look up the tool in the registry to get its proper workbench
const tool = toolRegistry[toolId];
const tool = isValidToolId(toolId)? toolRegistry[toolId] : null;
const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench();
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
// Validate toolId and convert to ToolId type
const validToolId = isValidToolId(toolId) ? toolId : null;
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, workbench } });
}, [toolRegistry])
};

View File

@ -6,10 +6,11 @@
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
import { useToolManagement } from '../hooks/useToolManagement';
import { PageEditorFunctions } from '../types/pageEditor';
import { ToolRegistryEntry } from '../data/toolsTaxonomy';
import { ToolRegistryEntry, ToolRegistry } from '../data/toolsTaxonomy';
import { useNavigationActions, useNavigationState } from './NavigationContext';
import { ToolId, isValidToolId } from '../types/toolId';
import { useNavigationUrlSync } from '../hooks/useUrlSync';
import { getDefaultWorkbench } from '../types/navigation';
import { getDefaultWorkbench } from '../types/workbench';
// State interface
interface ToolWorkflowState {
@ -84,7 +85,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
setSearchQuery: (query: string) => void;
// Tool Actions
selectTool: (toolId: string) => void;
selectTool: (toolId: ToolId | null) => void;
clearToolSelection: () => void;
// Tool Reset Actions
@ -174,7 +175,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: string) => {
// Set the selected tool and determine the appropriate workbench
actions.setSelectedTool(toolId);
const validToolId = isValidToolId(toolId) ? toolId : null;
actions.setSelectedTool(validToolId);
// Get the tool from registry to determine workbench
const tool = getSelectedTool(toolId);
@ -229,7 +231,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
navigationState.selectedTool,
handleToolSelect,
handleBackToTools,
toolRegistry,
toolRegistry as ToolRegistry,
true
);

View File

@ -1,9 +1,9 @@
import { type TFunction } from 'i18next';
import React from 'react';
import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { BaseToolProps } from '../types/tool';
import { BaseParameters } from '../types/parameters';
import { WorkbenchType } from '../types/navigation';
import { WorkbenchType } from '../types/workbench';
import { ToolId } from '../types/toolId';
export enum SubcategoryId {
SIGNING = 'signing',
@ -47,7 +47,7 @@ export type ToolRegistryEntry = {
settingsComponent?: React.ComponentType<any>;
}
export type ToolRegistry = Record<string /* FIX ME: Should be ToolId */, ToolRegistryEntry>;
export type ToolRegistry = Record<ToolId, ToolRegistryEntry>;
export const SUBCATEGORY_ORDER: SubcategoryId[] = [
SubcategoryId.SIGNING,
@ -126,7 +126,7 @@ export const getToolWorkbench = (tool: ToolRegistryEntry): WorkbenchType => {
};
/**
* Get URL path for a tool
* Get URL path for a tool
*/
export const getToolUrlPath = (toolId: string, tool: ToolRegistryEntry): string => {
return tool.urlPath || `/${toolId.replace(/([A-Z])/g, '-$1').toLowerCase()}`;

View File

@ -39,6 +39,7 @@ import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/Add
import OCRSettings from "../components/tools/ocr/OCRSettings";
import ConvertSettings from "../components/tools/convert/ConvertSettings";
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
import { ToolId } from "../types/toolId";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
@ -162,7 +163,7 @@ export function useFlatToolRegistry(): ToolRegistry {
operationConfig: addPasswordOperationConfig,
settingsComponent: AddPasswordSettings,
},
watermark: {
addWatermark: {
icon: <LocalIcon icon="branding-watermark-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.watermark.title", "Add Watermark"),
component: AddWatermark,
@ -258,7 +259,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Document Review
read: {
read: {
icon: <LocalIcon icon="article-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.read.title", "Read"),
component: null,
@ -284,7 +285,6 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.crop.title", "Crop PDF"),
component: null,
description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
@ -293,16 +293,14 @@ export function useFlatToolRegistry(): ToolRegistry {
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.rotate.title", "Rotate"),
component: null,
description: t("home.rotate.desc", "Easily rotate your PDFs."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
},
splitPdf: {
split: {
icon: <LocalIcon icon="content-cut-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.split.title", "Split"),
component: SplitPdfPanel,
description: t("home.split.desc", "Split PDFs into multiple documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
@ -372,7 +370,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Extraction
extractPages: {
"extract-page": {
icon: <LocalIcon icon="upload-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.extractPages.title", "Extract Pages"),
component: null,
@ -490,7 +488,7 @@ export function useFlatToolRegistry(): ToolRegistry {
// Advanced Formatting
adjustContrast: {
"adjust-contrast": {
icon: <LocalIcon icon="palette" width="1.5rem" height="1.5rem" />,
name: t("home.adjustContrast.title", "Adjust Colors/Contrast"),
component: null,
@ -700,9 +698,9 @@ export function useFlatToolRegistry(): ToolRegistry {
return allTools;
}
const filteredTools = Object.keys(allTools)
.filter((key) => allTools[key].component !== null || allTools[key].link)
.filter((key) => allTools[key as ToolId].component !== null || allTools[key as ToolId].link)
.reduce((obj, key) => {
obj[key] = allTools[key];
obj[key as ToolId] = allTools[key as ToolId];
return obj;
}, {} as ToolRegistry);
return filteredTools;

View File

@ -2,29 +2,29 @@ import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
interface UseAutomationFormProps {
mode: AutomationMode;
existingAutomation?: AutomationConfig;
toolRegistry: Record<string, ToolRegistryEntry>;
toolRegistry: ToolRegistry;
}
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
const { t } = useTranslation();
const [automationName, setAutomationName] = useState('');
const [automationDescription, setAutomationDescription] = useState('');
const [automationIcon, setAutomationIcon] = useState<string>('');
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const getToolName = useCallback((operation: string) => {
const tool = toolRegistry?.[operation] as any;
const tool = toolRegistry?.[operation as keyof ToolRegistry] as any;
return tool?.name || t(`tools.${operation}.name`, operation);
}, [toolRegistry, t]);
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
const config = toolRegistry[operation]?.operationConfig;
const config = toolRegistry[operation as keyof ToolRegistry]?.operationConfig;
if (config?.defaultParameters) {
return { ...config.defaultParameters };
}
@ -119,4 +119,4 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
getToolName,
getToolDefaultParameters
};
}
}

View File

@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useNavigationActions, useNavigationState } from '../contexts/NavigationContext';
import { ToolId } from '../types/toolId';
// Material UI Icons
import CompressIcon from '@mui/icons-material/Compress';
@ -9,7 +10,7 @@ import CropIcon from '@mui/icons-material/Crop';
import TextFieldsIcon from '@mui/icons-material/TextFields';
export interface SuggestedTool {
id: string /* FIX ME: Should be ToolId */;
id: ToolId;
title: string;
icon: React.ComponentType<any>;
navigate: () => void;
@ -32,7 +33,7 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
icon: CleaningServicesIcon
},
{
id: 'splitPdf',
id: 'split',
title: 'Split',
icon: CropIcon
},

View File

@ -35,7 +35,7 @@ export const useToolManagement = (): ToolManagementResult => {
const isToolAvailable = useCallback((toolKey: string): boolean => {
if (endpointsLoading) return true;
const endpoints = baseRegistry[toolKey]?.endpoints || [];
const endpoints = baseRegistry[toolKey as keyof typeof baseRegistry]?.endpoints || [];
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
}, [endpointsLoading, endpointStatus, baseRegistry]);

View File

@ -3,7 +3,7 @@
*/
import { useEffect, useCallback, useRef } from 'react';
import { WorkbenchType, ToolId } from '../types/navigation';
import { ToolId } from '../types/toolId';
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
import { ToolRegistry } from '../data/toolsTaxonomy';
import { firePixel } from '../utils/scarfTracking';
@ -55,7 +55,7 @@ export function useNavigationUrlSync(
clearToolRoute(false); // Use pushState for user navigation
}
}
prevSelectedTool.current = selectedTool;
}, [selectedTool, registry, enableSync]);

View File

@ -161,7 +161,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const filesPlaceholder = useMemo(() => {
if (currentStep === AUTOMATION_STEPS.RUN && stepData.automation?.operations?.length) {
const firstOperation = stepData.automation.operations[0];
const toolConfig = toolRegistry[firstOperation.operation];
const toolConfig = toolRegistry[firstOperation.operation as keyof typeof toolRegistry];
// Check if the tool has supportedFormats that include non-PDF formats
if (toolConfig?.supportedFormats && toolConfig.supportedFormats.length > 1) {

View File

@ -2,14 +2,8 @@
* Navigation types for workbench and tool separation
*/
// Define workbench values once as source of truth
const WORKBENCH_TYPES = ['viewer', 'pageEditor', 'fileEditor'] as const;
// Workbench types - how the user interacts with content
export type WorkbenchType = typeof WORKBENCH_TYPES[number];
// Tool identity - what PDF operation we're performing (derived from registry)
export type ToolId = string;
import { WorkbenchType } from './workbench';
import { ToolId } from './toolId';
// Navigation state
export interface NavigationState {
@ -17,12 +11,6 @@ export interface NavigationState {
selectedTool: ToolId | null;
}
export const getDefaultWorkbench = (): WorkbenchType => 'fileEditor';
// Type guard using the same source of truth - no duplication
export const isValidWorkbench = (value: string): value is WorkbenchType => {
return WORKBENCH_TYPES.includes(value as WorkbenchType);
};
// Route parsing result
export interface ToolRoute {

View File

@ -2,7 +2,8 @@
* Navigation action interfaces to break circular dependencies
*/
import { WorkbenchType, ToolId } from './navigation';
import { WorkbenchType } from './workbench';
import { ToolId } from './toolId';
export interface NavigationActions {
setWorkbench: (workbench: WorkbenchType) => void;

View File

@ -0,0 +1,25 @@
// Define all possible tool IDs as source of truth
const TOOL_IDS = [
'certSign', 'sign', 'addPassword', 'remove-password', 'removePages', 'remove-blank-pages', 'remove-annotations', 'remove-image',
'change-permissions', 'addWatermark',
'sanitize', 'auto-split-pages', 'auto-split-by-size-count', 'split', 'mergePdfs',
'convert', 'ocr', 'add-image', 'rotate',
'detect-split-scanned-photos',
'edit-table-of-contents',
'scanner-effect',
'auto-rename-pdf-file', 'multi-page-layout', 'adjust-page-size-scale', 'adjust-contrast', 'cropPdf', 'single-large-page', 'multi-tool',
'repair', 'compare', 'addPageNumbers', 'redact',
'flatten', 'remove-certificate-sign',
'unlock-pdf-forms', 'compress', 'extract-page', 'reorganize-pages', 'extract-images',
'add-stamp', 'add-attachments', 'change-metadata', 'overlay-pdfs',
'manage-certificates', 'get-all-info-on-pdf', 'validate-pdf-signature', 'read', 'automate', 'replace-and-invert-color',
'show-javascript', 'dev-api', 'dev-folder-scanning', 'dev-sso-guide', 'dev-airgapped'
] as const;
// Tool identity - what PDF operation we're performing (type-safe)
export type ToolId = typeof TOOL_IDS[number];
// Type guard using the same source of truth
export const isValidToolId = (value: string): value is ToolId => {
return TOOL_IDS.includes(value as ToolId);
};

View File

@ -0,0 +1,12 @@
// Define workbench values once as source of truth
const WORKBENCH_TYPES = ['viewer', 'pageEditor', 'fileEditor'] as const;
// Workbench types - how the user interacts with content
export type WorkbenchType = typeof WORKBENCH_TYPES[number];
export const getDefaultWorkbench = (): WorkbenchType => 'fileEditor';
// Type guard using the same source of truth
export const isValidWorkbench = (value: string): value is WorkbenchType => {
return WORKBENCH_TYPES.includes(value as WorkbenchType);
};

View File

@ -31,7 +31,7 @@ export const executeToolOperationWithPrefix = async (
): Promise<File[]> => {
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
const config = toolRegistry[operationName]?.operationConfig;
const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig;
if (!config) {
console.error(`❌ Tool operation not supported: ${operationName}`);
throw new Error(`Tool operation not supported: ${operationName}`);

View File

@ -1,10 +1,10 @@
import { ToolId } from '../types/navigation';
import { ToolId } from '../types/toolId';
// Map URL paths to tool keys (multiple URLs can map to same tool)
export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
'/split-pdfs': 'split',
'/split': 'split',
'/merge-pdfs': 'merge',
'/merge-pdfs': 'mergePdfs',
'/compress-pdf': 'compress',
'/convert': 'convert',
'/convert-pdf': 'convert',
@ -19,15 +19,15 @@ export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
'/pdf-to-word': 'convert',
'/pdf-to-xml': 'convert',
'/add-password': 'addPassword',
'/change-permissions': 'changePermissions',
'/change-permissions': 'change-permissions',
'/sanitize-pdf': 'sanitize',
'/ocr': 'ocr',
'/ocr-pdf': 'ocr',
'/add-watermark': 'addWatermark',
'/remove-password': 'removePassword',
'/remove-password': 'remove-password',
'/single-large-page': 'single-large-page',
'/repair': 'repair',
'/unlock-pdf-forms': 'unlockPdfForms',
'/remove-certificate-sign': 'removeCertificateSign',
'/remove-cert-sign': 'removeCertificateSign'
};
'/unlock-pdf-forms': 'unlock-pdf-forms',
'/remove-certificate-sign': 'remove-certificate-sign',
'/remove-cert-sign': 'remove-certificate-sign'
};

View File

@ -2,12 +2,10 @@
* URL routing utilities for tool navigation with registry support
*/
import {
ToolId,
ToolRoute,
getDefaultWorkbench
} from '../types/navigation';
import { ToolRegistry, getToolWorkbench, getToolUrlPath, isValidToolId } from '../data/toolsTaxonomy';
import { ToolRoute } from '../types/navigation';
import { ToolId, isValidToolId } from '../types/toolId';
import { getDefaultWorkbench } from '../types/workbench';
import { ToolRegistry, getToolWorkbench, getToolUrlPath } from '../data/toolsTaxonomy';
import { firePixel } from './scarfTracking';
import { URL_TO_TOOL_MAP } from './urlMapping';
@ -31,7 +29,7 @@ export function parseToolRoute(registry: ToolRegistry): ToolRoute {
// Fallback: Try to find tool by primary URL path in registry
for (const [toolId, tool] of Object.entries(registry)) {
const toolUrlPath = getToolUrlPath(toolId, tool);
if (path === toolUrlPath) {
if (path === toolUrlPath && isValidToolId(toolId)) {
return {
workbench: getToolWorkbench(tool),
toolId
@ -41,7 +39,7 @@ export function parseToolRoute(registry: ToolRegistry): ToolRoute {
// Check for query parameter fallback (e.g., ?tool=split)
const toolParam = searchParams.get('tool');
if (toolParam && isValidToolId(toolParam, registry)) {
if (toolParam && isValidToolId(toolParam) && registry[toolParam]) {
const tool = registry[toolParam];
return {
workbench: getToolWorkbench(tool),