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:
James Brunton
2025-10-21 14:53:18 +01:00
committed by GitHub
parent 3e23dc59b6
commit c9eee00d66
35 changed files with 2272 additions and 352 deletions

View File

@@ -25,7 +25,7 @@ interface AutomationCreationProps {
existingAutomation?: AutomationConfig;
onBack: () => void;
onComplete: (automation: AutomationConfig) => void;
toolRegistry: ToolRegistry;
toolRegistry: Partial<ToolRegistry>;
}
export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) {

View File

@@ -7,7 +7,7 @@ import DeleteIcon from '@mui/icons-material/Delete';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { Tooltip } from '../../shared/Tooltip';
import { ToolIcon } from '../../shared/ToolIcon';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
import { ToolId } from 'src/types/toolId';
interface AutomationEntryProps {
@@ -32,7 +32,7 @@ interface AutomationEntryProps {
/** Copy handler (for suggested automations) */
onCopy?: () => void;
/** Tool registry to resolve operation names */
toolRegistry?: Record<ToolId, ToolRegistryEntry>;
toolRegistry?: Partial<ToolRegistry>;
}
export default function AutomationEntry({
@@ -56,8 +56,9 @@ export default function AutomationEntry({
// Helper function to resolve tool display names
const getToolDisplayName = (operation: string): string => {
if (toolRegistry?.[operation as ToolId]?.name) {
return toolRegistry[operation as ToolId].name;
const entry = toolRegistry?.[operation as ToolId];
if (entry?.name) {
return entry.name;
}
// Fallback to translation or operation key
return t(`${operation}.title`, operation);

View File

@@ -4,7 +4,7 @@ import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import CheckIcon from "@mui/icons-material/Check";
import { useFileSelection } from "../../../contexts/FileContext";
import { useFlatToolRegistry } from "../../../data/useTranslatedToolRegistry";
import { useToolRegistry } from "../../../contexts/ToolRegistryContext";
import { AutomationConfig, ExecutionStep } from "../../../types/automation";
import { AUTOMATION_CONSTANTS, EXECUTION_STATUS } from "../../../constants/automation";
import { useResourceCleanup } from "../../../utils/resourceManager";
@@ -18,7 +18,8 @@ interface AutomationRunProps {
export default function AutomationRun({ automation, onComplete, automateOperation }: AutomationRunProps) {
const { t } = useTranslation();
const { selectedFiles } = useFileSelection();
const toolRegistry = useFlatToolRegistry();
const { regularTools } = useToolRegistry();
const toolRegistry = regularTools;
const cleanup = useResourceCleanup();
// Progress tracking state

View File

@@ -6,8 +6,7 @@ import AutomationEntry from "./AutomationEntry";
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
import { AutomationConfig, SuggestedAutomation } from "../../../types/automation";
import { iconMap } from './iconMap';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { ToolId } from '../../../types/toolId';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
interface AutomationSelectionProps {
savedAutomations: AutomationConfig[];
@@ -16,7 +15,7 @@ interface AutomationSelectionProps {
onEdit: (automation: AutomationConfig) => void;
onDelete: (automation: AutomationConfig) => void;
onCopyFromSuggested: (automation: SuggestedAutomation) => void;
toolRegistry: Record<ToolId, ToolRegistryEntry>;
toolRegistry: Partial<ToolRegistry>;
}
export default function AutomationSelection({

View File

@@ -15,6 +15,7 @@ import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import WarningIcon from '@mui/icons-material/Warning';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
import { ToolId } from '../../../types/toolId';
import { getAvailableToExtensions } from '../../../utils/convertUtils';
interface ToolConfigurationModalProps {
opened: boolean;
@@ -26,7 +27,7 @@ interface ToolConfigurationModalProps {
};
onSave: (parameters: any) => void;
onCancel: () => void;
toolRegistry: ToolRegistry;
toolRegistry: Partial<ToolRegistry>;
}
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>({});
// Get tool info from registry
const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry];
const toolInfo = toolRegistry[tool.operation as ToolId];
const SettingsComponent = toolInfo?.automationSettings;
// Initialize parameters from tool (which should contain defaults from registry)

View File

@@ -5,14 +5,14 @@ import SettingsIcon from "@mui/icons-material/Settings";
import CloseIcon from "@mui/icons-material/Close";
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
import { AutomationTool } from "../../../types/automation";
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
import { ToolRegistry } from "../../../data/toolsTaxonomy";
import { ToolId } from "../../../types/toolId";
import ToolSelector from "./ToolSelector";
import AutomationEntry from "./AutomationEntry";
interface ToolListProps {
tools: AutomationTool[];
toolRegistry: Record<ToolId, ToolRegistryEntry>;
toolRegistry: Partial<ToolRegistry>;
onToolUpdate: (index: number, updates: Partial<AutomationTool>) => void;
onToolRemove: (index: number) => void;
onToolConfigure: (index: number) => void;

View File

@@ -1,7 +1,7 @@
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
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 { renderToolButtons } from '../shared/renderToolButtons';
import ToolSearch from '../toolPicker/ToolSearch';
@@ -11,7 +11,7 @@ import { ToolId } from '../../../types/toolId';
interface ToolSelectorProps {
onSelect: (toolKey: string) => void;
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
placeholder?: string; // Custom placeholder text
}
@@ -54,7 +54,7 @@ export default function ToolSelector({
// Create filtered tool registry for ToolSearch
const filteredToolRegistry = useMemo(() => {
const registry: Record<ToolId, ToolRegistryEntry> = {} as Record<ToolId, ToolRegistryEntry>;
const registry: Partial<ToolRegistry> = {};
baseFilteredTools.forEach(([key, tool]) => {
registry[key as ToolId] = tool;
});
@@ -142,10 +142,10 @@ export default function ToolSelector({
};
// Get display value for selected tool
const selectedTool = selectedValue ? toolRegistry[selectedValue as ToolId] : undefined;
const getDisplayValue = () => {
if (selectedValue && toolRegistry[selectedValue as ToolId]) {
return toolRegistry[selectedValue as ToolId].name;
}
if (selectedTool) return selectedTool.name;
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
};
@@ -153,12 +153,18 @@ export default function ToolSelector({
<div ref={containerRef} className='rounded-xl'>
{/* 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
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
borderRadius: "var(--mantine-radius-lg)" }}>
<ToolButton id={'tool' as ToolId} tool={toolRegistry[selectedValue as ToolId]} isSelected={false}
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
<ToolButton
id={'tool' as ToolId}
tool={selectedTool}
isSelected={false}
onSelect={()=>{}}
rounded={true}
disableNavigation={true}
/>
</div>
) : (
// Show search input when no tool selected OR when dropdown is opened

View File

@@ -3,13 +3,7 @@ import { useHistoryCapability } from '@embedpdf/plugin-history/react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { useSignature } from '../../contexts/SignatureContext';
import { uuidV4 } from '@embedpdf/models';
export interface HistoryAPI {
undo: () => void;
redo: () => void;
canUndo: () => boolean;
canRedo: () => boolean;
}
import type { HistoryAPI } from './viewerTypes';
export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge(_, ref) {
const { provides: historyApi } = useHistoryCapability();
@@ -42,7 +36,7 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
const currentStoredData = getImageData(annotation.id);
// Check if the annotation lacks image data but we have it stored
if (currentStoredData && (!annotation.imageSrc || annotation.imageSrc !== currentStoredData)) {
// Generate new ID to avoid React key conflicts
const newId = uuidV4();
@@ -113,4 +107,4 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
return null; // This is a bridge component with no UI
});
HistoryAPIBridge.displayName = 'HistoryAPIBridge';
HistoryAPIBridge.displayName = 'HistoryAPIBridge';

View File

@@ -34,8 +34,9 @@ import { SpreadAPIBridge } from './SpreadAPIBridge';
import { SearchAPIBridge } from './SearchAPIBridge';
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
import { RotateAPIBridge } from './RotateAPIBridge';
import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge';
import { HistoryAPIBridge, HistoryAPI } from './HistoryAPIBridge';
import { SignatureAPIBridge } from './SignatureAPIBridge';
import { HistoryAPIBridge } from './HistoryAPIBridge';
import type { SignatureAPI, HistoryAPI } from './viewerTypes';
import { ExportAPIBridge } from './ExportAPIBridge';
interface LocalEmbedPDFProps {

View File

@@ -2,17 +2,7 @@ import { useImperativeHandle, forwardRef, useEffect } from 'react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
import { useSignature } from '../../contexts/SignatureContext';
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[]>;
}
import type { SignatureAPI } from './viewerTypes';
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
const { provides: annotationApi } = useAnnotationCapability();
@@ -246,4 +236,4 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
return null; // This is a bridge component with no UI
});
SignatureAPIBridge.displayName = 'SignatureAPIBridge';
SignatureAPIBridge.displayName = 'SignatureAPIBridge';

View 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;
}