mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Update dependencies and enhance ESLint config
This commit is contained in:
parent
fd52dc0226
commit
4a8e2e477c
1
.gitignore
vendored
1
.gitignore
vendored
@ -210,3 +210,4 @@ node_modules/
|
||||
test_batch.json
|
||||
*.backup.*.json
|
||||
frontend/public/locales/*/translation.backup*.json
|
||||
/frontend
|
||||
|
@ -1,19 +1,39 @@
|
||||
// @ts-check
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const tsconfigRootDir = fileURLToPath(new URL('./', import.meta.url));
|
||||
|
||||
const srcGlobs = ['{src,frontend/src}/**/*.{ts,tsx,js,jsx}'];
|
||||
const srcTsGlobs = ['{src,frontend/src}/**/*.{ts,tsx}'];
|
||||
const nodeGlobs = [
|
||||
'scripts/**/*.{js,ts}',
|
||||
'vite.config.ts',
|
||||
'vitest.config.ts',
|
||||
'vitest.minimal.config.ts',
|
||||
'playwright.config.ts',
|
||||
'tailwind.config.js',
|
||||
'postcss.config.js',
|
||||
'eslint.config.mjs',
|
||||
];
|
||||
|
||||
export default defineConfig(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
{
|
||||
ignores: [
|
||||
"dist", // Contains 3rd party code
|
||||
"public", // Contains 3rd party code
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [...tseslint.configs.recommended],
|
||||
rules: {
|
||||
"no-undef": "off", // Temporarily disabled until codebase conformant
|
||||
"@typescript-eslint/no-empty-object-type": [
|
||||
@ -26,17 +46,112 @@ export default defineConfig(
|
||||
"@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-unused-vars": [
|
||||
"error",
|
||||
"warn",
|
||||
{
|
||||
"args": "all", // All function args must be used (or explicitly ignored)
|
||||
"argsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
||||
"caughtErrors": "all", // Caught errors must be used (or explicitly ignored)
|
||||
"caughtErrorsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
|
||||
"destructuredArrayIgnorePattern": "^_", // 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)
|
||||
args: 'all', // All function args must be used (or explicitly ignored)
|
||||
argsIgnorePattern: '^_', // Allow unused variables beginning with an underscore
|
||||
caughtErrors: 'all', // Caught errors must be used (or explicitly ignored)
|
||||
caughtErrorsIgnorePattern: '^_', // Allow unused variables beginning with an underscore
|
||||
destructuredArrayIgnorePattern: '^_', // 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)
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: srcTsGlobs,
|
||||
extends: [
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: srcGlobs,
|
||||
extends: [reactPlugin.configs.flat.recommended],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
'react-hooks': reactHooksPlugin,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off', // Not needed with React 17+
|
||||
|
||||
'react-hooks/exhaustive-deps': 'off', // Temporarily disabled until codebase conformant
|
||||
'react-hooks/rules-of-hooks': 'warn',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-explicit-any': 'off', // Temporarily disabled until codebase conformant
|
||||
"@typescript-eslint/no-inferrable-types": "off", // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-unsafe-return': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-unsafe-call': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-unsafe-arguments': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-unsafe-argument': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/require-await': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/only-throw-error': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-floating-promises': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/prefer-optional-chain': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-unnecessary-type-assertions': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/unbound-method': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-base-to-string': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-misused-promises': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/restrict-template-expressions': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/dot-notation': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/prefer-includes': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/consistent-indexed-object-style': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/non-nullable-type-assertion-style': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/consistent-generic-constructors': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/class-literal-property-style': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/consistent-type-definitions': 'off', // Temporarily disabled until codebase conformant
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'off', // Temporarily disabled until codebase conformant
|
||||
|
||||
'react/no-children-prop': 'warn', // Children should be passed as actual children, not via the children prop
|
||||
'react/prop-types': 'off', // We use TypeScript's types for props instead
|
||||
'react/display-name': 'off', // Temporarily disabled until codebase conformant
|
||||
'react/no-unescaped-entities': 'off', // Temporarily disabled until codebase conformant
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'src/**/*.{test,spec}.{ts,tsx}',
|
||||
'src/tests/**/*.{ts,tsx}',
|
||||
],
|
||||
extends: [tseslint.configs.disableTypeChecked],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.vitest,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: nodeGlobs,
|
||||
extends: [tseslint.configs.disableTypeChecked],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
2075
frontend/package-lock.json
generated
2075
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,32 +6,36 @@
|
||||
"proxy": "http://localhost:8080",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@embedpdf/core": "^1.2.1",
|
||||
"@embedpdf/engines": "^1.2.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.2.1",
|
||||
"@embedpdf/plugin-loader": "^1.2.1",
|
||||
"@embedpdf/plugin-pan": "^1.2.1",
|
||||
"@embedpdf/plugin-render": "^1.2.1",
|
||||
"@embedpdf/plugin-rotate": "^1.2.1",
|
||||
"@embedpdf/plugin-scroll": "^1.2.1",
|
||||
"@embedpdf/plugin-search": "^1.2.1",
|
||||
"@embedpdf/plugin-selection": "^1.2.1",
|
||||
"@embedpdf/plugin-spread": "^1.2.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.2.1",
|
||||
"@embedpdf/plugin-tiling": "^1.2.1",
|
||||
"@embedpdf/plugin-viewport": "^1.2.1",
|
||||
"@embedpdf/plugin-zoom": "^1.2.1",
|
||||
"@embedpdf/core": "^1.3.1",
|
||||
"@embedpdf/engines": "^1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.1",
|
||||
"@embedpdf/plugin-loader": "^1.3.1",
|
||||
"@embedpdf/plugin-pan": "^1.3.1",
|
||||
"@embedpdf/plugin-render": "^1.3.1",
|
||||
"@embedpdf/plugin-rotate": "^1.3.1",
|
||||
"@embedpdf/plugin-scroll": "^1.3.1",
|
||||
"@embedpdf/plugin-search": "^1.3.1",
|
||||
"@embedpdf/plugin-selection": "^1.3.1",
|
||||
"@embedpdf/plugin-spread": "^1.3.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.1",
|
||||
"@embedpdf/plugin-tiling": "^1.3.1",
|
||||
"@embedpdf/plugin-viewport": "^1.3.1",
|
||||
"@embedpdf/plugin-zoom": "^1.3.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/dates": "^8.3.1",
|
||||
"@mantine/dropzone": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@mantine/core": "^8.3.2",
|
||||
"@mantine/dates": "^8.3.2",
|
||||
"@mantine/dropzone": "^8.3.2",
|
||||
"@mantine/hooks": "^8.3.2",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^1.12.2",
|
||||
"i18next": "^25.5.2",
|
||||
@ -41,11 +45,11 @@
|
||||
"license-report": "^6.8.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.4.149",
|
||||
"posthog-js": "^1.268.0",
|
||||
"posthog-js": "^1.268.5",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-router-dom": "^7.9.1",
|
||||
"react-i18next": "^16.0.0",
|
||||
"react-router-dom": "^7.9.2",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"web-vitals": "^5.1.0"
|
||||
},
|
||||
@ -54,6 +58,7 @@
|
||||
"dev": "npm run typecheck && vite",
|
||||
"prebuild": "npm run generate-icons",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix",
|
||||
"build": "npm run typecheck && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
@ -94,9 +99,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@iconify-json/material-symbols": "^1.2.37",
|
||||
"@iconify-json/material-symbols": "^1.2.39",
|
||||
"@iconify/utils": "^3.0.2",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@ -109,7 +114,9 @@
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"globals": "^16.4.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"license-checker": "^25.0.1",
|
||||
"madge": "^8.0.0",
|
||||
|
@ -14,7 +14,7 @@ interface DragDropGridProps<T extends DragDropItem> {
|
||||
selectionMode: boolean;
|
||||
isAnimating: boolean;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
||||
renderItem: (item: T, index: number, refs: React.RefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
||||
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ interface PageThumbnailProps {
|
||||
selectionMode: boolean;
|
||||
movingPage: number | null;
|
||||
isAnimating: boolean;
|
||||
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||
pageRefs: React.RefObject<Map<string, HTMLDivElement>>;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
onTogglePage: (pageId: string) => void;
|
||||
onAnimateReorder: () => void;
|
||||
|
@ -8,7 +8,7 @@ import { StirlingFileStub } from "../../types/fileContext";
|
||||
import { FileId } from "../../types/file";
|
||||
|
||||
interface FileGridProps {
|
||||
files: Array<{ file: File; record?: StirlingFileStub }>;
|
||||
files: { file: File; record?: StirlingFileStub }[];
|
||||
onRemove?: (index: number) => void;
|
||||
onDoubleClick?: (item: { file: File; record?: StirlingFileStub }) => void;
|
||||
onView?: (item: { file: File; record?: StirlingFileStub }) => void;
|
||||
|
@ -8,7 +8,7 @@ export { useToast, ToastProvider, ToastRenderer };
|
||||
let _api: ReturnType<typeof createImperativeApi> | null = null;
|
||||
|
||||
function createImperativeApi() {
|
||||
const subscribers: Array<(fn: any) => void> = [];
|
||||
const subscribers: ((fn: any) => void)[] = [];
|
||||
let api: any = null;
|
||||
return {
|
||||
provide(instance: any) {
|
||||
|
@ -9,7 +9,7 @@ import NoToolsFound from './shared/NoToolsFound';
|
||||
import "./toolPicker/ToolPicker.css";
|
||||
|
||||
interface SearchResultsProps {
|
||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
||||
filteredTools: { item: [string, ToolRegistryEntry]; matchedText?: string }[];
|
||||
onSelect: (id: string) => void;
|
||||
searchQuery?: string;
|
||||
}
|
||||
@ -40,13 +40,13 @@ const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect,
|
||||
{group.tools.map(({ id, tool }) => {
|
||||
const matchedText = matchedTextMap.get(id);
|
||||
// Check if the match was from synonyms and show the actual synonym that matched
|
||||
const isSynonymMatch = matchedText && tool.synonyms?.some(synonym =>
|
||||
const isSynonymMatch = matchedText && tool.synonyms?.some(synonym =>
|
||||
matchedText.toLowerCase().includes(synonym.toLowerCase())
|
||||
);
|
||||
const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym =>
|
||||
const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym =>
|
||||
matchedText.toLowerCase().includes(synonym.toLowerCase())
|
||||
) : undefined;
|
||||
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
key={id}
|
||||
|
@ -10,7 +10,7 @@ import { renderToolButtons } from "./shared/renderToolButtons";
|
||||
interface ToolPickerProps {
|
||||
selectedToolKey: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
||||
filteredTools: { item: [string, ToolRegistryEntry]; matchedText?: string }[];
|
||||
isSearching?: boolean;
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ describe('ChangePermissionsSettings', () => {
|
||||
);
|
||||
|
||||
// Should render checkboxes for all permission types
|
||||
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
|
||||
const permissionKeys = Object.keys(defaultParameters) as (keyof ChangePermissionsParameters)[];
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
expect(checkboxes).toHaveLength(permissionKeys.length);
|
||||
|
||||
@ -55,7 +55,7 @@ describe('ChangePermissionsSettings', () => {
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
|
||||
const permissionKeys = Object.keys(defaultParameters) as (keyof ChangePermissionsParameters)[];
|
||||
|
||||
permissionKeys.forEach(permission => {
|
||||
expect(screen.getByText(`mock-changePermissions.permissions.${permission}.label`)).toBeInTheDocument();
|
||||
@ -183,13 +183,13 @@ describe('ChangePermissionsSettings', () => {
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
|
||||
const permissionKeys = Object.keys(defaultParameters) as (keyof ChangePermissionsParameters)[];
|
||||
permissionKeys.forEach(permission => {
|
||||
expect(mockT).toHaveBeenCalledWith(`changePermissions.permissions.${permission}.label`, permission);
|
||||
});
|
||||
});
|
||||
|
||||
test.each(Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>)('should handle %s permission type individually', (permission) => {
|
||||
test.each(Object.keys(defaultParameters) as (keyof ChangePermissionsParameters)[])('should handle %s permission type individually', (permission) => {
|
||||
const testParameters: ChangePermissionsParameters = {
|
||||
...defaultParameters,
|
||||
[permission]: true
|
||||
|
@ -14,7 +14,7 @@ const ChangePermissionsSettings = ({ parameters, onParameterChange, disabled = f
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Stack gap="xs">
|
||||
{(Object.keys(parameters) as Array<keyof ChangePermissionsParameters>).map((key) => (
|
||||
{(Object.keys(parameters) as (keyof ChangePermissionsParameters)[]).map((key) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
label={t(`changePermissions.permissions.${key}.label`, key)}
|
||||
|
@ -27,7 +27,7 @@ import { StirlingFile } from "../../../types/fileContext";
|
||||
interface ConvertSettingsProps {
|
||||
parameters: ConvertParameters;
|
||||
onParameterChange: <K extends keyof ConvertParameters>(key: K, value: ConvertParameters[K]) => void;
|
||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||
getAvailableToExtensions: (fromExtension: string) => {value: string, label: string, group: string}[];
|
||||
selectedFiles: StirlingFile[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ interface SanitizeSettingsProps {
|
||||
const SanitizeSettings = ({ parameters, onParameterChange, disabled = false }: SanitizeSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = (Object.keys(defaultParameters) as Array<keyof SanitizeParameters>).map((key) => ({
|
||||
const options = (Object.keys(defaultParameters) as (keyof SanitizeParameters)[]).map((key) => ({
|
||||
key: key,
|
||||
label: t(`sanitize.options.${key}`, key),
|
||||
description: t(`sanitize.options.${key}.desc`, `${key} from the PDF`),
|
||||
|
@ -14,7 +14,7 @@ export const renderToolButtons = (
|
||||
onSelect: (id: string) => void,
|
||||
showSubcategoryHeader: boolean = true,
|
||||
disableNavigation: boolean = false,
|
||||
searchResults?: Array<{ item: [string, any]; matchedText?: string }>
|
||||
searchResults?: { item: [string, any]; matchedText?: string }[]
|
||||
) => {
|
||||
// Create a map of matched text for quick lookup
|
||||
const matchedTextMap = new Map<string, string>();
|
||||
@ -32,7 +32,7 @@ export const renderToolButtons = (
|
||||
<div>
|
||||
{subcategory.tools.map(({ id, tool }) => {
|
||||
const matchedSynonym = matchedTextMap.get(id);
|
||||
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
key={id}
|
||||
|
@ -1,62 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
/**
|
||||
* Reusable tooltip for page selection functionality.
|
||||
* Can be used by any tool that uses the GeneralUtils.parsePageList syntax.
|
||||
*/
|
||||
export const usePageSelectionTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("pageSelection.tooltip.header.title", "Page Selection Guide")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
description: t("pageSelection.tooltip.description", "Choose which pages to use for the operation. Supports single pages, ranges, formulas, and the all keyword.")
|
||||
},
|
||||
{
|
||||
title: t("pageSelection.tooltip.individual.title", "Individual Pages"),
|
||||
description: t("pageSelection.tooltip.individual.description", "Enter numbers separated by commas."),
|
||||
bullets: [
|
||||
t("pageSelection.tooltip.individual.bullet1", "<strong>1,3,5</strong> → selects pages 1, 3, 5"),
|
||||
t("pageSelection.tooltip.individual.bullet2", "<strong>2,7,12</strong> → selects pages 2, 7, 12")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("pageSelection.tooltip.ranges.title", "Page Ranges"),
|
||||
description: t("pageSelection.tooltip.ranges.description", "Use - for consecutive pages."),
|
||||
bullets: [
|
||||
t("pageSelection.tooltip.ranges.bullet1", "<strong>3-6</strong> → selects pages 3–6"),
|
||||
t("pageSelection.tooltip.ranges.bullet2", "<strong>10-15</strong> → selects pages 10–15"),
|
||||
t("pageSelection.tooltip.ranges.bullet3", "<strong>5-</strong> → selects pages 5 to end")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("pageSelection.tooltip.mathematical.title", "Mathematical Functions"),
|
||||
description: t("pageSelection.tooltip.mathematical.description", "Use n in formulas for patterns."),
|
||||
bullets: [
|
||||
t("pageSelection.tooltip.mathematical.bullet2", "<strong>2n-1</strong> → all odd pages (1, 3, 5…)"),
|
||||
t("pageSelection.tooltip.mathematical.bullet1", "<strong>2n</strong> → all even pages (2, 4, 6…)"),
|
||||
t("pageSelection.tooltip.mathematical.bullet3", "<strong>3n</strong> → every 3rd page (3, 6, 9…)"),
|
||||
t("pageSelection.tooltip.mathematical.bullet4", "<strong>4n-1</strong> → pages 3, 7, 11, 15…")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("pageSelection.tooltip.special.title", "Special Keywords"),
|
||||
bullets: [
|
||||
t("pageSelection.tooltip.special.bullet1", "<strong>all</strong> → selects all pages"),
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("pageSelection.tooltip.complex.title", "Complex Combinations"),
|
||||
description: t("pageSelection.tooltip.complex.description", "Mix different types."),
|
||||
bullets: [
|
||||
t("pageSelection.tooltip.complex.bullet1", "<strong>1,3-5,8,2n</strong> → pages 1, 3–5, 8, plus evens"),
|
||||
t("pageSelection.tooltip.complex.bullet2", "<strong>10-,2n-1</strong> → from page 10 to end + odd pages")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -14,19 +14,19 @@ interface SearchLayerProps {
|
||||
}
|
||||
|
||||
interface SearchResultState {
|
||||
results: Array<{
|
||||
results: {
|
||||
pageIndex: number;
|
||||
rects: Array<{
|
||||
rects: {
|
||||
origin: { x: number; y: number };
|
||||
size: { width: number; height: number };
|
||||
}>;
|
||||
}>;
|
||||
}[];
|
||||
}[];
|
||||
activeResultIndex?: number;
|
||||
}
|
||||
|
||||
export function CustomSearchLayer({
|
||||
pageIndex,
|
||||
scale,
|
||||
export function CustomSearchLayer({
|
||||
pageIndex,
|
||||
scale,
|
||||
highlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.BACKGROUND,
|
||||
activeHighlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.ACTIVE_BACKGROUND,
|
||||
opacity = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.OPACITY,
|
||||
@ -42,17 +42,17 @@ export function CustomSearchLayer({
|
||||
if (!searchProvides) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
|
||||
// Auto-scroll to active search result
|
||||
if (state?.results && state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
|
||||
const activeResult = state.results[state.activeResultIndex];
|
||||
if (activeResult) {
|
||||
if (activeResult) {
|
||||
const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number
|
||||
scrollActions.scrollToPage(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setSearchResultState(state);
|
||||
});
|
||||
|
||||
@ -69,7 +69,7 @@ export function CustomSearchLayer({
|
||||
const filtered = searchResultState.results
|
||||
.map((result, originalIndex) => ({ result, originalIndex }))
|
||||
.filter(({ result }) => result.pageIndex === pageIndex);
|
||||
|
||||
|
||||
return filtered;
|
||||
}, [searchResultState, pageIndex]);
|
||||
|
||||
@ -78,7 +78,7 @@ export function CustomSearchLayer({
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
@ -117,4 +117,4 @@ export function CustomSearchLayer({
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
interface SearchResult {
|
||||
pageIndex: number;
|
||||
rects: Array<{
|
||||
rects: {
|
||||
origin: { x: number; y: number };
|
||||
size: { width: number; height: number };
|
||||
}>;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -17,7 +17,7 @@ interface SearchResult {
|
||||
export function SearchAPIBridge() {
|
||||
const { provides: search } = useSearch();
|
||||
const { registerBridge } = useViewer();
|
||||
|
||||
|
||||
const [localState, setLocalState] = useState({
|
||||
results: null as SearchResult[] | null,
|
||||
activeIndex: 0
|
||||
@ -32,7 +32,7 @@ export function SearchAPIBridge() {
|
||||
results: state?.results || null,
|
||||
activeIndex: (state?.activeResultIndex || 0) + 1 // Convert to 1-based index
|
||||
};
|
||||
|
||||
|
||||
setLocalState(prevState => {
|
||||
// Only update if state actually changed
|
||||
if (prevState.results !== newState.results || prevState.activeIndex !== newState.activeIndex) {
|
||||
|
@ -101,7 +101,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||
handleReaderToggle: () => void;
|
||||
|
||||
// Computed values
|
||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
|
||||
filteredTools: { item: [string, ToolRegistryEntry]; matchedText?: string }[]; // Filtered by search
|
||||
isPanelVisible: boolean;
|
||||
}
|
||||
|
||||
|
@ -82,10 +82,10 @@ interface RotationState {
|
||||
|
||||
interface SearchResult {
|
||||
pageIndex: number;
|
||||
rects: Array<{
|
||||
rects: {
|
||||
origin: { x: number; y: number };
|
||||
size: { width: number; height: number };
|
||||
}>;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface SearchState {
|
||||
@ -179,7 +179,7 @@ interface ViewerContextType {
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
// Bridge registration - internal use by bridges
|
||||
// Bridge registration - internal use by bridges
|
||||
registerBridge: (type: string, ref: BridgeRef) => void;
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ const DEBUG = process.env.NODE_ENV === 'development';
|
||||
*/
|
||||
class SimpleMutex {
|
||||
private locked = false;
|
||||
private queue: Array<() => void> = [];
|
||||
private queue: (() => void)[] = [];
|
||||
|
||||
async lock(): Promise<void> {
|
||||
if (!this.locked) {
|
||||
@ -151,7 +151,7 @@ interface AddFileOptions {
|
||||
files?: File[];
|
||||
|
||||
// For 'processed' files
|
||||
filesWithThumbnails?: Array<{ file: File; thumbnail?: string; pageCount?: number }>;
|
||||
filesWithThumbnails?: { file: File; thumbnail?: string; pageCount?: number }[];
|
||||
|
||||
// Insertion position
|
||||
insertAfterPageId?: string;
|
||||
@ -165,8 +165,8 @@ interface AddFileOptions {
|
||||
*/
|
||||
export async function addFiles(
|
||||
options: AddFileOptions,
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
stateRef: React.RefObject<FileContextState>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
lifecycleManager: FileLifecycleManager,
|
||||
enablePersistence: boolean = false
|
||||
@ -278,7 +278,7 @@ export async function consumeFiles(
|
||||
inputFileIds: FileId[],
|
||||
outputStirlingFiles: StirlingFile[],
|
||||
outputStirlingFileStubs: StirlingFileStub[],
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>
|
||||
): Promise<FileId[]> {
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputStirlingFiles.length} output files with pre-created stubs`);
|
||||
@ -355,9 +355,9 @@ export async function consumeFiles(
|
||||
* Helper function to restore files to filesRef and manage IndexedDB cleanup
|
||||
*/
|
||||
async function restoreFilesAndCleanup(
|
||||
filesToRestore: Array<{ file: File; record: StirlingFileStub }>,
|
||||
filesToRestore: { file: File; record: StirlingFileStub }[],
|
||||
fileIdsToRemove: FileId[],
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
indexedDB?: { deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||
): Promise<void> {
|
||||
// Remove files from filesRef
|
||||
@ -406,7 +406,7 @@ export async function undoConsumeFiles(
|
||||
inputFiles: File[],
|
||||
inputStirlingFileStubs: StirlingFileStub[],
|
||||
outputFileIds: FileId[],
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any>; deleteFile: (fileId: FileId) => Promise<void> } | null
|
||||
): Promise<void> {
|
||||
@ -468,8 +468,8 @@ export async function undoConsumeFiles(
|
||||
export async function addStirlingFileStubs(
|
||||
stirlingFileStubs: StirlingFileStub[],
|
||||
options: { insertAfterPageId?: string; selectFiles?: boolean } = {},
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
stateRef: React.RefObject<FileContextState>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>,
|
||||
_lifecycleManager: FileLifecycleManager
|
||||
): Promise<StirlingFile[]> {
|
||||
|
@ -15,8 +15,8 @@ import {
|
||||
* Create stable selectors using stateRef and filesRef
|
||||
*/
|
||||
export function createFileSelectors(
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||
stateRef: React.RefObject<FileContextState>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>
|
||||
): FileContextSelectors {
|
||||
return {
|
||||
getFile: (id: FileId) => {
|
||||
@ -111,7 +111,7 @@ export function buildQuickKeySet(stirlingFileStubs: Record<FileId, StirlingFileS
|
||||
/**
|
||||
* Helper for building quickKey sets from IndexedDB metadata
|
||||
*/
|
||||
export function buildQuickKeySetFromMetadata(metadata: Array<{ name: string; size: number; lastModified: number }>): Set<string> {
|
||||
export function buildQuickKeySetFromMetadata(metadata: { name: string; size: number; lastModified: number }[]): Set<string> {
|
||||
const quickKeys = new Set<string>();
|
||||
metadata.forEach(meta => {
|
||||
// Format: name|size|lastModified (same as createQuickKey)
|
||||
@ -125,8 +125,8 @@ export function buildQuickKeySetFromMetadata(metadata: Array<{ name: string; siz
|
||||
* Get primary file (first in list) - commonly used pattern
|
||||
*/
|
||||
export function getPrimaryFile(
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>
|
||||
stateRef: React.RefObject<FileContextState>,
|
||||
filesRef: React.RefObject<Map<FileId, File>>
|
||||
): { file?: File; record?: StirlingFileStub } {
|
||||
const primaryFileId = stateRef.current.files.ids[0];
|
||||
if (!primaryFileId) return {};
|
||||
|
@ -16,7 +16,7 @@ export class FileLifecycleManager {
|
||||
private fileGenerations = new Map<string, number>(); // Generation tokens to prevent stale cleanup
|
||||
|
||||
constructor(
|
||||
private filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
private filesRef: React.RefObject<Map<FileId, File>>,
|
||||
private dispatch: React.Dispatch<FileContextAction>
|
||||
) {}
|
||||
|
||||
@ -34,7 +34,7 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Clean up resources for a specific file (with stateRef access for complete cleanup)
|
||||
*/
|
||||
cleanupFile = (fileId: FileId, stateRef?: React.MutableRefObject<any>): void => {
|
||||
cleanupFile = (fileId: FileId, stateRef?: React.RefObject<any>): void => {
|
||||
// Use comprehensive cleanup (same as removeFiles)
|
||||
this.cleanupAllResourcesForFile(fileId, stateRef);
|
||||
|
||||
@ -68,7 +68,7 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Schedule delayed cleanup for a file with generation token to prevent stale cleanup
|
||||
*/
|
||||
scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: React.MutableRefObject<any>): void => {
|
||||
scheduleCleanup = (fileId: FileId, delay: number = 30000, stateRef?: React.RefObject<any>): void => {
|
||||
// Cancel existing timer
|
||||
const existingTimer = this.cleanupTimers.get(fileId);
|
||||
if (existingTimer) {
|
||||
@ -101,7 +101,7 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Remove a file immediately with complete resource cleanup
|
||||
*/
|
||||
removeFiles = (fileIds: FileId[], stateRef?: React.MutableRefObject<any>): void => {
|
||||
removeFiles = (fileIds: FileId[], stateRef?: React.RefObject<any>): void => {
|
||||
fileIds.forEach(fileId => {
|
||||
// Clean up all resources for this file
|
||||
this.cleanupAllResourcesForFile(fileId, stateRef);
|
||||
@ -114,7 +114,7 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Complete resource cleanup for a single file
|
||||
*/
|
||||
private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.MutableRefObject<any>): void => {
|
||||
private cleanupAllResourcesForFile = (fileId: FileId, stateRef?: React.RefObject<any>): void => {
|
||||
// Remove from files ref
|
||||
this.filesRef.current.delete(fileId);
|
||||
|
||||
@ -166,7 +166,7 @@ export class FileLifecycleManager {
|
||||
/**
|
||||
* Update file record with race condition guards
|
||||
*/
|
||||
updateStirlingFileStub = (fileId: FileId, updates: Partial<StirlingFileStub>, stateRef?: React.MutableRefObject<any>): void => {
|
||||
updateStirlingFileStub = (fileId: FileId, updates: Partial<StirlingFileStub>, stateRef?: React.RefObject<any>): void => {
|
||||
// Guard against updating removed files (race condition protection)
|
||||
if (!this.filesRef.current.has(fileId)) {
|
||||
if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`);
|
||||
|
@ -125,7 +125,7 @@ describe('useAddPasswordParameters', () => {
|
||||
expect(result.current.validateParameters()).toBe(true);
|
||||
});
|
||||
|
||||
test.each(Object.keys(defaultChangePermissionsParameters) as Array<keyof ChangePermissionsParameters>)('should handle boolean restriction parameter %s', (param) => {
|
||||
test.each(Object.keys(defaultChangePermissionsParameters) as (keyof ChangePermissionsParameters)[])('should handle boolean restriction parameter %s', (param) => {
|
||||
const { result } = renderHook(() => useAddPasswordParameters());
|
||||
|
||||
act(() => {
|
||||
|
@ -96,7 +96,7 @@ describe('useChangePermissionsOperation', () => {
|
||||
// Verify the form data contains the file
|
||||
expect(formData.get('fileInput')).toBe(testFile);
|
||||
|
||||
(Object.keys(testParameters) as Array<keyof ChangePermissionsParameters>).forEach(key => {
|
||||
(Object.keys(testParameters) as (keyof ChangePermissionsParameters)[]).forEach(key => {
|
||||
expect(formData.get(key), `Parameter ${key} should be set correctly`).toBe(testParameters[key].toString());
|
||||
});
|
||||
});
|
||||
|
@ -30,7 +30,7 @@ describe('useChangePermissionsParameters', () => {
|
||||
test('should update all permission parameters', () => {
|
||||
const { result } = renderHook(() => useChangePermissionsParameters());
|
||||
|
||||
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
|
||||
const permissionKeys = Object.keys(defaultParameters) as (keyof ChangePermissionsParameters)[];
|
||||
|
||||
// Set all to true
|
||||
act(() => {
|
||||
@ -99,7 +99,7 @@ describe('useChangePermissionsParameters', () => {
|
||||
|
||||
// Set all restrictions - should still be valid
|
||||
act(() => {
|
||||
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
|
||||
const permissionKeys = Object.keys(defaultParameters) as (keyof ChangePermissionsParameters)[];
|
||||
permissionKeys.forEach(key => {
|
||||
result.current.updateParameter(key, true);
|
||||
});
|
||||
|
@ -42,8 +42,8 @@ export interface ConvertParameters extends BaseParameters {
|
||||
|
||||
export interface ConvertParametersHook extends BaseParametersHook<ConvertParameters> {
|
||||
getEndpoint: () => string;
|
||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||
analyzeFileTypes: (files: Array<{name: string}>) => void;
|
||||
getAvailableToExtensions: (fromExtension: string) => {value: string, label: string, group: string}[];
|
||||
analyzeFileTypes: (files: {name: string}[]) => void;
|
||||
}
|
||||
|
||||
export const defaultParameters: ConvertParameters = {
|
||||
@ -157,7 +157,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
||||
const getAvailableToExtensions = getAvailableToExtensionsUtil;
|
||||
|
||||
|
||||
const analyzeFileTypes = useCallback((files: Array<{name: string}>) => {
|
||||
const analyzeFileTypes = useCallback((files: {name: string}[]) => {
|
||||
if (files.length === 0) {
|
||||
// No files - only reset smart detection, keep user's format choices
|
||||
baseHook.setParameters(prev => {
|
||||
|
@ -345,7 +345,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
|
||||
test('should handle malformed file objects', () => {
|
||||
const { result } = renderHook(() => useConvertParameters());
|
||||
|
||||
const malformedFiles: Array<{name: string}> = [
|
||||
const malformedFiles: {name: string}[] = [
|
||||
{ name: 'valid.pdf' },
|
||||
// @ts-expect-error - Testing runtime resilience
|
||||
{ name: null },
|
||||
|
@ -4,7 +4,7 @@ import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } f
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type SubcategoryIdMap = {
|
||||
[subcategoryId in SubcategoryId]: Array<{ id: string /* FIX ME: Should be ToolId */; tool: ToolRegistryEntry }>;
|
||||
[subcategoryId in SubcategoryId]: { id: string /* FIX ME: Should be ToolId */; tool: ToolRegistryEntry }[];
|
||||
}
|
||||
|
||||
type GroupedTools = {
|
||||
@ -28,7 +28,7 @@ export interface ToolSection {
|
||||
};
|
||||
|
||||
export function useToolSections(
|
||||
filteredTools: Array<{ item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }>,
|
||||
filteredTools: { item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }[],
|
||||
searchQuery?: string
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
@ -37,7 +37,7 @@ export function useToolSections(
|
||||
if (!filteredTools || !Array.isArray(filteredTools)) {
|
||||
return {} as GroupedTools;
|
||||
}
|
||||
|
||||
|
||||
const grouped = {} as GroupedTools;
|
||||
filteredTools.forEach(({ item: [id, tool] }) => {
|
||||
const categoryId = tool.categoryId;
|
||||
@ -102,7 +102,7 @@ export function useToolSections(
|
||||
if (!filteredTools || !Array.isArray(filteredTools)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const subMap = {} as SubcategoryIdMap;
|
||||
const seen = new Set<string /* FIX ME: Should be ToolId */>();
|
||||
filteredTools.forEach(({ item: [id, tool] }) => {
|
||||
|
@ -6,10 +6,10 @@ export interface AutomationConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
operations: Array<{
|
||||
operations: {
|
||||
operation: string;
|
||||
parameters: any;
|
||||
}>;
|
||||
}[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@ -35,7 +35,7 @@ class AutomationStorage {
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
||||
store.createIndex('name', 'name', { unique: false });
|
||||
@ -49,18 +49,18 @@ class AutomationStorage {
|
||||
if (!this.db) {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
|
||||
return this.db;
|
||||
}
|
||||
|
||||
async saveAutomation(automation: Omit<AutomationConfig, 'id' | 'createdAt' | 'updatedAt'>): Promise<AutomationConfig> {
|
||||
const db = await this.ensureDB();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
|
||||
const automationWithMeta: AutomationConfig = {
|
||||
id: `automation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
...automation,
|
||||
@ -85,7 +85,7 @@ class AutomationStorage {
|
||||
|
||||
async updateAutomation(automation: AutomationConfig): Promise<AutomationConfig> {
|
||||
const db = await this.ensureDB();
|
||||
|
||||
|
||||
const updatedAutomation: AutomationConfig = {
|
||||
...automation,
|
||||
updatedAt: new Date().toISOString()
|
||||
@ -165,13 +165,13 @@ class AutomationStorage {
|
||||
|
||||
async searchAutomations(query: string): Promise<AutomationConfig[]> {
|
||||
const automations = await this.getAllAutomations();
|
||||
|
||||
|
||||
if (!query.trim()) {
|
||||
return automations;
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return automations.filter(automation =>
|
||||
return automations.filter(automation =>
|
||||
automation.name.toLowerCase().includes(lowerQuery) ||
|
||||
(automation.description && automation.description.toLowerCase().includes(lowerQuery)) ||
|
||||
automation.operations.some(op => op.operation.toLowerCase().includes(lowerQuery))
|
||||
@ -180,4 +180,4 @@ class AutomationStorage {
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const automationStorage = new AutomationStorage();
|
||||
export const automationStorage = new AutomationStorage();
|
||||
|
@ -10,12 +10,12 @@ import { FileId } from '../types/file';
|
||||
|
||||
export interface ProcessedFileMetadata {
|
||||
totalPages: number;
|
||||
pages: Array<{
|
||||
pages: {
|
||||
pageNumber: number;
|
||||
thumbnail?: string;
|
||||
rotation: number;
|
||||
splitBefore: boolean;
|
||||
}>;
|
||||
}[];
|
||||
thumbnailUrl?: string; // Page 1 thumbnail for FileEditor
|
||||
lastProcessed: number;
|
||||
}
|
||||
|
@ -8,8 +8,8 @@ export class ProcessingErrorHandler {
|
||||
* Create a ProcessingError from an unknown error
|
||||
*/
|
||||
static createProcessingError(
|
||||
error: unknown,
|
||||
retryCount: number = 0,
|
||||
error: unknown,
|
||||
retryCount: number = 0,
|
||||
maxRetries: number = this.DEFAULT_MAX_RETRIES
|
||||
): ProcessingError {
|
||||
const originalError = error instanceof Error ? error : new Error(String(error));
|
||||
@ -17,7 +17,7 @@ export class ProcessingErrorHandler {
|
||||
|
||||
// Determine error type based on error message and properties
|
||||
const errorType = this.determineErrorType(originalError, message);
|
||||
|
||||
|
||||
// Determine if error is recoverable
|
||||
const recoverable = this.isRecoverable(errorType, retryCount, maxRetries);
|
||||
|
||||
@ -38,7 +38,7 @@ export class ProcessingErrorHandler {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
// Network-related errors
|
||||
if (lowerMessage.includes('network') ||
|
||||
if (lowerMessage.includes('network') ||
|
||||
lowerMessage.includes('fetch') ||
|
||||
lowerMessage.includes('connection')) {
|
||||
return 'network';
|
||||
@ -83,8 +83,8 @@ export class ProcessingErrorHandler {
|
||||
* Determine if an error is recoverable based on type and retry count
|
||||
*/
|
||||
private static isRecoverable(
|
||||
errorType: ProcessingError['type'],
|
||||
retryCount: number,
|
||||
errorType: ProcessingError['type'],
|
||||
retryCount: number,
|
||||
maxRetries: number
|
||||
): boolean {
|
||||
// Never recoverable
|
||||
@ -113,22 +113,22 @@ export class ProcessingErrorHandler {
|
||||
switch (errorType) {
|
||||
case 'network':
|
||||
return 'Network connection failed. Please check your internet connection and try again.';
|
||||
|
||||
|
||||
case 'memory':
|
||||
return 'Insufficient memory to process this file. Try closing other applications or processing a smaller file.';
|
||||
|
||||
|
||||
case 'timeout':
|
||||
return 'Processing timed out. This file may be too large or complex to process.';
|
||||
|
||||
|
||||
case 'cancelled':
|
||||
return 'Processing was cancelled by user.';
|
||||
|
||||
|
||||
case 'corruption':
|
||||
return 'This PDF file appears to be corrupted or encrypted. Please try a different file.';
|
||||
|
||||
|
||||
case 'parsing':
|
||||
return `Failed to process PDF: ${originalMessage}`;
|
||||
|
||||
|
||||
default:
|
||||
return `Processing failed: ${originalMessage}`;
|
||||
}
|
||||
@ -149,7 +149,7 @@ export class ProcessingErrorHandler {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = this.createProcessingError(error, attempt, maxRetries);
|
||||
|
||||
|
||||
// Notify error handler
|
||||
if (onError) {
|
||||
onError(lastError);
|
||||
@ -168,7 +168,7 @@ export class ProcessingErrorHandler {
|
||||
// Wait before retry with progressive backoff
|
||||
const delay = this.RETRY_DELAYS[Math.min(attempt, this.RETRY_DELAYS.length - 1)];
|
||||
await this.delay(delay);
|
||||
|
||||
|
||||
console.log(`Retrying operation (attempt ${attempt + 2}/${maxRetries + 1}) after ${delay}ms delay`);
|
||||
}
|
||||
}
|
||||
@ -207,7 +207,7 @@ export class ProcessingErrorHandler {
|
||||
*/
|
||||
static createTimeoutController(timeoutMs: number): AbortController {
|
||||
const controller = new AbortController();
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
controller.abort();
|
||||
}, timeoutMs);
|
||||
@ -233,7 +233,7 @@ export class ProcessingErrorHandler {
|
||||
'Try refreshing the page',
|
||||
'Try again in a few moments'
|
||||
];
|
||||
|
||||
|
||||
case 'memory':
|
||||
return [
|
||||
'Close other browser tabs or applications',
|
||||
@ -241,14 +241,14 @@ export class ProcessingErrorHandler {
|
||||
'Restart your browser',
|
||||
'Use a device with more memory'
|
||||
];
|
||||
|
||||
|
||||
case 'timeout':
|
||||
return [
|
||||
'Try processing a smaller file',
|
||||
'Break large files into smaller sections',
|
||||
'Check your internet connection speed'
|
||||
];
|
||||
|
||||
|
||||
case 'corruption':
|
||||
return [
|
||||
'Verify the PDF file opens in other applications',
|
||||
@ -256,14 +256,14 @@ export class ProcessingErrorHandler {
|
||||
'Try a different PDF file',
|
||||
'Contact the file creator if it appears corrupted'
|
||||
];
|
||||
|
||||
|
||||
case 'parsing':
|
||||
return [
|
||||
'Verify this is a valid PDF file',
|
||||
'Try a different PDF file',
|
||||
'Contact support if the problem persists'
|
||||
];
|
||||
|
||||
|
||||
default:
|
||||
return [
|
||||
'Try refreshing the page',
|
||||
@ -279,4 +279,4 @@ export class ProcessingErrorHandler {
|
||||
private static delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
type ConversionEndpoint
|
||||
} from '../helpers/conversionEndpointDiscovery';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import fs from 'fs';
|
||||
|
||||
// Test configuration
|
||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:5173';
|
||||
@ -238,7 +238,7 @@ async function testConversion(page: Page, conversion: ConversionEndpoint) {
|
||||
// Save and verify file is not empty
|
||||
const path = await download.path();
|
||||
if (path) {
|
||||
const fs = require('fs');
|
||||
// fs is already imported at the top of the file
|
||||
const stats = fs.statSync(path);
|
||||
expect(stats.size).toBeGreaterThan(0);
|
||||
|
||||
|
@ -20,9 +20,9 @@ export function createTestStirlingFile(
|
||||
* Create multiple StirlingFile objects for testing
|
||||
*/
|
||||
export function createTestFilesWithId(
|
||||
files: Array<{ name: string; content?: string; type?: string }>
|
||||
files: { name: string; content?: string; type?: string }[]
|
||||
): StirlingFile[] {
|
||||
return files.map(({ name, content = 'test content', type = 'application/pdf' }) =>
|
||||
createTestStirlingFile(name, content, type)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {
|
||||
import {
|
||||
CONVERSION_ENDPOINTS,
|
||||
ENDPOINT_NAMES,
|
||||
EXTENSION_TO_ENDPOINT,
|
||||
@ -11,15 +11,15 @@ import {
|
||||
*/
|
||||
export const getEndpointName = (fromExtension: string, toExtension: string): string => {
|
||||
if (!fromExtension || !toExtension) return '';
|
||||
|
||||
|
||||
let endpointKey = EXTENSION_TO_ENDPOINT[fromExtension]?.[toExtension];
|
||||
|
||||
// If no explicit mapping exists and we're converting to PDF,
|
||||
|
||||
// If no explicit mapping exists and we're converting to PDF,
|
||||
// fall back to 'any' which uses file-to-pdf endpoint
|
||||
if (!endpointKey && toExtension === 'pdf' && fromExtension !== 'any') {
|
||||
endpointKey = EXTENSION_TO_ENDPOINT['any']?.[toExtension];
|
||||
}
|
||||
|
||||
|
||||
return endpointKey || '';
|
||||
};
|
||||
|
||||
@ -29,7 +29,7 @@ export const getEndpointName = (fromExtension: string, toExtension: string): str
|
||||
export const getEndpointUrl = (fromExtension: string, toExtension: string): string => {
|
||||
const endpointName = getEndpointName(fromExtension, toExtension);
|
||||
if (!endpointName) return '';
|
||||
|
||||
|
||||
// Find the endpoint URL from CONVERSION_ENDPOINTS using the endpoint name
|
||||
for (const [key, endpoint] of Object.entries(CONVERSION_ENDPOINTS)) {
|
||||
if (ENDPOINT_NAMES[key as keyof typeof ENDPOINT_NAMES] === endpointName) {
|
||||
@ -64,7 +64,7 @@ export const isWebFormat = (extension: string): boolean => {
|
||||
* Gets available target extensions for a given source extension
|
||||
* Extracted from useConvertParameters to be reusable in automation settings
|
||||
*/
|
||||
export const getAvailableToExtensions = (fromExtension: string): Array<{value: string, label: string, group: string}> => {
|
||||
export const getAvailableToExtensions = (fromExtension: string): {value: string, label: string, group: string}[] => {
|
||||
if (!fromExtension) return [];
|
||||
|
||||
// Handle dynamic format identifiers (file-<extension>)
|
||||
@ -87,4 +87,4 @@ export const getAvailableToExtensions = (fromExtension: string): Array<{value: s
|
||||
return TO_FORMAT_OPTIONS.filter(option =>
|
||||
supportedExtensions.includes(option.value)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@ -70,9 +70,9 @@ export function scoreMatch(queryRaw: string, targetRaw: string): number {
|
||||
|
||||
export function minScoreForQuery(query: string): number {
|
||||
const len = normalizeText(query).length;
|
||||
if (len <= 3) return 40;
|
||||
if (len <= 6) return 30;
|
||||
return 25;
|
||||
if (len <= 3) return 40;
|
||||
if (len <= 6) return 30;
|
||||
return 25;
|
||||
}
|
||||
|
||||
// Decide if a target matches a query based on a threshold
|
||||
@ -82,8 +82,8 @@ export function isFuzzyMatch(query: string, target: string, minScore?: number):
|
||||
}
|
||||
|
||||
// Convenience: rank a list of items by best score across provided getters
|
||||
export function rankByFuzzy<T>(items: T[], query: string, getters: Array<(item: T) => string>, minScore?: number): Array<{ item: T; score: number; matchedText?: string }>{
|
||||
const results: Array<{ item: T; score: number; matchedText?: string }> = [];
|
||||
export function rankByFuzzy<T>(items: T[], query: string, getters: ((item: T) => string)[], minScore?: number): { item: T; score: number; matchedText?: string }[]{
|
||||
const results: { item: T; score: number; matchedText?: string }[] = [];
|
||||
const threshold = typeof minScore === 'number' ? minScore : minScoreForQuery(query);
|
||||
for (const item of items) {
|
||||
let best = 0;
|
||||
|
@ -18,10 +18,10 @@ export function filterToolRegistryByQuery(
|
||||
const nq = normalizeForSearch(query);
|
||||
const threshold = minScoreForQuery(query);
|
||||
|
||||
const exactName: Array<{ id: string; tool: ToolRegistryEntry; pos: number }> = [];
|
||||
const exactSyn: Array<{ id: string; tool: ToolRegistryEntry; text: string; pos: number }> = [];
|
||||
const fuzzyName: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
||||
const fuzzySyn: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
||||
const exactName: { id: string; tool: ToolRegistryEntry; pos: number }[] = [];
|
||||
const exactSyn: { id: string; tool: ToolRegistryEntry; text: string; pos: number }[] = [];
|
||||
const fuzzyName: { id: string; tool: ToolRegistryEntry; score: number; text: string }[] = [];
|
||||
const fuzzySyn: { id: string; tool: ToolRegistryEntry; score: number; text: string }[] = [];
|
||||
|
||||
for (const [id, tool] of entries) {
|
||||
const nameNorm = normalizeForSearch(tool.name || '');
|
||||
|
@ -13,7 +13,7 @@
|
||||
/* Language and Environment */
|
||||
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"jsx": "react-jsx", /* Specify what JSX code is generated. */
|
||||
"jsx": "react-jsx", /* Specify what JSX code is generated. */
|
||||
// "libReplacement": true, /* Enable lib replacement. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
@ -24,13 +24,12 @@
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "esnext", /* Specify what module code is generated. */
|
||||
"module": "esnext", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
},
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
@ -43,7 +42,7 @@
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
@ -113,6 +112,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"src/global.d.ts"
|
||||
, "vite.config.ts" ]
|
||||
"src/global.d.ts",
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user