diff --git a/.editorconfig b/.editorconfig index 5b76408cc..5e5445769 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,7 +24,7 @@ indent_size = 2 insert_final_newline = false trim_trailing_whitespace = false -[{*.js,*.jsx,*.ts,*.tsx}] +[{*.js,*.jsx,*.mjs,*.ts,*.tsx}] indent_size = 2 [*.css] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e38b82fb..b38abe5dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -147,6 +147,8 @@ jobs: cache-dependency-path: frontend/package-lock.json - name: Install frontend dependencies run: cd frontend && npm ci + - name: Lint frontend + run: cd frontend && npm run lint - name: Build frontend run: cd frontend && npm run build - name: Run frontend tests diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6ab09796f..4f627148e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -19,5 +19,6 @@ "yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing "stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting "redhat.vscode-yaml", // YAML extension for Visual Studio Code + "dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting ] } diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 000000000..c0f9fd678 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,42 @@ +// @ts-check + +import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; + +export default defineConfig( + eslint.configs.recommended, + tseslint.configs.recommended, + { + ignores: [ + "dist", // Contains 3rd party code + "public", // Contains 3rd party code + ], + }, + { + rules: { + "no-undef": "off", // Temporarily disabled until codebase conformant + "@typescript-eslint/no-empty-object-type": [ + "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 + allowInterfaces: 'with-single-extends', + }, + ], + "@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", + { + "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) + }, + ], + }, + } +); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7e6d23d50..342f0512f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -41,6 +41,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@eslint/js": "^9.34.0", "@iconify-json/material-symbols": "^1.2.33", "@iconify/utils": "^3.0.1", "@playwright/test": "^1.40.0", @@ -49,6 +50,7 @@ "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^4.5.0", "@vitest/coverage-v8": "^1.0.0", + "eslint": "^9.34.0", "jsdom": "^23.0.0", "license-checker": "^25.0.1", "madge": "^8.0.0", @@ -56,7 +58,8 @@ "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", - "typescript": "^5.8.3", + "typescript": "^5.9.2", + "typescript-eslint": "^8.42.0", "vite": "^6.3.5", "vitest": "^1.0.0" } @@ -1164,6 +1167,173 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", @@ -1217,6 +1387,72 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@iconify-json/material-symbols": { "version": "1.2.33", "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.33.tgz", @@ -2659,6 +2895,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.2.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", @@ -2709,15 +2952,80 @@ "@types/react": "*" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", - "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.42.0.tgz", + "integrity": "sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.40.0", - "@typescript-eslint/types": "^8.40.0", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/type-utils": "8.42.0", + "@typescript-eslint/utils": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.42.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.42.0.tgz", + "integrity": "sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.42.0.tgz", + "integrity": "sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.42.0", + "@typescript-eslint/types": "^8.42.0", "debug": "^4.3.4" }, "engines": { @@ -2731,10 +3039,28 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz", + "integrity": "sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", - "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz", + "integrity": "sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==", "dev": true, "license": "MIT", "engines": { @@ -2748,10 +3074,35 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.42.0.tgz", + "integrity": "sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0", + "@typescript-eslint/utils": "8.42.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/types": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", - "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.42.0.tgz", + "integrity": "sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==", "dev": true, "license": "MIT", "engines": { @@ -2763,16 +3114,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", - "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz", + "integrity": "sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.40.0", - "@typescript-eslint/tsconfig-utils": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/project-service": "8.42.0", + "@typescript-eslint/tsconfig-utils": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/visitor-keys": "8.42.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2817,14 +3168,38 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", - "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "node_modules/@typescript-eslint/utils": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.42.0.tgz", + "integrity": "sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.42.0", + "@typescript-eslint/types": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz", + "integrity": "sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.42.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3136,6 +3511,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -3162,6 +3547,23 @@ "node": ">= 6.0.0" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3236,6 +3638,13 @@ "node": ">=10" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -4028,6 +4437,13 @@ "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -4492,6 +4908,84 @@ "node": ">=0.10.0" } }, + "node_modules/eslint": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -4505,6 +4999,24 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -4519,6 +5031,32 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -4592,6 +5130,13 @@ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "dev": true }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -4622,6 +5167,20 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -4638,6 +5197,19 @@ "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", "license": "MIT" }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-selector": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", @@ -4705,6 +5277,44 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -4971,6 +5581,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -5014,6 +5637,13 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -5246,6 +5876,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -5267,6 +5907,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -5558,6 +6208,19 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "23.2.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz", @@ -5635,12 +6298,33 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5705,12 +6389,36 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", "dev": true }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/license-checker": { "version": "25.0.1", "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", @@ -6105,12 +6813,35 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6535,6 +7266,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -6736,6 +7474,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -6805,6 +7561,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-manager-detector": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", @@ -6869,6 +7670,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -7493,6 +8304,16 @@ "node": ">=18" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -9022,6 +9843,19 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", @@ -9045,9 +9879,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -9058,6 +9892,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.42.0.tgz", + "integrity": "sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.42.0", + "@typescript-eslint/parser": "8.42.0", + "@typescript-eslint/typescript-estree": "8.42.0", + "@typescript-eslint/utils": "8.42.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/ufo": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", @@ -9111,6 +9969,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -10541,6 +11409,16 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 160c96c18..d73e9ad97 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "predev": "npm run generate-icons", "dev": "npx tsc --noEmit && vite", "prebuild": "npm run generate-icons", + "lint": "npx eslint", "build": "npx tsc --noEmit && vite build", "preview": "vite preview", "typecheck": "tsc --noEmit", @@ -72,6 +73,7 @@ ] }, "devDependencies": { + "@eslint/js": "^9.34.0", "@iconify-json/material-symbols": "^1.2.33", "@iconify/utils": "^3.0.1", "@playwright/test": "^1.40.0", @@ -80,6 +82,7 @@ "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^4.5.0", "@vitest/coverage-v8": "^1.0.0", + "eslint": "^9.34.0", "jsdom": "^23.0.0", "license-checker": "^25.0.1", "madge": "^8.0.0", @@ -87,7 +90,8 @@ "postcss-cli": "^11.0.1", "postcss-preset-mantine": "^1.17.0", "postcss-simple-vars": "^7.0.1", - "typescript": "^5.8.3", + "typescript": "^5.9.2", + "typescript-eslint": "^8.42.0", "vite": "^6.3.5", "vitest": "^1.0.0" } diff --git a/frontend/scripts/generate-icons.js b/frontend/scripts/generate-icons.js index 0fd42a4df..d99414b66 100644 --- a/frontend/scripts/generate-icons.js +++ b/frontend/scripts/generate-icons.js @@ -107,7 +107,7 @@ async function main() { needsRegeneration = false; info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`); } - } catch (error) { + } catch { // If we can't parse existing file, regenerate needsRegeneration = true; } diff --git a/frontend/scripts/generate-licenses.js b/frontend/scripts/generate-licenses.js index aaac69800..3daf04d8d 100644 --- a/frontend/scripts/generate-licenses.js +++ b/frontend/scripts/generate-licenses.js @@ -24,7 +24,7 @@ try { // Install license-checker if not present try { require.resolve('license-checker'); - } catch (e) { + } catch { console.log('📦 Installing license-checker...'); execSync('npm install --save-dev license-checker', { stdio: 'inherit' }); } @@ -224,7 +224,7 @@ function getLicenseUrl(licenseType) { // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" if (licenseType.includes('AND') || licenseType.includes('OR')) { // Extract the first license from compound expressions for URL - const match = licenseType.match(/\(?\s*([A-Za-z0-9\-\.]+)/); + const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/); if (match && licenseUrls[match[1]]) { return licenseUrls[match[1]]; } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a30d3869..c3cbf3e89 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,9 @@ import "./styles/cookieconsent.css"; import "./index.css"; import { RightRailProvider } from "./contexts/RightRailContext"; +// Import file ID debugging helpers (development only) +import "./utils/fileIdSafety"; + // Loading component for i18next suspense const LoadingFallback = () => (
= ({ selectedTool }) => { const [isDragging, setIsDragging] = useState(false); const [isMobile, setIsMobile] = useState(false); - const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager(); - - // Wrapper for storeFile that generates UUID - const storeFileWithId = useCallback(async (file: File) => { - const fileId = createFileId(); // Generate UUID for storage - return await storeFile(file, fileId); - }, [storeFile]); + const { loadRecentFiles, handleRemoveFile, convertToFile } = useFileManager(); // File management handlers const isFileSupported = useCallback((fileName: string) => { diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index bf95d9796..8f71f59b0 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -1,42 +1,28 @@ -import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; +import React, { useState, useCallback, useRef, useMemo } from 'react'; import { Text, Center, Box, Notification, LoadingOverlay, Stack, Group, Portal } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; -import { useTranslation } from 'react-i18next'; -import UploadFileIcon from '@mui/icons-material/UploadFile'; -import { useFileSelection, useFileState, useFileManagement, useFileActions } from '../../contexts/FileContext'; +import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext'; import { useNavigationActions } from '../../contexts/NavigationContext'; -import { FileOperation } from '../../types/fileContext'; -import { fileStorage } from '../../services/fileStorage'; -import { generateThumbnailForFile } from '../../utils/thumbnailUtils'; import { zipFileService } from '../../services/zipFileService'; import { detectFileExtension } from '../../utils/fileUtils'; -import styles from './FileEditor.module.css'; import FileEditorThumbnail from './FileEditorThumbnail'; import FilePickerModal from '../shared/FilePickerModal'; import SkeletonLoader from '../shared/SkeletonLoader'; -import { FileId } from '../../types/file'; - +import { FileId, StirlingFile } from '../../types/fileContext'; interface FileEditorProps { - onOpenPageEditor?: (file: File) => void; - onMergeFiles?: (files: File[]) => void; + onOpenPageEditor?: () => void; + onMergeFiles?: (files: StirlingFile[]) => void; toolMode?: boolean; - showUpload?: boolean; - showBulkActions?: boolean; supportedExtensions?: string[]; } const FileEditor = ({ - onOpenPageEditor, - onMergeFiles, toolMode = false, - showUpload = true, - showBulkActions = true, supportedExtensions = ["pdf"] }: FileEditorProps) => { - const { t } = useTranslation(); // Utility function to check if a file extension is supported const isFileSupported = useCallback((fileName: string): boolean => { @@ -49,13 +35,10 @@ const FileEditor = ({ const { addFiles, removeFiles, reorderFiles } = useFileManagement(); // Extract needed values from state (memoized to prevent infinite loops) - const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]); - const activeFileRecords = useMemo(() => selectors.getFileRecords(), [selectors.getFilesSignature()]); + const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [selectors.getFilesSignature()]); const selectedFileIds = state.ui.selectedFileIds; - const isProcessing = state.ui.isProcessing; - // Get the real context actions - const { actions } = useFileActions(); + // Get navigation actions const { actions: navActions } = useNavigationActions(); // Get file selection context @@ -92,10 +75,10 @@ const FileEditor = ({ const contextSelectedIdsRef = useRef([]); contextSelectedIdsRef.current = contextSelectedIds; - // Use activeFileRecords directly - no conversion needed + // Use activeStirlingFileStubs directly - no conversion needed const localSelectedIds = contextSelectedIds; - // Helper to convert FileRecord to FileThumbnail format + // Helper to convert StirlingFileStub to FileThumbnail format const recordToFileItem = useCallback((record: any) => { const file = selectors.getFile(record.id); if (!file) return null; @@ -161,29 +144,9 @@ const FileEditor = ({ if (extractionResult.success) { allExtractedFiles.push(...extractionResult.extractedFiles); - // Record ZIP extraction operation - const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const operation: FileOperation = { - id: operationId, - type: 'convert', - timestamp: Date.now(), - fileIds: extractionResult.extractedFiles.map(f => f.name) as FileId[] /* FIX ME: This doesn't seem right */, - status: 'pending', - metadata: { - originalFileName: file.name, - outputFileNames: extractionResult.extractedFiles.map(f => f.name), - fileSize: file.size, - parameters: { - extractionType: 'zip', - extractedCount: extractionResult.extractedCount, - totalFiles: extractionResult.totalFiles - } + if (extractionResult.errors.length > 0) { + errors.push(...extractionResult.errors); } - }; - - if (extractionResult.errors.length > 0) { - errors.push(...extractionResult.errors); - } } else { errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`); } @@ -213,25 +176,6 @@ const FileEditor = ({ // Process all extracted files if (allExtractedFiles.length > 0) { - // Record upload operations for PDF files - for (const file of allExtractedFiles) { - const operationId = `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const operation: FileOperation = { - id: operationId, - type: 'upload', - timestamp: Date.now(), - fileIds: [file.name as FileId /* This doesn't seem right */], - status: 'pending', - metadata: { - originalFileName: file.name, - fileSize: file.size, - parameters: { - uploadMethod: 'drag-drop' - } - } - }; - } - // Add files to context (they will be processed automatically) await addFiles(allExtractedFiles); setStatus(`Added ${allExtractedFiles.length} files`); @@ -252,27 +196,10 @@ const FileEditor = ({ } }, [addFiles]); - const selectAll = useCallback(() => { - setSelectedFiles(activeFileRecords.map(r => r.id)); // Use FileRecord IDs directly - }, [activeFileRecords, setSelectedFiles]); - - const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]); - - const closeAllFiles = useCallback(() => { - if (activeFileRecords.length === 0) return; - - // Remove all files from context but keep in storage - const allFileIds = activeFileRecords.map(record => record.id); - removeFiles(allFileIds, false); // false = keep in storage - - // Clear selections - setSelectedFiles([]); - }, [activeFileRecords, removeFiles, setSelectedFiles]); - const toggleFile = useCallback((fileId: FileId) => { const currentSelectedIds = contextSelectedIdsRef.current; - const targetRecord = activeFileRecords.find(r => r.id === fileId); + const targetRecord = activeStirlingFileStubs.find(r => r.id === fileId); if (!targetRecord) return; const contextFileId = fileId; // No need to create a new ID @@ -302,21 +229,12 @@ const FileEditor = ({ // Update context (this automatically updates tool selection since they use the same action) setSelectedFiles(newSelection); - }, [setSelectedFiles, toolMode, setStatus, activeFileRecords]); + }, [setSelectedFiles, toolMode, setStatus, activeStirlingFileStubs]); - const toggleSelectionMode = useCallback(() => { - setSelectionMode(prev => { - const newMode = !prev; - if (!newMode) { - setSelectedFiles([]); - } - return newMode; - }); - }, [setSelectedFiles]); // File reordering handler for drag and drop const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => { - const currentIds = activeFileRecords.map(r => r.id); + const currentIds = activeStirlingFileStubs.map(r => r.id); // Find indices const sourceIndex = currentIds.findIndex(id => id === sourceFileId); @@ -368,71 +286,34 @@ const FileEditor = ({ // Update status const moveCount = filesToMove.length; setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`); - }, [activeFileRecords, reorderFiles, setStatus]); + }, [activeStirlingFileStubs, reorderFiles, setStatus]); // File operations using context const handleDeleteFile = useCallback((fileId: FileId) => { - const record = activeFileRecords.find(r => r.id === fileId); + const record = activeStirlingFileStubs.find(r => r.id === fileId); const file = record ? selectors.getFile(record.id) : null; if (record && file) { - // Record close operation - const fileName = file.name; - const contextFileId = record.id; - const operationId = `close-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const operation: FileOperation = { - id: operationId, - type: 'remove', - timestamp: Date.now(), - fileIds: [fileName as FileId /* FIX ME: This doesn't seem right */], - status: 'pending', - metadata: { - originalFileName: fileName, - fileSize: record.size, - parameters: { - action: 'close', - reason: 'user_request' - } - } - }; - // Remove file from context but keep in storage (close, don't delete) + const contextFileId = record.id; removeFiles([contextFileId], false); // Remove from context selections const currentSelected = selectedFileIds.filter(id => id !== contextFileId); setSelectedFiles(currentSelected); } - }, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]); + }, [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds]); const handleViewFile = useCallback((fileId: FileId) => { - const record = activeFileRecords.find(r => r.id === fileId); + const record = activeStirlingFileStubs.find(r => r.id === fileId); if (record) { // Set the file as selected in context and switch to viewer for preview setSelectedFiles([fileId]); navActions.setWorkbench('viewer'); } - }, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]); - - const handleMergeFromHere = useCallback((fileId: FileId) => { - const startIndex = activeFileRecords.findIndex(r => r.id === fileId); - if (startIndex === -1) return; - - const recordsToMerge = activeFileRecords.slice(startIndex); - const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as File[]; - if (onMergeFiles) { - onMergeFiles(filesToMerge); - } - }, [activeFileRecords, selectors, onMergeFiles]); - - const handleSplitFile = useCallback((fileId: FileId) => { - const file = selectors.getFile(fileId); - if (file && onOpenPageEditor) { - onOpenPageEditor(file); - } - }, [selectors, onOpenPageEditor]); + }, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]); const handleLoadFromStorage = useCallback(async (selectedFiles: File[]) => { if (selectedFiles.length === 0) return; @@ -467,7 +348,7 @@ const FileEditor = ({ - {activeFileRecords.length === 0 && !zipExtractionProgress.isExtracting ? ( + {activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? (
📁 @@ -475,7 +356,7 @@ const FileEditor = ({ Upload PDF files, ZIP archives, or load from storage to get started
- ) : activeFileRecords.length === 0 && zipExtractionProgress.isExtracting ? ( + ) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? ( @@ -522,7 +403,7 @@ const FileEditor = ({ pointerEvents: 'auto' }} > - {activeFileRecords.map((record, index) => { + {activeStirlingFileStubs.map((record, index) => { const fileItem = recordToFileItem(record); if (!fileItem) return null; @@ -531,7 +412,7 @@ const FileEditor = ({ key={record.id} file={fileItem} index={index} - totalFiles={activeFileRecords.length} + totalFiles={activeStirlingFileStubs.length} selectedFiles={localSelectedIds} selectionMode={selectionMode} onToggleFile={toggleFile} diff --git a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx index e82483898..7e7370785 100644 --- a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx @@ -44,7 +44,6 @@ const FileEditorThumbnail = ({ selectedFiles, onToggleFile, onDeleteFile, - onViewFile, onSetStatus, onReorderFiles, onDownloadFile, @@ -61,8 +60,8 @@ const FileEditorThumbnail = ({ // Resolve the actual File object for pin/unpin operations const actualFile = useMemo(() => { - return activeFiles.find((f: File) => f.name === file.name && f.size === file.size); - }, [activeFiles, file.name, file.size]); + return activeFiles.find(f => f.fileId === file.id); + }, [activeFiles, file.id]); const isPinned = actualFile ? isFilePinned(actualFile) : false; const downloadSelectedFile = useCallback(() => { diff --git a/frontend/src/components/fileManager/FileDetails.tsx b/frontend/src/components/fileManager/FileDetails.tsx index dcd460644..b1e7e93e3 100644 --- a/frontend/src/components/fileManager/FileDetails.tsx +++ b/frontend/src/components/fileManager/FileDetails.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Stack, Button, Box } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail'; @@ -11,27 +11,26 @@ interface FileDetailsProps { compact?: boolean; } -const FileDetails: React.FC = ({ +const FileDetails: React.FC = ({ compact = false }) => { const { selectedFiles, onOpenFiles, modalHeight } = useFileManagerContext(); const { t } = useTranslation(); const [currentFileIndex, setCurrentFileIndex] = useState(0); const [isAnimating, setIsAnimating] = useState(false); - + // Get the currently displayed file const currentFile = selectedFiles.length > 0 ? selectedFiles[currentFileIndex] : null; const hasSelection = selectedFiles.length > 0; - const hasMultipleFiles = selectedFiles.length > 1; // Use IndexedDB hook for the current file const { thumbnail: currentThumbnail } = useIndexedDBThumbnail(currentFile); - + // Get thumbnail for current file const getCurrentThumbnail = () => { return currentThumbnail; }; - + const handlePrevious = () => { if (isAnimating) return; setIsAnimating(true); @@ -40,7 +39,7 @@ const FileDetails: React.FC = ({ setIsAnimating(false); }, 150); }; - + const handleNext = () => { if (isAnimating) return; setIsAnimating(true); @@ -49,14 +48,14 @@ const FileDetails: React.FC = ({ setIsAnimating(false); }, 150); }; - + // Reset index when selection changes React.useEffect(() => { if (currentFileIndex >= selectedFiles.length) { setCurrentFileIndex(0); } }, [selectedFiles.length, currentFileIndex]); - + if (compact) { return ( = ({ onNext={handleNext} /> - + {/* Section 2: File Details */} - -
); diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx index 1eda1f6c8..be926f85d 100644 --- a/frontend/src/components/pageEditor/FileThumbnail.tsx +++ b/frontend/src/components/pageEditor/FileThumbnail.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react'; -import { Text, ActionIcon, CheckboxIndicator } from '@mantine/core'; +import { ActionIcon, CheckboxIndicator } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined'; @@ -44,7 +44,6 @@ const FileThumbnail = ({ selectedFiles, onToggleFile, onDeleteFile, - onViewFile, onSetStatus, onReorderFiles, onDownloadFile, @@ -61,8 +60,8 @@ const FileThumbnail = ({ // Resolve the actual File object for pin/unpin operations const actualFile = useMemo(() => { - return activeFiles.find((f: File) => f.name === file.name && f.size === file.size); - }, [activeFiles, file.name, file.size]); + return activeFiles.find(f => f.fileId === file.id); + }, [activeFiles, file.id]); const isPinned = actualFile ? isFilePinned(actualFile) : false; const downloadSelectedFile = useCallback(() => { @@ -93,40 +92,6 @@ const FileThumbnail = ({ // ---- Selection ---- const isSelected = selectedFiles.includes(file.id); - // ---- Meta formatting ---- - const prettySize = useMemo(() => { - const bytes = file.size ?? 0; - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; - }, [file.size]); - - const extUpper = useMemo(() => { - const m = /\.([a-z0-9]+)$/i.exec(file.name ?? ''); - return (m?.[1] || '').toUpperCase(); - }, [file.name]); - - const pageLabel = useMemo( - () => - file.pageCount > 0 - ? `${file.pageCount} ${file.pageCount === 1 ? 'Page' : 'Pages'}` - : '', - [file.pageCount] - ); - - const dateLabel = useMemo(() => { - const d = - file.modifiedAt != null ? new Date(file.modifiedAt) : new Date(); // fallback - if (Number.isNaN(d.getTime())) return ''; - return new Intl.DateTimeFormat(undefined, { - month: 'short', - day: '2-digit', - year: 'numeric', - }).format(d); - }, [file.modifiedAt]); - // ---- Drag & drop wiring ---- const fileElementRef = useCallback((element: HTMLDivElement | null) => { if (!element) return; diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 07823414a..2c85d8f81 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -1,13 +1,7 @@ -import React, { useState, useCallback, useRef, useEffect, useMemo } from "react"; -import { - Button, Text, Center, Box, - Notification, TextInput, LoadingOverlay, Modal, Alert, - Stack, Group, Portal -} from "@mantine/core"; -import { useTranslation } from "react-i18next"; -import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext"; -import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor"; -import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing"; +import { useState, useCallback, useRef, useEffect } from "react"; +import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core"; +import { useFileState, useFileActions } from "../../contexts/FileContext"; +import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor"; import { pdfExportService } from "../../services/pdfExportService"; import { documentManipulationService } from "../../services/documentManipulationService"; // Thumbnail generation is now handled by individual PageThumbnail components @@ -19,16 +13,11 @@ import NavigationWarningModal from '../shared/NavigationWarningModal'; import { FileId } from "../../types/file"; import { - DOMCommand, - RotatePageCommand, DeletePagesCommand, ReorderPagesCommand, SplitCommand, BulkRotateCommand, - BulkSplitCommand, - SplitAllCommand, PageBreakCommand, - BulkPageBreakCommand, UndoManager } from './commands/pageCommands'; import { GRID_CONSTANTS } from './constants'; @@ -49,35 +38,24 @@ const PageEditor = ({ // Prefer IDs + selectors to avoid array identity churn const activeFileIds = state.files.ids; - const primaryFileId = activeFileIds[0] ?? null; - const selectedFiles = selectors.getSelectedFiles(); - - // Stable signature for effects (prevents loops) - const filesSignature = selectors.getFilesSignature(); // UI state const globalProcessing = state.ui.isProcessing; - const processingProgress = state.ui.processingProgress; - const hasUnsavedChanges = state.ui.hasUnsavedChanges; // Edit state management const [editedDocument, setEditedDocument] = useState(null); - const [hasUnsavedDraft, setHasUnsavedDraft] = useState(false); - const [showResumeModal, setShowResumeModal] = useState(false); - const [foundDraft, setFoundDraft] = useState(null); - const autoSaveTimer = useRef(null); // DOM-first undo manager (replaces the old React state undo system) const undoManagerRef = useRef(new UndoManager()); // Document state management - const { document: mergedPdfDocument, isVeryLargeDocument, isLoading: documentLoading } = usePageDocument(); + const { document: mergedPdfDocument } = usePageDocument(); // UI state management const { selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading, - setSelectionMode, setSelectedPageIds, setMovingPage, setIsAnimating, setSplitPositions, setExportLoading, + setSelectionMode, setSelectedPageIds, setMovingPage, setSplitPositions, setExportLoading, togglePage, toggleSelectAll, animateReorder } = usePageEditorState(); @@ -146,12 +124,6 @@ const PageEditor = ({ }).filter(id => id !== ''); }, [displayDocument]); - // Convert selectedPageIds to numbers for components that still need numbers - const selectedPageNumbers = useMemo(() => - getPageNumbersFromIds(selectedPageIds), - [selectedPageIds, getPageNumbersFromIds] - ); - // Select all pages by default when document initially loads const hasInitializedSelection = useRef(false); useEffect(() => { diff --git a/frontend/src/components/pageEditor/PageEditorControls.tsx b/frontend/src/components/pageEditor/PageEditorControls.tsx index 2a932ae50..3e36b06b6 100644 --- a/frontend/src/components/pageEditor/PageEditorControls.tsx +++ b/frontend/src/components/pageEditor/PageEditorControls.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Tooltip, ActionIcon, @@ -9,9 +8,7 @@ import ContentCutIcon from "@mui/icons-material/ContentCut"; import RotateLeftIcon from "@mui/icons-material/RotateLeft"; import RotateRightIcon from "@mui/icons-material/RotateRight"; import DeleteIcon from "@mui/icons-material/Delete"; -import CloseIcon from "@mui/icons-material/Close"; import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak"; -import DownloadIcon from "@mui/icons-material/Download"; interface PageEditorControlsProps { // Close/Reset functions @@ -46,7 +43,6 @@ interface PageEditorControlsProps { } const PageEditorControls = ({ - onClosePdf, onUndo, onRedo, canUndo, @@ -54,12 +50,7 @@ const PageEditorControls = ({ onRotate, onDelete, onSplit, - onSplitAll, onPageBreak, - onPageBreakAll, - onExportAll, - exportLoading, - selectionMode, selectedPageIds, displayDocument, splitPositions, diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx index 0b2782ba1..dfa098af1 100644 --- a/frontend/src/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -52,16 +52,13 @@ const PageThumbnail: React.FC = ({ pageRefs, onReorderPages, onTogglePage, - onAnimateReorder, onExecuteCommand, onSetStatus, onSetMovingPage, onDeletePage, createRotateCommand, - createDeleteCommand, createSplitCommand, pdfDocument, - setPdfDocument, splitPositions, onInsertFiles, }: PageThumbnailProps) => { @@ -172,7 +169,7 @@ const PageThumbnail: React.FC = ({ type: 'page', pageNumber: page.pageNumber }), - onDrop: ({ source }) => {} + onDrop: (_) => {} }); (element as any).__dragCleanup = () => { diff --git a/frontend/src/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/components/pageEditor/hooks/usePageDocument.ts index b620c87b8..0b7aa00b4 100644 --- a/frontend/src/components/pageEditor/hooks/usePageDocument.ts +++ b/frontend/src/components/pageEditor/hooks/usePageDocument.ts @@ -27,9 +27,9 @@ export function usePageDocument(): PageDocumentHook { const globalProcessing = state.ui.isProcessing; // Get primary file record outside useMemo to track processedFile changes - const primaryFileRecord = primaryFileId ? selectors.getFileRecord(primaryFileId) : null; - const processedFilePages = primaryFileRecord?.processedFile?.pages; - const processedFileTotalPages = primaryFileRecord?.processedFile?.totalPages; + const primaryStirlingFileStub = primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : null; + const processedFilePages = primaryStirlingFileStub?.processedFile?.pages; + const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages; // Compute merged document with stable signature (prevents infinite loops) const mergedPdfDocument = useMemo((): PDFDocument | null => { @@ -38,16 +38,16 @@ export function usePageDocument(): PageDocumentHook { const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null; // If we have file IDs but no file record, something is wrong - return null to show loading - if (!primaryFileRecord) { + if (!primaryStirlingFileStub) { console.log('🎬 PageEditor: No primary file record found, showing loading'); return null; } const name = activeFileIds.length === 1 - ? (primaryFileRecord.name ?? 'document.pdf') + ? (primaryStirlingFileStub.name ?? 'document.pdf') : activeFileIds - .map(id => (selectors.getFileRecord(id)?.name ?? 'file').replace(/\.pdf$/i, '')) + .map(id => (selectors.getStirlingFileStub(id)?.name ?? 'file').replace(/\.pdf$/i, '')) .join(' + '); // Build page insertion map from files with insertion positions @@ -55,7 +55,7 @@ export function usePageDocument(): PageDocumentHook { const originalFileIds: FileId[] = []; activeFileIds.forEach(fileId => { - const record = selectors.getFileRecord(fileId); + const record = selectors.getStirlingFileStub(fileId); if (record?.insertAfterPageId !== undefined) { if (!insertionMap.has(record.insertAfterPageId)) { insertionMap.set(record.insertAfterPageId, []); @@ -68,16 +68,15 @@ export function usePageDocument(): PageDocumentHook { // Build pages by interleaving original pages with insertions let pages: PDFPage[] = []; - let totalPageCount = 0; // Helper function to create pages from a file const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => { - const fileRecord = selectors.getFileRecord(fileId); - if (!fileRecord) { + const stirlingFileStub = selectors.getStirlingFileStub(fileId); + if (!stirlingFileStub) { return []; } - const processedFile = fileRecord.processedFile; + const processedFile = stirlingFileStub.processedFile; let filePages: PDFPage[] = []; if (processedFile?.pages && processedFile.pages.length > 0) { @@ -144,8 +143,6 @@ export function usePageDocument(): PageDocumentHook { }); } - totalPageCount = pages.length; - if (pages.length === 0) { return null; } @@ -159,7 +156,7 @@ export function usePageDocument(): PageDocumentHook { }; return mergedDoc; - }, [activeFileIds, primaryFileId, primaryFileRecord, processedFilePages, processedFileTotalPages, selectors, filesSignature]); + }, [activeFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, filesSignature]); // Large document detection for smart loading const isVeryLargeDocument = useMemo(() => { diff --git a/frontend/src/components/shared/FileCard.tsx b/frontend/src/components/shared/FileCard.tsx index 73ae01dba..6c63af42e 100644 --- a/frontend/src/components/shared/FileCard.tsx +++ b/frontend/src/components/shared/FileCard.tsx @@ -6,13 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage"; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditIcon from "@mui/icons-material/Edit"; -import { FileRecord } from "../../types/fileContext"; +import { StirlingFileStub } from "../../types/fileContext"; import { getFileSize, getFileDate } from "../../utils/fileUtils"; import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail"; interface FileCardProps { file: File; - record?: FileRecord; + record?: StirlingFileStub; onRemove: () => void; onDoubleClick?: () => void; onView?: () => void; @@ -25,7 +25,7 @@ interface FileCardProps { const FileCard = ({ file, record, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => { const { t } = useTranslation(); // Use record thumbnail if available, otherwise fall back to IndexedDB lookup - const fileMetadata = record ? { id: record.id, name: file.name, type: file.type, size: file.size, lastModified: file.lastModified } : null; + const fileMetadata = record ? { id: record.id, name: record.name, type: record.type, size: record.size, lastModified: record.lastModified } : null; const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileMetadata); const thumb = record?.thumbnailUrl || indexedDBThumb; const [isHovered, setIsHovered] = useState(false); diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx index 1a43196d6..431c5bded 100644 --- a/frontend/src/components/shared/FileGrid.tsx +++ b/frontend/src/components/shared/FileGrid.tsx @@ -1,18 +1,18 @@ -import React, { useState } from "react"; -import { Box, Flex, Group, Text, Button, TextInput, Select, Badge } from "@mantine/core"; +import { useState } from "react"; +import { Box, Flex, Group, Text, Button, TextInput, Select } from "@mantine/core"; import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import SortIcon from "@mui/icons-material/Sort"; import FileCard from "./FileCard"; -import { FileRecord } from "../../types/fileContext"; +import { StirlingFileStub } from "../../types/fileContext"; import { FileId } from "../../types/file"; interface FileGridProps { - files: Array<{ file: File; record?: FileRecord }>; + files: Array<{ file: File; record?: StirlingFileStub }>; onRemove?: (index: number) => void; - onDoubleClick?: (item: { file: File; record?: FileRecord }) => void; - onView?: (item: { file: File; record?: FileRecord }) => void; - onEdit?: (item: { file: File; record?: FileRecord }) => void; + onDoubleClick?: (item: { file: File; record?: StirlingFileStub }) => void; + onView?: (item: { file: File; record?: StirlingFileStub }) => void; + onEdit?: (item: { file: File; record?: StirlingFileStub }) => void; onSelect?: (fileId: FileId) => void; selectedFiles?: FileId[]; showSearch?: boolean; @@ -123,9 +123,17 @@ const FileGrid = ({ h="30rem" style={{ overflowY: "auto", width: "100%" }} > - {displayFiles.map((item, idx) => { - const fileId = item.record?.id || item.file.name as FileId /* FIX ME: This doesn't seem right */; - const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId); + {displayFiles + .filter(item => { + if (!item.record?.id) { + console.error('FileGrid: File missing StirlingFileStub with proper ID:', item.file.name); + return false; + } + return true; + }) + .map((item, idx) => { + const fileId = item.record!.id; // Safe to assert after filter + const originalIdx = files.findIndex(f => f.record?.id === fileId); const supported = isFileSupported ? isFileSupported(item.file.name) : true; return ( (null); const [rippleEffect, setRippleEffect] = useState<{x: number, y: number, key: number} | null>(null); @@ -36,7 +35,6 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal } // Start transition animation - setIsChanging(true); setPendingLanguage(value); // Simulate processing time for smooth transition @@ -44,7 +42,6 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal i18n.changeLanguage(value); setTimeout(() => { - setIsChanging(false); setPendingLanguage(null); setOpened(false); @@ -54,7 +51,7 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal }, 200); }; - const currentLanguage = supportedLanguages[i18n.language as keyof typeof supportedLanguages] || + const currentLanguage = supportedLanguages[i18n.language as keyof typeof supportedLanguages] || supportedLanguages['en-GB']; // Trigger animation when dropdown opens @@ -77,8 +74,8 @@ const LanguageSelector = ({ position = 'bottom-start', offset = 8, compact = fal } `} - = ({ icon, ...props }) => { // Convert our icon naming convention to the local collection format - const iconName = icon.startsWith('material-symbols:') - ? icon + const iconName = icon.startsWith('material-symbols:') + ? icon : `material-symbols:${icon}`; - + // Development logging (only in dev mode) if (process.env.NODE_ENV === 'development') { const logKey = `icon-${iconName}`; @@ -44,9 +44,9 @@ export const LocalIcon: React.FC = ({ icon, ...props }) => { sessionStorage.setItem(logKey, 'logged'); } } - + // Always render the icon - Iconify will use local if available, CDN if not return ; }; -export default LocalIcon; \ No newline at end of file +export default LocalIcon; diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index 644712d7c..2ebe45002 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -3,7 +3,6 @@ import { ActionIcon, Stack, Divider } from "@mantine/core"; import { useTranslation } from 'react-i18next'; import LocalIcon from './LocalIcon'; import { useRainbowThemeContext } from "./RainbowThemeProvider"; -import AppConfigModal from './AppConfigModal'; import { useIsOverflowing } from '../../hooks/useIsOverflowing'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; @@ -19,8 +18,7 @@ import { getActiveNavButton, } from './quickAccessBar/QuickAccessBar'; -const QuickAccessBar = forwardRef(({ -}, ref) => { +const QuickAccessBar = forwardRef((_, ref) => { const { t } = useTranslation(); const { isRainbowMode } = useRainbowThemeContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index b141ec493..ee0f9b911 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -29,12 +29,11 @@ export default function RightRail() { // File state and selection const { state, selectors } = useFileState(); - const { selectedFiles, selectedFileIds, selectedPageNumbers, setSelectedFiles, setSelectedPages } = useFileSelection(); + const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection(); const { removeFiles } = useFileManagement(); const activeFiles = selectors.getFiles(); const filesSignature = selectors.getFilesSignature(); - const fileRecords = selectors.getFileRecords(); // Compute selection state and total items const getSelectionState = useCallback(() => { @@ -85,7 +84,7 @@ export default function RightRail() { if (currentView === 'fileEditor' || currentView === 'viewer') { // Download selected files (or all if none selected) const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles; - + filesToDownload.forEach(file => { const link = document.createElement('a'); link.href = URL.createObjectURL(file); @@ -206,8 +205,8 @@ export default function RightRail() { )} {/* Group: Selection controls + Close, animate as one unit when entering/leaving viewer */} -
@@ -358,14 +357,14 @@ export default function RightRail() { 0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All')) } position="left" offset={12} arrow>
- { cancelAnimationFrame(raf); - try { ro.disconnect(); } catch {} - try { mo.disconnect(); } catch {} + try { ro.disconnect(); } catch { /* Ignore errors */ } + try { mo.disconnect(); } catch { /* Ignore errors */ } }; } diff --git a/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx b/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx index 60c915593..0146d86e0 100644 --- a/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx +++ b/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx @@ -1,10 +1,10 @@ /** * ActiveToolButton - Shows the currently selected tool at the top of the Quick Access Bar - * + * * When a user selects a tool from the All Tools list, this component displays the tool's * icon and name at the top of the navigation bar. It provides a quick way to see which * tool is currently active and offers a back button to return to the All Tools list. - * + * * Features: * - Shows tool icon and name when a tool is selected * - Hover to reveal back arrow for returning to All Tools @@ -28,7 +28,7 @@ interface ActiveToolButtonProps { const NAV_IDS = ['read', 'sign', 'automate']; -const ActiveToolButton: React.FC = ({ activeButton, setActiveButton }) => { +const ActiveToolButton: React.FC = ({ setActiveButton }) => { const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow(); const { getHomeNavigation } = useSidebarNavigation(); @@ -41,7 +41,6 @@ const ActiveToolButton: React.FC = ({ activeButton, setAc const [indicatorTool, setIndicatorTool] = useState(null); const [indicatorVisible, setIndicatorVisible] = useState(false); const [replayAnim, setReplayAnim] = useState(false); - const [isAnimating, setIsAnimating] = useState(false); const [isBackHover, setIsBackHover] = useState(false); const prevKeyRef = useRef(null); const collapseTimeoutRef = useRef(null); @@ -74,11 +73,9 @@ const ActiveToolButton: React.FC = ({ activeButton, setAc replayRafRef.current = requestAnimationFrame(() => { setReplayAnim(true); }); - setIsAnimating(true); prevKeyRef.current = (selectedToolKey as string) || null; animTimeoutRef.current = window.setTimeout(() => { setReplayAnim(false); - setIsAnimating(false); animTimeoutRef.current = null; }, 500); } @@ -87,10 +84,8 @@ const ActiveToolButton: React.FC = ({ activeButton, setAc clearTimers(); setIndicatorTool(selectedTool); setIndicatorVisible(true); - setIsAnimating(true); prevKeyRef.current = (selectedToolKey as string) || null; animTimeoutRef.current = window.setTimeout(() => { - setIsAnimating(false); animTimeoutRef.current = null; }, 500); } @@ -98,11 +93,9 @@ const ActiveToolButton: React.FC = ({ activeButton, setAc const triggerCollapse = () => { clearTimers(); setIndicatorVisible(false); - setIsAnimating(true); collapseTimeoutRef.current = window.setTimeout(() => { setIndicatorTool(null); prevKeyRef.current = null; - setIsAnimating(false); collapseTimeoutRef.current = null; }, 500); // match CSS transition duration } diff --git a/frontend/src/components/tools/SearchResults.tsx b/frontend/src/components/tools/SearchResults.tsx index ee4a8fe5c..dc9fd6af0 100644 --- a/frontend/src/components/tools/SearchResults.tsx +++ b/frontend/src/components/tools/SearchResults.tsx @@ -1,5 +1,5 @@ -import React, { useMemo } from 'react'; -import { Box, Stack, Text } from '@mantine/core'; +import React from 'react'; +import { Box, Stack } from '@mantine/core'; import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy'; import ToolButton from './toolPicker/ToolButton'; import { useTranslation } from 'react-i18next'; @@ -40,12 +40,10 @@ const SearchResults: React.FC = ({ filteredTools, onSelect } ))} - {/* global spacer to allow scrolling past last row in search mode */} + {/* Global spacer to allow scrolling past last row in search mode */}
); }; export default SearchResults; - - diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index de95f0b94..98d1d96f3 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -1,5 +1,3 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import ToolPicker from './ToolPicker'; @@ -8,12 +6,11 @@ import ToolRenderer from './ToolRenderer'; import ToolSearch from './toolPicker/ToolSearch'; import { useSidebarContext } from "../../contexts/SidebarContext"; import rainbowStyles from '../../styles/rainbow.module.css'; -import { Stack, ScrollArea } from '@mantine/core'; +import { ScrollArea } from '@mantine/core'; // No props needed - component uses context export default function ToolPanel() { - const { t } = useTranslation(); const { isRainbowMode } = useRainbowThemeContext(); const { sidebarRefs } = useSidebarContext(); const { toolPanelRef } = sidebarRefs; @@ -27,7 +24,6 @@ export default function ToolPanel() { filteredTools, toolRegistry, setSearchQuery, - handleBackToTools } = useToolWorkflow(); const { selectedToolKey, handleToolSelect } = useToolWorkflow(); diff --git a/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx b/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx index 92373f60d..b483eca74 100644 --- a/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx +++ b/frontend/src/components/tools/addPassword/AddPasswordSettings.test.tsx @@ -3,7 +3,6 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { MantineProvider } from '@mantine/core'; import AddPasswordSettings from './AddPasswordSettings'; import { defaultParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters'; -import type { AddPasswordParameters } from '../../../hooks/tools/addPassword/useAddPasswordParameters'; // Mock useTranslation with predictable return values const mockT = vi.fn((key: string) => `mock-${key}`); diff --git a/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx b/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx index 96ce6aad5..36ef3ce01 100644 --- a/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx +++ b/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import { Stack, Text, PasswordInput, Select } from "@mantine/core"; +import { Stack, PasswordInput, Select } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPasswordParameters"; diff --git a/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx index 84bbb296a..04949c27c 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import { Button, Stack, Text } from "@mantine/core"; +import { Button, Stack } from "@mantine/core"; import { useTranslation } from "react-i18next"; interface WatermarkTypeSettingsProps { diff --git a/frontend/src/components/tools/addWatermark/WatermarkWording.tsx b/frontend/src/components/tools/addWatermark/WatermarkWording.tsx index 5278ca332..cee909fca 100644 --- a/frontend/src/components/tools/addWatermark/WatermarkWording.tsx +++ b/frontend/src/components/tools/addWatermark/WatermarkWording.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Stack, Text, TextInput } from "@mantine/core"; +import { Stack, TextInput } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters"; import { removeEmojis } from "../../../utils/textUtils"; diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index 5dfdc0468..b5e793ccb 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, @@ -38,10 +38,8 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o automationIcon, setAutomationIcon, selectedTools, - addTool, removeTool, updateTool, - hasUnsavedChanges, canSaveAutomation, getToolName, getToolDefaultParameters @@ -84,14 +82,6 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o updateTool(selectedTools.length, newTool); }; - const handleBackClick = () => { - if (hasUnsavedChanges()) { - setUnsavedWarningOpen(true); - } else { - onBack(); - } - }; - const handleConfirmBack = () => { setUnsavedWarningOpen(false); onBack(); diff --git a/frontend/src/components/tools/automate/AutomationEntry.tsx b/frontend/src/components/tools/automate/AutomationEntry.tsx index 7a33bd65e..30c280491 100644 --- a/frontend/src/components/tools/automate/AutomationEntry.tsx +++ b/frontend/src/components/tools/automate/AutomationEntry.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Group, Text, ActionIcon, Menu, Box } from '@mantine/core'; +import { Group, Text, ActionIcon, Menu, Box } from '@mantine/core'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -69,11 +69,11 @@ export default function AutomationEntry({ const toolChain = operations.map((op, index) => ( - 0; - + return shouldShowTooltip ? ( - {boxContent} diff --git a/frontend/src/components/tools/automate/AutomationRun.tsx b/frontend/src/components/tools/automate/AutomationRun.tsx index 1a3cb1263..2d6bda6fc 100644 --- a/frontend/src/components/tools/automate/AutomationRun.tsx +++ b/frontend/src/components/tools/automate/AutomationRun.tsx @@ -20,11 +20,11 @@ export default function AutomationRun({ automation, onComplete, automateOperatio const { selectedFiles } = useFileSelection(); const toolRegistry = useFlatToolRegistry(); const cleanup = useResourceCleanup(); - + // Progress tracking state const [executionSteps, setExecutionSteps] = useState([]); const [currentStepIndex, setCurrentStepIndex] = useState(-1); - + // Use the operation hook's loading state const isExecuting = automateOperation?.isLoading || false; const hasResults = automateOperation?.files.length > 0 || automateOperation?.downloadUrl !== null; @@ -74,15 +74,15 @@ export default function AutomationRun({ automation, onComplete, automateOperatio try { // Use the automateOperation.executeOperation to handle file consumption properly await automateOperation.executeOperation( - { + { automationConfig: automation, - onStepStart: (stepIndex: number, operationName: string) => { + onStepStart: (stepIndex: number, _operationName: string) => { setCurrentStepIndex(stepIndex); setExecutionSteps(prev => prev.map((step, idx) => idx === stepIndex ? { ...step, status: EXECUTION_STATUS.RUNNING } : step )); }, - onStepComplete: (stepIndex: number, resultFiles: File[]) => { + onStepComplete: (stepIndex: number, _resultFiles: File[]) => { setExecutionSteps(prev => prev.map((step, idx) => idx === stepIndex ? { ...step, status: EXECUTION_STATUS.COMPLETED } : step )); @@ -95,7 +95,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio }, selectedFiles ); - + // Mark all as completed and reset current step setCurrentStepIndex(-1); console.log(`✅ Automation completed successfully`); @@ -118,20 +118,20 @@ export default function AutomationRun({ automation, onComplete, automateOperatio case EXECUTION_STATUS.ERROR: return ; case EXECUTION_STATUS.RUNNING: - return
; default: - return
; } }; @@ -170,8 +170,8 @@ export default function AutomationRun({ automation, onComplete, automateOperatio {getStepIcon(step)}
-
); -} \ No newline at end of file +} diff --git a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx index b2aa1c975..4fce30b3d 100644 --- a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx +++ b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Modal, @@ -32,7 +32,6 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, const { t } = useTranslation(); const [parameters, setParameters] = useState({}); - const [isValid, setIsValid] = useState(true); // Get tool info from registry const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry]; @@ -87,9 +86,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, }; const handleSave = () => { - if (isValid) { - onSave(parameters); - } + onSave(parameters); }; return ( @@ -127,7 +124,6 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, diff --git a/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx b/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx index 1090bd11f..071e27cfd 100644 --- a/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx +++ b/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx @@ -1,4 +1,4 @@ -import { Stack, Text, Checkbox } from "@mantine/core"; +import { Stack, Checkbox } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissions/useChangePermissionsParameters"; diff --git a/frontend/src/components/tools/convert/ConvertSettings.tsx b/frontend/src/components/tools/convert/ConvertSettings.tsx index bae2954a9..3a019f8da 100644 --- a/frontend/src/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertSettings.tsx @@ -22,13 +22,13 @@ import { OUTPUT_OPTIONS, FIT_OPTIONS } from "../../../constants/convertConstants"; -import { FileId } from "../../../types/file"; +import { StirlingFile } from "../../../types/fileContext"; interface ConvertSettingsProps { parameters: ConvertParameters; onParameterChange: (key: keyof ConvertParameters, value: any) => void; getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>; - selectedFiles: File[]; + selectedFiles: StirlingFile[]; disabled?: boolean; } @@ -129,7 +129,7 @@ const ConvertSettings = ({ }; const filterFilesByExtension = (extension: string) => { - const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as File[]; + const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as StirlingFile[]; return files.filter(file => { const fileExtension = detectFileExtension(file.name); @@ -143,21 +143,8 @@ const ConvertSettings = ({ }); }; - const updateFileSelection = (files: File[]) => { - // Map File objects to their actual IDs in FileContext - const fileIds = files.map(file => { - // Find the file ID by matching file properties - const fileRecord = state.files.ids - .map(id => selectors.getFileRecord(id)) - .find(record => - record && - record.name === file.name && - record.size === file.size && - record.lastModified === file.lastModified - ); - return fileRecord?.id; - }).filter((id): id is FileId => id !== undefined); // Type guard to ensure only strings - + const updateFileSelection = (files: StirlingFile[]) => { + const fileIds = files.map(file => file.fileId); setSelectedFiles(fileIds); }; diff --git a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx index e1a662bd2..49e057a1c 100644 --- a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx @@ -3,11 +3,12 @@ import { Stack, Text, Select, Alert } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters'; import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection'; +import { StirlingFile } from '../../../types/fileContext'; interface ConvertToPdfaSettingsProps { parameters: ConvertParameters; onParameterChange: (key: keyof ConvertParameters, value: any) => void; - selectedFiles: File[]; + selectedFiles: StirlingFile[]; disabled?: boolean; } diff --git a/frontend/src/components/tools/ocr/OCRSettings.tsx b/frontend/src/components/tools/ocr/OCRSettings.tsx index b4d9aa766..6009888b9 100644 --- a/frontend/src/components/tools/ocr/OCRSettings.tsx +++ b/frontend/src/components/tools/ocr/OCRSettings.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Stack, Select, Text, Divider } from '@mantine/core'; +import { Stack, Select, Divider } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import LanguagePicker from './LanguagePicker'; import { OCRParameters } from '../../../hooks/tools/ocr/useOCRParameters'; diff --git a/frontend/src/components/tools/removeCertificateSign/RemoveCertificateSignSettings.tsx b/frontend/src/components/tools/removeCertificateSign/RemoveCertificateSignSettings.tsx index f34e3f2e6..cb1f480d3 100644 --- a/frontend/src/components/tools/removeCertificateSign/RemoveCertificateSignSettings.tsx +++ b/frontend/src/components/tools/removeCertificateSign/RemoveCertificateSignSettings.tsx @@ -8,11 +8,7 @@ interface RemoveCertificateSignSettingsProps { disabled?: boolean; } -const RemoveCertificateSignSettings: React.FC = ({ - parameters, - onParameterChange, // Unused - kept for interface consistency and future extensibility - disabled = false -}) => { +const RemoveCertificateSignSettings: React.FC = (_) => { const { t } = useTranslation(); return ( @@ -24,4 +20,4 @@ const RemoveCertificateSignSettings: React.FC = ({ - parameters, - onParameterChange, - disabled = false -}) => { +const RepairSettings: React.FC = (_) => { const { t } = useTranslation(); return ( @@ -24,4 +20,4 @@ const RepairSettings: React.FC = ({ ); }; -export default RepairSettings; \ No newline at end of file +export default RepairSettings; diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx index 9b375fc2f..354613ecb 100644 --- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx +++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Text, Anchor } from "@mantine/core"; import { useTranslation } from "react-i18next"; import FolderIcon from '@mui/icons-material/Folder'; @@ -6,9 +6,10 @@ import UploadIcon from '@mui/icons-material/Upload'; import { useFilesModalContext } from "../../../contexts/FilesModalContext"; import { useAllFiles } from "../../../contexts/FileContext"; import { useFileManager } from "../../../hooks/useFileManager"; +import { StirlingFile } from "../../../types/fileContext"; export interface FileStatusIndicatorProps { - selectedFiles?: File[]; + selectedFiles?: StirlingFile[]; placeholder?: string; } @@ -17,7 +18,7 @@ const FileStatusIndicator = ({ }: FileStatusIndicatorProps) => { const { t } = useTranslation(); const { openFilesModal, onFilesSelect } = useFilesModalContext(); - const { files: workbenchFiles } = useAllFiles(); + const { files: stirlingFileStubs } = useAllFiles(); const { loadRecentFiles } = useFileManager(); const [hasRecentFiles, setHasRecentFiles] = useState(null); @@ -27,7 +28,7 @@ const FileStatusIndicator = ({ try { const recentFiles = await loadRecentFiles(); setHasRecentFiles(recentFiles.length > 0); - } catch (error) { + } catch { setHasRecentFiles(false); } }; @@ -55,7 +56,7 @@ const FileStatusIndicator = ({ } // Check if there are no files in the workbench - if (workbenchFiles.length === 0) { + if (stirlingFileStubs.length === 0) { // If no recent files, show upload button if (!hasRecentFiles) { return ( diff --git a/frontend/src/components/tools/shared/FilesToolStep.tsx b/frontend/src/components/tools/shared/FilesToolStep.tsx index b062e9c02..8c188d4a9 100644 --- a/frontend/src/components/tools/shared/FilesToolStep.tsx +++ b/frontend/src/components/tools/shared/FilesToolStep.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import FileStatusIndicator from './FileStatusIndicator'; +import { StirlingFile } from '../../../types/fileContext'; export interface FilesToolStepProps { - selectedFiles: File[]; + selectedFiles: StirlingFile[]; isCollapsed?: boolean; onCollapsedClick?: () => void; placeholder?: string; diff --git a/frontend/src/components/tools/shared/ReviewToolStep.tsx b/frontend/src/components/tools/shared/ReviewToolStep.tsx index 364077e4f..6dcb6fc6c 100644 --- a/frontend/src/components/tools/shared/ReviewToolStep.tsx +++ b/frontend/src/components/tools/shared/ReviewToolStep.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Button, Group, Stack } from "@mantine/core"; +import React, { useEffect, useRef } from "react"; +import { Button, Stack } from "@mantine/core"; import { useTranslation } from "react-i18next"; import DownloadIcon from "@mui/icons-material/Download"; import UndoIcon from "@mui/icons-material/Undo"; diff --git a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx index 7a67fceca..18cf8a0d5 100644 --- a/frontend/src/components/tools/shared/SuggestedToolsSection.tsx +++ b/frontend/src/components/tools/shared/SuggestedToolsSection.tsx @@ -3,8 +3,6 @@ import { Stack, Text, Divider, Card, Group, Anchor } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useSuggestedTools } from '../../../hooks/useSuggestedTools'; -export interface SuggestedToolsSectionProps {} - export function SuggestedToolsSection(): React.ReactElement { const { t } = useTranslation(); const suggestedTools = useSuggestedTools(); diff --git a/frontend/src/components/tools/shared/ToolStep.tsx b/frontend/src/components/tools/shared/ToolStep.tsx index 4201fd82d..2fa8eb2da 100644 --- a/frontend/src/components/tools/shared/ToolStep.tsx +++ b/frontend/src/components/tools/shared/ToolStep.tsx @@ -1,5 +1,5 @@ -import React, { createContext, useContext, useMemo, useRef } from 'react'; -import { Text, Stack, Box, Flex, Divider } from '@mantine/core'; +import React, { createContext, useContext, useMemo } from 'react'; +import { Text, Stack, Flex, Divider } from '@mantine/core'; import LocalIcon from '../../shared/LocalIcon'; import { Tooltip } from '../../shared/Tooltip'; import { TooltipTip } from '../../../types/tips'; diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index b6a7594c6..84051f426 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -4,9 +4,10 @@ import { createToolSteps, ToolStepProvider } from './ToolStep'; import OperationButton from './OperationButton'; import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation'; import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle'; +import { StirlingFile } from '../../../types/fileContext'; export interface FilesStepConfig { - selectedFiles: File[]; + selectedFiles: StirlingFile[]; isCollapsed?: boolean; placeholder?: string; onCollapsedClick?: () => void; @@ -80,7 +81,7 @@ export function createToolFlow(config: ToolFlowConfig) { })} {/* Middle Steps */} - {config.steps.map((stepConfig, index) => + {config.steps.map((stepConfig) => steps.create(stepConfig.title, { isVisible: stepConfig.isVisible, isCollapsed: stepConfig.isCollapsed, diff --git a/frontend/src/components/tools/shared/renderToolButtons.tsx b/frontend/src/components/tools/shared/renderToolButtons.tsx index a7535e0e0..4d92d4798 100644 --- a/frontend/src/components/tools/shared/renderToolButtons.tsx +++ b/frontend/src/components/tools/shared/renderToolButtons.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import { Box, Stack } from '@mantine/core'; +import { Box } from '@mantine/core'; import ToolButton from '../toolPicker/ToolButton'; import SubcategoryHeader from './SubcategoryHeader'; diff --git a/frontend/src/components/tools/singleLargePage/SingleLargePageSettings.tsx b/frontend/src/components/tools/singleLargePage/SingleLargePageSettings.tsx index 87dfef926..11bf7009f 100644 --- a/frontend/src/components/tools/singleLargePage/SingleLargePageSettings.tsx +++ b/frontend/src/components/tools/singleLargePage/SingleLargePageSettings.tsx @@ -8,11 +8,7 @@ interface SingleLargePageSettingsProps { disabled?: boolean; } -const SingleLargePageSettings: React.FC = ({ - parameters, - onParameterChange, - disabled = false -}) => { +const SingleLargePageSettings: React.FC = (_) => { const { t } = useTranslation(); return ( @@ -24,4 +20,4 @@ const SingleLargePageSettings: React.FC = ({ ); }; -export default SingleLargePageSettings; \ No newline at end of file +export default SingleLargePageSettings; diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx index 53a01cb77..d4350044e 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -126,7 +126,7 @@ const ToolSearch = ({ key={id} variant="subtle" onClick={() => { - onToolSelect && onToolSelect(id); + onToolSelect?.(id); setDropdownOpen(false); }} leftSection={
{tool.icon}
} diff --git a/frontend/src/components/tools/unlockPdfForms/UnlockPdfFormsSettings.tsx b/frontend/src/components/tools/unlockPdfForms/UnlockPdfFormsSettings.tsx index cc8697d7a..d7ca66f25 100644 --- a/frontend/src/components/tools/unlockPdfForms/UnlockPdfFormsSettings.tsx +++ b/frontend/src/components/tools/unlockPdfForms/UnlockPdfFormsSettings.tsx @@ -8,11 +8,7 @@ interface UnlockPdfFormsSettingsProps { disabled?: boolean; } -const UnlockPdfFormsSettings: React.FC = ({ - parameters, - onParameterChange, // Unused - kept for interface consistency and future extensibility - disabled = false -}) => { +const UnlockPdfFormsSettings: React.FC = (_) => { const { t } = useTranslation(); return ( @@ -24,4 +20,4 @@ const UnlockPdfFormsSettings: React.FC = ({ ); }; -export default UnlockPdfFormsSettings; \ No newline at end of file +export default UnlockPdfFormsSettings; diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx index 0932e995b..dfcd5dc7d 100644 --- a/frontend/src/components/viewer/Viewer.tsx +++ b/frontend/src/components/viewer/Viewer.tsx @@ -1,20 +1,19 @@ import React, { useEffect, useState, useRef, useCallback } from "react"; -import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group, NumberInput, useMantineTheme, ActionIcon, Box, Tabs } from "@mantine/core"; +import { Paper, Stack, Text, ScrollArea, Center, Button, Group, NumberInput, useMantineTheme, ActionIcon, Box, Tabs } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { pdfWorkerManager } from "../../services/pdfWorkerManager"; import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import FirstPageIcon from "@mui/icons-material/FirstPage"; import LastPageIcon from "@mui/icons-material/LastPage"; -import ViewSidebarIcon from "@mui/icons-material/ViewSidebar"; import ViewWeekIcon from "@mui/icons-material/ViewWeek"; // for dual page (book) import DescriptionIcon from "@mui/icons-material/Description"; // for single page import CloseIcon from "@mui/icons-material/Close"; -import { useLocalStorage } from "@mantine/hooks"; import { fileStorage } from "../../services/fileStorage"; import SkeletonLoader from '../shared/SkeletonLoader'; -import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext"; +import { useFileState } from "../../contexts/FileContext"; import { useFileWithUrl } from "../../hooks/useFileWithUrl"; +import { isFileObject } from "../../types/fileContext"; import { FileId } from "../../types/file"; @@ -141,8 +140,6 @@ export interface ViewerProps { } const Viewer = ({ - sidebarsVisible, - setSidebarsVisible, onClose, previewFile, }: ViewerProps) => { @@ -151,13 +148,7 @@ const Viewer = ({ // Get current file from FileContext const { selectors } = useFileState(); - const { actions } = useFileActions(); - const currentFile = useCurrentFile(); - const getCurrentFile = () => currentFile.file; - const getCurrentProcessedFile = () => currentFile.record?.processedFile || undefined; - const clearAllFiles = actions.clearAllFiles; - const addFiles = actions.addFiles; const activeFiles = selectors.getFiles(); // Tab management for multiple files @@ -201,7 +192,7 @@ const Viewer = ({ const effectiveFile = React.useMemo(() => { if (previewFile) { // Validate the preview file - if (!(previewFile instanceof File)) { + if (!isFileObject(previewFile)) { return null; } @@ -405,7 +396,7 @@ const Viewer = ({ // Start progressive preloading after a short delay setTimeout(() => startProgressivePreload(), 100); } - } catch (error) { + } catch { if (!cancelled) { setPageImages([]); setNumPages(0); diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index 39faa0643..3c75b2080 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -19,7 +19,10 @@ import { FileContextStateValue, FileContextActionsValue, FileContextActions, - FileRecord + FileId, + StirlingFileStub, + StirlingFile, + createStirlingFile } from '../types/fileContext'; // Import modular components @@ -29,7 +32,6 @@ import { AddedFile, addFiles, consumeFiles, undoConsumeFiles, createFileActions import { FileLifecycleManager } from './file/lifecycle'; import { FileStateContext, FileActionsContext } from './file/contexts'; import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext'; -import { FileId } from '../types/file'; const DEBUG = process.env.NODE_ENV === 'development'; @@ -37,7 +39,6 @@ const DEBUG = process.env.NODE_ENV === 'development'; // Inner provider component that has access to IndexedDB function FileContextInner({ children, - enableUrlSync = true, enablePersistence = true }: FileContextProviderProps) { const [state, dispatch] = useReducer(fileContextReducer, initialFileContextState); @@ -79,7 +80,7 @@ function FileContextInner({ } // File operations using unified addFiles helper with persistence - const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => { + const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => { const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager); // Auto-select the newly added files if requested @@ -98,15 +99,15 @@ function FileContextInner({ })); } - return addedFilesWithIds.map(({ file }) => file); + return addedFilesWithIds.map(({ file, id }) => createStirlingFile(file, id)); }, [indexedDB, enablePersistence]); - const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => { + const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => { const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager); - return result.map(({ file }) => file); + return result.map(({ file, id }) => createStirlingFile(file, id)); }, []); - const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise => { + const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise => { const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager); // Auto-select the newly added files if requested @@ -114,7 +115,7 @@ function FileContextInner({ selectFiles(result); } - return result.map(({ file }) => file); + return result.map(({ file, id }) => createStirlingFile(file, id)); }, []); // Action creators @@ -122,42 +123,21 @@ function FileContextInner({ // Helper functions for pinned files const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise => { - return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch, indexedDB); + return consumeFiles(inputFileIds, outputFiles, filesRef, dispatch, indexedDB); }, [indexedDB]); - const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]): Promise => { - return undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds, stateRef, filesRef, dispatch, indexedDB); + const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise => { + return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, filesRef, dispatch, indexedDB); }, [indexedDB]); - // Helper to find FileId from File object - const findFileId = useCallback((file: File): FileId | undefined => { - return (Object.keys(stateRef.current.files.byId) as FileId[]).find(id => { - const storedFile = filesRef.current.get(id); - return storedFile && - storedFile.name === file.name && - storedFile.size === file.size && - storedFile.lastModified === file.lastModified; - }); - }, []); + // File pinning functions - use StirlingFile directly + const pinFileWrapper = useCallback((file: StirlingFile) => { + baseActions.pinFile(file.fileId); + }, [baseActions]); - // File-to-ID wrapper functions for pinning - const pinFileWrapper = useCallback((file: File) => { - const fileId = findFileId(file); - if (fileId) { - baseActions.pinFile(fileId); - } else { - console.warn('File not found for pinning:', file.name); - } - }, [baseActions, findFileId]); - - const unpinFileWrapper = useCallback((file: File) => { - const fileId = findFileId(file); - if (fileId) { - baseActions.unpinFile(fileId); - } else { - console.warn('File not found for unpinning:', file.name); - } - }, [baseActions, findFileId]); + const unpinFileWrapper = useCallback((file: StirlingFile) => { + baseActions.unpinFile(file.fileId); + }, [baseActions]); // Complete actions object const actions = useMemo(() => ({ @@ -178,8 +158,8 @@ function FileContextInner({ } } }, - updateFileRecord: (fileId: FileId, updates: Partial) => - lifecycleManager.updateFileRecord(fileId, updates, stateRef), + updateStirlingFileStub: (fileId: FileId, updates: Partial) => + lifecycleManager.updateStirlingFileStub(fileId, updates, stateRef), reorderFiles: (orderedFileIds: FileId[]) => { dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } }); }, @@ -303,7 +283,7 @@ export { useFileSelection, useFileManagement, useFileUI, - useFileRecord, + useStirlingFileStub, useAllFiles, useSelectedFiles, // Primary API hooks for tools diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index abbdfcfbd..5a609e63e 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react'; import { FileMetadata } from '../types/file'; -import { StoredFile, fileStorage } from '../services/fileStorage'; +import { fileStorage } from '../services/fileStorage'; import { downloadFiles } from '../utils/downloadUtils'; import { FileId } from '../types/file'; diff --git a/frontend/src/contexts/IndexedDBContext.tsx b/frontend/src/contexts/IndexedDBContext.tsx index dfd2ac5f2..b6a0b6797 100644 --- a/frontend/src/contexts/IndexedDBContext.tsx +++ b/frontend/src/contexts/IndexedDBContext.tsx @@ -6,7 +6,7 @@ import React, { createContext, useContext, useCallback, useRef } from 'react'; const DEBUG = process.env.NODE_ENV === 'development'; -import { fileStorage, StoredFile } from '../services/fileStorage'; +import { fileStorage } from '../services/fileStorage'; import { FileId } from '../types/file'; import { FileMetadata } from '../types/file'; import { generateThumbnailForFile } from '../utils/thumbnailUtils'; @@ -61,7 +61,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) { const thumbnail = existingThumbnail || await generateThumbnailForFile(file); // Store in IndexedDB - const storedFile = await fileStorage.storeFile(file, fileId, thumbnail); + await fileStorage.storeFile(file, fileId, thumbnail); // Cache the file object for immediate reuse fileCache.current.set(fileId, { file, lastAccessed: Date.now() }); diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx index 3b65f01d1..f9e8ba9a5 100644 --- a/frontend/src/contexts/NavigationContext.tsx +++ b/frontend/src/contexts/NavigationContext.tsx @@ -103,7 +103,7 @@ const NavigationActionsContext = createContext = ({ children, enableUrlSync = true }) => { +}> = ({ children }) => { const [state, dispatch] = useReducer(navigationReducer, initialState); const toolRegistry = useFlatToolRegistry(); diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index b16261bc9..4473dc020 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -89,6 +89,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { clearToolSelection: () => void; // Tool Reset Actions + toolResetFunctions: Record void>; registerToolReset: (toolId: string, resetFunction: () => void) => void; resetTool: (toolId: string) => void; @@ -258,6 +259,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { clearToolSelection: () => actions.setSelectedTool(null), // Tool Reset Actions + toolResetFunctions, registerToolReset, resetTool, diff --git a/frontend/src/contexts/file/FileReducer.ts b/frontend/src/contexts/file/FileReducer.ts index 93c06c4d2..4c4196764 100644 --- a/frontend/src/contexts/file/FileReducer.ts +++ b/frontend/src/contexts/file/FileReducer.ts @@ -6,7 +6,7 @@ import { FileId } from '../../types/file'; import { FileContextState, FileContextAction, - FileRecord + StirlingFileStub } from '../../types/fileContext'; // Initial state @@ -29,7 +29,7 @@ export const initialFileContextState: FileContextState = { function processFileSwap( state: FileContextState, filesToRemove: FileId[], - filesToAdd: FileRecord[] + filesToAdd: StirlingFileStub[] ): FileContextState { // Only remove unpinned files const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id)); @@ -70,11 +70,11 @@ function processFileSwap( export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState { switch (action.type) { case 'ADD_FILES': { - const { fileRecords } = action.payload; + const { stirlingFileStubs } = action.payload; const newIds: FileId[] = []; - const newById: Record = { ...state.files.byId }; + const newById: Record = { ...state.files.byId }; - fileRecords.forEach(record => { + stirlingFileStubs.forEach(record => { // Only add if not already present (dedupe by stable ID) if (!newById[record.id]) { newIds.push(record.id); @@ -233,13 +233,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA } case 'CONSUME_FILES': { - const { inputFileIds, outputFileRecords } = action.payload; - return processFileSwap(state, inputFileIds, outputFileRecords); + const { inputFileIds, outputStirlingFileStubs } = action.payload; + return processFileSwap(state, inputFileIds, outputStirlingFileStubs); } case 'UNDO_CONSUME_FILES': { - const { inputFileRecords, outputFileIds } = action.payload; - return processFileSwap(state, outputFileIds, inputFileRecords); + const { inputStirlingFileStubs, outputFileIds } = action.payload; + return processFileSwap(state, outputFileIds, inputStirlingFileStubs); } case 'RESET_CONTEXT': { diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index e55108553..552ce0b1f 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -3,18 +3,17 @@ */ import { - FileRecord, + StirlingFileStub, FileContextAction, FileContextState, - toFileRecord, + toStirlingFileStub, createFileId, createQuickKey } from '../../types/fileContext'; import { FileId, FileMetadata } from '../../types/file'; import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils'; import { FileLifecycleManager } from './lifecycle'; -import { fileProcessingService } from '../../services/fileProcessingService'; -import { buildQuickKeySet, buildQuickKeySetFromMetadata } from './fileSelectors'; +import { buildQuickKeySet } from './fileSelectors'; const DEBUG = process.env.NODE_ENV === 'development'; @@ -109,8 +108,8 @@ export async function addFiles( await addFilesMutex.lock(); try { - const fileRecords: FileRecord[] = []; - const addedFiles: AddedFile[] = []; + const stirlingFileStubs: StirlingFileStub[] = []; + const addedFiles: AddedFile[] = []; // Build quickKey lookup from existing files for deduplication const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId); @@ -163,7 +162,7 @@ export async function addFiles( } // Create record with immediate thumbnail and page metadata - const record = toFileRecord(file, fileId); + const record = toStirlingFileStub(file, fileId); if (thumbnail) { record.thumbnailUrl = thumbnail; // Track blob URLs for cleanup (images return blob URLs that need revocation) @@ -184,7 +183,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - fileRecords.push(record); + stirlingFileStubs.push(record); addedFiles.push({ file, id: fileId, thumbnail }); } break; @@ -205,7 +204,7 @@ export async function addFiles( const fileId = createFileId(); filesRef.current.set(fileId, file); - const record = toFileRecord(file, fileId); + const record = toStirlingFileStub(file, fileId); if (thumbnail) { record.thumbnailUrl = thumbnail; // Track blob URLs for cleanup (images return blob URLs that need revocation) @@ -226,7 +225,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - fileRecords.push(record); + stirlingFileStubs.push(record); addedFiles.push({ file, id: fileId, thumbnail }); } break; @@ -254,7 +253,7 @@ export async function addFiles( filesRef.current.set(fileId, file); - const record = toFileRecord(file, fileId); + const record = toStirlingFileStub(file, fileId); // Generate processedFile metadata for stored files let pageCount: number = 1; @@ -301,7 +300,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - fileRecords.push(record); + stirlingFileStubs.push(record); addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail }); } @@ -310,9 +309,9 @@ export async function addFiles( } // Dispatch ADD_FILES action if we have new files - if (fileRecords.length > 0) { - dispatch({ type: 'ADD_FILES', payload: { fileRecords } }); - if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${fileRecords.length} files`); + if (stirlingFileStubs.length > 0) { + dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs } }); + if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${stirlingFileStubs.length} files`); } return addedFiles; @@ -328,7 +327,7 @@ export async function addFiles( async function processFilesIntoRecords( files: File[], filesRef: React.MutableRefObject> -): Promise> { +): Promise> { return Promise.all( files.map(async (file) => { const fileId = createFileId(); @@ -347,7 +346,7 @@ async function processFilesIntoRecords( if (DEBUG) console.warn(`📄 Failed to generate thumbnail for file ${file.name}:`, error); } - const record = toFileRecord(file, fileId); + const record = toStirlingFileStub(file, fileId); if (thumbnail) { record.thumbnailUrl = thumbnail; } @@ -365,10 +364,10 @@ async function processFilesIntoRecords( * Helper function to persist files to IndexedDB */ async function persistFilesToIndexedDB( - fileRecords: Array<{ file: File; fileId: FileId; thumbnail?: string }>, + stirlingFileStubs: Array<{ file: File; fileId: FileId; thumbnail?: string }>, indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise } ): Promise { - await Promise.all(fileRecords.map(async ({ file, fileId, thumbnail }) => { + await Promise.all(stirlingFileStubs.map(async ({ file, fileId, thumbnail }) => { try { await indexedDB.saveFile(file, fileId, thumbnail); } catch (error) { @@ -383,7 +382,6 @@ async function persistFilesToIndexedDB( export async function consumeFiles( inputFileIds: FileId[], outputFiles: File[], - stateRef: React.MutableRefObject, filesRef: React.MutableRefObject>, dispatch: React.Dispatch, indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise } | null @@ -391,11 +389,11 @@ export async function consumeFiles( if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`); // Process output files with thumbnails and metadata - const outputFileRecords = await processFilesIntoRecords(outputFiles, filesRef); + const outputStirlingFileStubs = await processFilesIntoRecords(outputFiles, filesRef); // Persist output files to IndexedDB if available if (indexedDB) { - await persistFilesToIndexedDB(outputFileRecords, indexedDB); + await persistFilesToIndexedDB(outputStirlingFileStubs, indexedDB); } // Dispatch the consume action @@ -403,21 +401,20 @@ export async function consumeFiles( type: 'CONSUME_FILES', payload: { inputFileIds, - outputFileRecords: outputFileRecords.map(({ record }) => record) + outputStirlingFileStubs: outputStirlingFileStubs.map(({ record }) => record) } }); - if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputFileRecords.length} outputs`); - + if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputStirlingFileStubs.length} outputs`); // Return the output file IDs for undo tracking - return outputFileRecords.map(({ fileId }) => fileId); + return outputStirlingFileStubs.map(({ fileId }) => fileId); } /** * Helper function to restore files to filesRef and manage IndexedDB cleanup */ async function restoreFilesAndCleanup( - filesToRestore: Array<{ file: File; record: FileRecord }>, + filesToRestore: Array<{ file: File; record: StirlingFileStub }>, fileIdsToRemove: FileId[], filesRef: React.MutableRefObject>, indexedDB?: { deleteFile: (fileId: FileId) => Promise } | null @@ -440,7 +437,7 @@ async function restoreFilesAndCleanup( if (DEBUG) console.warn(`📄 Skipping empty file ${file.name}`); return; } - + // Restore the file to filesRef if (DEBUG) console.log(`📄 Restoring file ${file.name} with id ${record.id} to filesRef`); filesRef.current.set(record.id, file); @@ -455,7 +452,7 @@ async function restoreFilesAndCleanup( throw error; // Re-throw to trigger rollback }) ); - + // Execute all IndexedDB operations await Promise.all(indexedDBPromises); } @@ -466,28 +463,27 @@ async function restoreFilesAndCleanup( */ export async function undoConsumeFiles( inputFiles: File[], - inputFileRecords: FileRecord[], + inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[], - stateRef: React.MutableRefObject, filesRef: React.MutableRefObject>, dispatch: React.Dispatch, indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise; deleteFile: (fileId: FileId) => Promise } | null ): Promise { - if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputFileRecords.length} input files, removing ${outputFileIds.length} output files`); + if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputStirlingFileStubs.length} input files, removing ${outputFileIds.length} output files`); // Validate inputs - if (inputFiles.length !== inputFileRecords.length) { - throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputFileRecords.length})`); + if (inputFiles.length !== inputStirlingFileStubs.length) { + throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputStirlingFileStubs.length})`); } // Create a backup of current filesRef state for rollback const backupFilesRef = new Map(filesRef.current); - + try { // Prepare files to restore const filesToRestore = inputFiles.map((file, index) => ({ file, - record: inputFileRecords[index] + record: inputStirlingFileStubs[index] })); // Restore input files and clean up output files @@ -502,13 +498,12 @@ export async function undoConsumeFiles( dispatch({ type: 'UNDO_CONSUME_FILES', payload: { - inputFileRecords, + inputStirlingFileStubs, outputFileIds } }); - if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputFileRecords.length} inputs, removed ${outputFileIds.length} outputs`); - + if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputStirlingFileStubs.length} inputs, removed ${outputFileIds.length} outputs`); } catch (error) { // Rollback filesRef to previous state if (DEBUG) console.error('📄 undoConsumeFiles: Error during undo, rolling back filesRef', error); diff --git a/frontend/src/contexts/file/fileHooks.ts b/frontend/src/contexts/file/fileHooks.ts index e1b8e5cc4..7d7f9b23e 100644 --- a/frontend/src/contexts/file/fileHooks.ts +++ b/frontend/src/contexts/file/fileHooks.ts @@ -9,7 +9,7 @@ import { FileContextStateValue, FileContextActionsValue } from './contexts'; -import { FileRecord } from '../../types/fileContext'; +import { StirlingFileStub, StirlingFile } from '../../types/fileContext'; import { FileId } from '../../types/file'; /** @@ -38,13 +38,13 @@ export function useFileActions(): FileContextActionsValue { /** * Hook for current/primary file (first in list) */ -export function useCurrentFile(): { file?: File; record?: FileRecord } { +export function useCurrentFile(): { file?: File; record?: StirlingFileStub } { const { state, selectors } = useFileState(); const primaryFileId = state.files.ids[0]; return useMemo(() => ({ file: primaryFileId ? selectors.getFile(primaryFileId) : undefined, - record: primaryFileId ? selectors.getFileRecord(primaryFileId) : undefined + record: primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : undefined }), [primaryFileId, selectors]); } @@ -87,7 +87,7 @@ export function useFileManagement() { addFiles: actions.addFiles, removeFiles: actions.removeFiles, clearAllFiles: actions.clearAllFiles, - updateFileRecord: actions.updateFileRecord, + updateStirlingFileStub: actions.updateStirlingFileStub, reorderFiles: actions.reorderFiles }), [actions]); } @@ -111,24 +111,24 @@ export function useFileUI() { /** * Hook for specific file by ID (optimized for individual file access) */ -export function useFileRecord(fileId: FileId): { file?: File; record?: FileRecord } { +export function useStirlingFileStub(fileId: FileId): { file?: File; record?: StirlingFileStub } { const { selectors } = useFileState(); return useMemo(() => ({ file: selectors.getFile(fileId), - record: selectors.getFileRecord(fileId) + record: selectors.getStirlingFileStub(fileId) }), [fileId, selectors]); } /** * Hook for all files (use sparingly - causes re-renders on file list changes) */ -export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } { +export function useAllFiles(): { files: StirlingFile[]; records: StirlingFileStub[]; fileIds: FileId[] } { const { state, selectors } = useFileState(); return useMemo(() => ({ files: selectors.getFiles(), - records: selectors.getFileRecords(), + records: selectors.getStirlingFileStubs(), fileIds: state.files.ids }), [state.files.ids, selectors]); } @@ -136,12 +136,12 @@ export function useAllFiles(): { files: File[]; records: FileRecord[]; fileIds: /** * Hook for selected files (optimized for selection-based UI) */ -export function useSelectedFiles(): { files: File[]; records: FileRecord[]; fileIds: FileId[] } { +export function useSelectedFiles(): { files: StirlingFile[]; records: StirlingFileStub[]; fileIds: FileId[] } { const { state, selectors } = useFileState(); return useMemo(() => ({ files: selectors.getSelectedFiles(), - records: selectors.getSelectedFileRecords(), + records: selectors.getSelectedStirlingFileStubs(), fileIds: state.ui.selectedFileIds }), [state.ui.selectedFileIds, selectors]); } @@ -166,9 +166,9 @@ export function useFileContext() { addFiles: actions.addFiles, consumeFiles: actions.consumeFiles, undoConsumeFiles: actions.undoConsumeFiles, - recordOperation: (fileId: FileId, operation: any) => {}, // Operation tracking not implemented - markOperationApplied: (fileId: FileId, operationId: string) => {}, // Operation tracking not implemented - markOperationFailed: (fileId: FileId, operationId: string, error: string) => {}, // Operation tracking not implemented + recordOperation: (_fileId: FileId, _operation: any) => {}, // Operation tracking not implemented + markOperationApplied: (_fileId: FileId, _operationId: string) => {}, // Operation tracking not implemented + markOperationFailed: (_fileId: FileId, _operationId: string, _error: string) => {}, // Operation tracking not implemented // File ID lookup findFileId: (file: File) => { diff --git a/frontend/src/contexts/file/fileSelectors.ts b/frontend/src/contexts/file/fileSelectors.ts index 2111693cf..a004831cc 100644 --- a/frontend/src/contexts/file/fileSelectors.ts +++ b/frontend/src/contexts/file/fileSelectors.ts @@ -4,9 +4,11 @@ import { FileId } from '../../types/file'; import { - FileRecord, + StirlingFileStub, FileContextState, - FileContextSelectors + FileContextSelectors, + StirlingFile, + createStirlingFile } from '../../types/fileContext'; /** @@ -17,16 +19,24 @@ export function createFileSelectors( filesRef: React.MutableRefObject> ): FileContextSelectors { return { - getFile: (id: FileId) => filesRef.current.get(id), + getFile: (id: FileId) => { + const file = filesRef.current.get(id); + return file ? createStirlingFile(file, id) : undefined; + }, getFiles: (ids?: FileId[]) => { const currentIds = ids || stateRef.current.files.ids; - return currentIds.map(id => filesRef.current.get(id)).filter(Boolean) as File[]; + return currentIds + .map(id => { + const file = filesRef.current.get(id); + return file ? createStirlingFile(file, id) : undefined; + }) + .filter(Boolean) as StirlingFile[]; }, - getFileRecord: (id: FileId) => stateRef.current.files.byId[id], + getStirlingFileStub: (id: FileId) => stateRef.current.files.byId[id], - getFileRecords: (ids?: FileId[]) => { + getStirlingFileStubs: (ids?: FileId[]) => { const currentIds = ids || stateRef.current.files.ids; return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean); }, @@ -35,11 +45,14 @@ export function createFileSelectors( getSelectedFiles: () => { return stateRef.current.ui.selectedFileIds - .map(id => filesRef.current.get(id)) - .filter(Boolean) as File[]; + .map(id => { + const file = filesRef.current.get(id); + return file ? createStirlingFile(file, id) : undefined; + }) + .filter(Boolean) as StirlingFile[]; }, - getSelectedFileRecords: () => { + getSelectedStirlingFileStubs: () => { return stateRef.current.ui.selectedFileIds .map(id => stateRef.current.files.byId[id]) .filter(Boolean); @@ -52,26 +65,21 @@ export function createFileSelectors( getPinnedFiles: () => { return Array.from(stateRef.current.pinnedFiles) - .map(id => filesRef.current.get(id)) - .filter(Boolean) as File[]; + .map(id => { + const file = filesRef.current.get(id); + return file ? createStirlingFile(file, id) : undefined; + }) + .filter(Boolean) as StirlingFile[]; }, - getPinnedFileRecords: () => { + getPinnedStirlingFileStubs: () => { return Array.from(stateRef.current.pinnedFiles) .map(id => stateRef.current.files.byId[id]) .filter(Boolean); }, - isFilePinned: (file: File) => { - // Find FileId by matching File object properties - const fileId = (Object.keys(stateRef.current.files.byId) as FileId[]).find(id => { - const storedFile = filesRef.current.get(id); - return storedFile && - storedFile.name === file.name && - storedFile.size === file.size && - storedFile.lastModified === file.lastModified; - }); - return fileId ? stateRef.current.pinnedFiles.has(fileId) : false; + isFilePinned: (file: StirlingFile) => { + return stateRef.current.pinnedFiles.has(file.fileId); }, // Stable signature for effects - prevents unnecessary re-renders @@ -90,9 +98,9 @@ export function createFileSelectors( /** * Helper for building quickKey sets for deduplication */ -export function buildQuickKeySet(fileRecords: Record): Set { +export function buildQuickKeySet(stirlingFileStubs: Record): Set { const quickKeys = new Set(); - Object.values(fileRecords).forEach(record => { + Object.values(stirlingFileStubs).forEach(record => { if (record.quickKey) { quickKeys.add(record.quickKey); } @@ -119,7 +127,7 @@ export function buildQuickKeySetFromMetadata(metadata: Array<{ name: string; siz export function getPrimaryFile( stateRef: React.MutableRefObject, filesRef: React.MutableRefObject> -): { file?: File; record?: FileRecord } { +): { file?: File; record?: StirlingFileStub } { const primaryFileId = stateRef.current.files.ids[0]; if (!primaryFileId) return {}; diff --git a/frontend/src/contexts/file/lifecycle.ts b/frontend/src/contexts/file/lifecycle.ts index 9986fe585..c65fec127 100644 --- a/frontend/src/contexts/file/lifecycle.ts +++ b/frontend/src/contexts/file/lifecycle.ts @@ -3,7 +3,7 @@ */ import { FileId } from '../../types/file'; -import { FileContextAction, FileRecord, ProcessedFilePage } from '../../types/fileContext'; +import { FileContextAction, StirlingFileStub, ProcessedFilePage } from '../../types/fileContext'; const DEBUG = process.env.NODE_ENV === 'development'; @@ -50,7 +50,7 @@ export class FileLifecycleManager { this.blobUrls.forEach(url => { try { URL.revokeObjectURL(url); - } catch (error) { + } catch { // Ignore revocation errors } }); @@ -134,7 +134,7 @@ export class FileLifecycleManager { if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) { try { URL.revokeObjectURL(record.thumbnailUrl); - } catch (error) { + } catch { // Ignore revocation errors } } @@ -142,18 +142,18 @@ export class FileLifecycleManager { if (record.blobUrl && record.blobUrl.startsWith('blob:')) { try { URL.revokeObjectURL(record.blobUrl); - } catch (error) { + } catch { // Ignore revocation errors } } // Clean up processed file thumbnails if (record.processedFile?.pages) { - record.processedFile.pages.forEach((page: ProcessedFilePage, index: number) => { + record.processedFile.pages.forEach((page: ProcessedFilePage) => { if (page.thumbnail && page.thumbnail.startsWith('blob:')) { try { URL.revokeObjectURL(page.thumbnail); - } catch (error) { + } catch { // Ignore revocation errors } } @@ -166,7 +166,7 @@ export class FileLifecycleManager { /** * Update file record with race condition guards */ - updateFileRecord = (fileId: FileId, updates: Partial, stateRef?: React.MutableRefObject): void => { + updateStirlingFileStub = (fileId: FileId, updates: Partial, stateRef?: React.MutableRefObject): 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}`); diff --git a/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.test.ts b/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.test.ts index 93b55bc26..53f6f7854 100644 --- a/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.test.ts +++ b/frontend/src/hooks/tools/addPassword/useAddPasswordOperation.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, test, vi, beforeEach, MockedFunction } from 'vitest'; +import { describe, expect, test, vi, beforeEach } from 'vitest'; import { renderHook } from '@testing-library/react'; import { useAddPasswordOperation } from './useAddPasswordOperation'; -import type { AddPasswordFullParameters, AddPasswordParameters } from './useAddPasswordParameters'; +import type { AddPasswordFullParameters } from './useAddPasswordParameters'; // Mock the useToolOperation hook vi.mock('../shared/useToolOperation', async () => { diff --git a/frontend/src/hooks/tools/automate/useAutomateOperation.ts b/frontend/src/hooks/tools/automate/useAutomateOperation.ts index 112bafbd2..3e51a615f 100644 --- a/frontend/src/hooks/tools/automate/useAutomateOperation.ts +++ b/frontend/src/hooks/tools/automate/useAutomateOperation.ts @@ -3,7 +3,6 @@ import { useCallback } from 'react'; import { executeAutomationSequence } from '../../../utils/automationExecutor'; import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry'; import { AutomateParameters } from '../../../types/automation'; -import { AUTOMATION_CONSTANTS } from '../../../constants/automation'; export function useAutomateOperation() { const toolRegistry = useFlatToolRegistry(); diff --git a/frontend/src/hooks/tools/automate/useSavedAutomations.ts b/frontend/src/hooks/tools/automate/useSavedAutomations.ts index 410ad3217..8ad7a80b6 100644 --- a/frontend/src/hooks/tools/automate/useSavedAutomations.ts +++ b/frontend/src/hooks/tools/automate/useSavedAutomations.ts @@ -44,9 +44,9 @@ export function useSavedAutomations() { const copyFromSuggested = useCallback(async (suggestedAutomation: SuggestedAutomation) => { try { const { automationStorage } = await import('../../../services/automationStorage'); - + // Map suggested automation icons to MUI icon keys - const getIconKey = (suggestedIcon: {id: string}): string => { + const getIconKey = (_suggestedIcon: {id: string}): string => { // Check the automation ID or name to determine the appropriate icon switch (suggestedAutomation.id) { case 'secure-pdf-ingestion': @@ -60,7 +60,7 @@ export function useSavedAutomations() { return 'SettingsIcon'; // Default fallback } }; - + // Convert suggested automation to saved automation format const savedAutomation = { name: suggestedAutomation.name, @@ -68,7 +68,7 @@ export function useSavedAutomations() { icon: getIconKey(suggestedAutomation.icon), operations: suggestedAutomation.operations }; - + await automationStorage.saveAutomation(savedAutomation); // Refresh the list after saving refreshAutomations(); @@ -91,4 +91,4 @@ export function useSavedAutomations() { deleteAutomation, copyFromSuggested }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts index 7250726f1..970c14375 100644 --- a/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts +++ b/frontend/src/hooks/tools/automate/useSuggestedAutomations.ts @@ -6,7 +6,6 @@ import { SuggestedAutomation } from '../../../types/automation'; // Create icon components const CompressIcon = () => React.createElement(LocalIcon, { icon: 'compress', width: '1.5rem', height: '1.5rem' }); -const TextFieldsIcon = () => React.createElement(LocalIcon, { icon: 'text-fields', width: '1.5rem', height: '1.5rem' }); const SecurityIcon = () => React.createElement(LocalIcon, { icon: 'security', width: '1.5rem', height: '1.5rem' }); const StarIcon = () => React.createElement(LocalIcon, { icon: 'star', width: '1.5rem', height: '1.5rem' }); diff --git a/frontend/src/hooks/tools/compress/useCompressOperation.ts b/frontend/src/hooks/tools/compress/useCompressOperation.ts index 8327dd698..c7080048f 100644 --- a/frontend/src/hooks/tools/compress/useCompressOperation.ts +++ b/frontend/src/hooks/tools/compress/useCompressOperation.ts @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useToolOperation, ToolOperationConfig, ToolType } from '../shared/useToolOperation'; +import { useToolOperation, ToolType } from '../shared/useToolOperation'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { CompressParameters, defaultParameters } from './useCompressParameters'; diff --git a/frontend/src/hooks/tools/convert/useConvertOperation.ts b/frontend/src/hooks/tools/convert/useConvertOperation.ts index 6de20282f..56dcbb204 100644 --- a/frontend/src/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/hooks/tools/convert/useConvertOperation.ts @@ -2,9 +2,8 @@ import { useCallback } from 'react'; import axios from 'axios'; import { useTranslation } from 'react-i18next'; import { ConvertParameters, defaultParameters } from './useConvertParameters'; -import { detectFileExtension } from '../../../utils/fileUtils'; import { createFileFromApiResponse } from '../../../utils/fileResponseUtils'; -import { useToolOperation, ToolOperationConfig, ToolType } from '../shared/useToolOperation'; +import { useToolOperation, ToolType } from '../shared/useToolOperation'; import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils'; // Static function that can be used by both the hook and automation executor diff --git a/frontend/src/hooks/tools/convert/useConvertParameters.ts b/frontend/src/hooks/tools/convert/useConvertParameters.ts index 2a7b7d523..74a1bd3a1 100644 --- a/frontend/src/hooks/tools/convert/useConvertParameters.ts +++ b/frontend/src/hooks/tools/convert/useConvertParameters.ts @@ -2,7 +2,6 @@ import { COLOR_TYPES, OUTPUT_OPTIONS, FIT_OPTIONS, - TO_FORMAT_OPTIONS, CONVERSION_MATRIX, type ColorType, type OutputOption, @@ -127,7 +126,7 @@ export const useConvertParameters = (): ConvertParametersHook => { endpointName: getEndpointName, validateFn: validateParameters, }), []); - + const baseHook = useBaseParameters(config); const getEndpoint = () => { @@ -166,7 +165,7 @@ export const useConvertParameters = (): ConvertParametersHook => { if (prev.isSmartDetection === false && prev.smartDetectionType === 'none') { return prev; // No change needed } - + return { ...prev, isSmartDetection: false, @@ -290,13 +289,13 @@ export const useConvertParameters = (): ConvertParametersHook => { // All files are images - use image-to-pdf conversion baseHook.setParameters(prev => { // Only update if something actually changed - if (prev.isSmartDetection === true && - prev.smartDetectionType === 'images' && - prev.fromExtension === 'image' && + if (prev.isSmartDetection === true && + prev.smartDetectionType === 'images' && + prev.fromExtension === 'image' && prev.toExtension === 'pdf') { return prev; // No change needed } - + return { ...prev, isSmartDetection: true, @@ -309,13 +308,13 @@ export const useConvertParameters = (): ConvertParametersHook => { // All files are web files - use html-to-pdf conversion baseHook.setParameters(prev => { // Only update if something actually changed - if (prev.isSmartDetection === true && - prev.smartDetectionType === 'web' && - prev.fromExtension === 'html' && + if (prev.isSmartDetection === true && + prev.smartDetectionType === 'web' && + prev.fromExtension === 'html' && prev.toExtension === 'pdf') { return prev; // No change needed } - + return { ...prev, isSmartDetection: true, @@ -328,13 +327,13 @@ export const useConvertParameters = (): ConvertParametersHook => { // Mixed non-image types - use file-to-pdf conversion baseHook.setParameters(prev => { // Only update if something actually changed - if (prev.isSmartDetection === true && - prev.smartDetectionType === 'mixed' && - prev.fromExtension === 'any' && + if (prev.isSmartDetection === true && + prev.smartDetectionType === 'mixed' && + prev.fromExtension === 'any' && prev.toExtension === 'pdf') { return prev; // No change needed } - + return { ...prev, isSmartDetection: true, diff --git a/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts b/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts index 32543ec91..e208d4479 100644 --- a/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts +++ b/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts @@ -4,7 +4,7 @@ */ import { describe, test, expect } from 'vitest'; -import { renderHook, act, waitFor } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react'; import { useConvertParameters } from './useConvertParameters'; describe('useConvertParameters - Auto Detection & Smart Conversion', () => { @@ -347,9 +347,9 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => { const malformedFiles: Array<{name: string}> = [ { name: 'valid.pdf' }, - // @ts-ignore - Testing runtime resilience + // @ts-expect-error - Testing runtime resilience { name: null }, - // @ts-ignore + // @ts-expect-error - Testing runtime resilience { name: undefined } ]; diff --git a/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation.ts b/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation.ts index 106150281..91eed974a 100644 --- a/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation.ts +++ b/frontend/src/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation.ts @@ -4,7 +4,7 @@ import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { RemoveCertificateSignParameters, defaultParameters } from './useRemoveCertificateSignParameters'; // Static function that can be used by both the hook and automation executor -export const buildRemoveCertificateSignFormData = (parameters: RemoveCertificateSignParameters, file: File): FormData => { +export const buildRemoveCertificateSignFormData = (_parameters: RemoveCertificateSignParameters, file: File): FormData => { const formData = new FormData(); formData.append("fileInput", file); return formData; diff --git a/frontend/src/hooks/tools/repair/useRepairOperation.ts b/frontend/src/hooks/tools/repair/useRepairOperation.ts index 44fcc9b70..d195ee881 100644 --- a/frontend/src/hooks/tools/repair/useRepairOperation.ts +++ b/frontend/src/hooks/tools/repair/useRepairOperation.ts @@ -4,7 +4,7 @@ import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { RepairParameters, defaultParameters } from './useRepairParameters'; // Static function that can be used by both the hook and automation executor -export const buildRepairFormData = (parameters: RepairParameters, file: File): FormData => { +export const buildRepairFormData = (_parameters: RepairParameters, file: File): FormData => { const formData = new FormData(); formData.append("fileInput", file); return formData; diff --git a/frontend/src/hooks/tools/shared/useBaseTool.ts b/frontend/src/hooks/tools/shared/useBaseTool.ts index 64e9af59e..643e9bca5 100644 --- a/frontend/src/hooks/tools/shared/useBaseTool.ts +++ b/frontend/src/hooks/tools/shared/useBaseTool.ts @@ -4,10 +4,11 @@ import { useEndpointEnabled } from '../../useEndpointConfig'; import { BaseToolProps } from '../../../types/tool'; import { ToolOperationHook } from './useToolOperation'; import { BaseParametersHook } from './useBaseParameters'; +import { StirlingFile } from '../../../types/fileContext'; interface BaseToolReturn { // File management - selectedFiles: File[]; + selectedFiles: StirlingFile[]; // Tool-specific hooks params: BaseParametersHook; diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index d8d35176d..263217e42 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -6,10 +6,8 @@ import { useToolState, type ProcessingProgress } from './useToolState'; import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls'; import { useToolResources } from './useToolResources'; import { extractErrorMessage } from '../../../utils/toolErrorHandler'; -import { createOperation } from '../../../utils/toolOperationTracker'; +import { StirlingFile, extractFiles, FileId, StirlingFileStub } from '../../../types/fileContext'; import { ResponseHandler } from '../../../utils/toolResponseProcessor'; -import { FileId } from '../../../types/file'; -import { FileRecord } from '../../../types/fileContext'; // Re-export for backwards compatibility export type { ProcessingProgress, ResponseHandler }; @@ -104,7 +102,7 @@ export interface ToolOperationHook { progress: ProcessingProgress | null; // Actions - executeOperation: (params: TParams, selectedFiles: File[]) => Promise; + executeOperation: (params: TParams, selectedFiles: StirlingFile[]) => Promise; resetResults: () => void; clearError: () => void; cancelOperation: () => void; @@ -130,7 +128,7 @@ export const useToolOperation = ( config: ToolOperationConfig ): ToolOperationHook => { const { t } = useTranslation(); - const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, undoConsumeFiles, findFileId, actions: fileActions, selectors } = useFileContext(); + const { addFiles, consumeFiles, undoConsumeFiles, selectors } = useFileContext(); // Composed hooks const { state, actions } = useToolState(); @@ -140,13 +138,13 @@ export const useToolOperation = ( // Track last operation for undo functionality const lastOperationRef = useRef<{ inputFiles: File[]; - inputFileRecords: FileRecord[]; + inputStirlingFileStubs: StirlingFileStub[]; outputFileIds: FileId[]; } | null>(null); const executeOperation = useCallback(async ( params: TParams, - selectedFiles: File[] + selectedFiles: StirlingFile[] ): Promise => { // Validation if (selectedFiles.length === 0) { @@ -160,9 +158,6 @@ export const useToolOperation = ( return; } - // Setup operation tracking - const { operation, operationId, fileId } = createOperation(config.operationType, params, selectedFiles); - recordOperation(fileId, operation); // Reset state actions.setLoading(true); @@ -173,8 +168,11 @@ export const useToolOperation = ( try { let processedFiles: File[]; + // Convert StirlingFile to regular File objects for API processing + const validRegularFiles = extractFiles(validFiles); + switch (config.toolType) { - case ToolType.singleFile: + case ToolType.singleFile: { // Individual file processing - separate API call per file const apiCallsConfig: ApiCallsConfig = { endpoint: config.endpoint, @@ -184,17 +182,18 @@ export const useToolOperation = ( }; processedFiles = await processFiles( params, - validFiles, + validRegularFiles, apiCallsConfig, actions.setProgress, actions.setStatus ); break; + } - case ToolType.multiFile: + case ToolType.multiFile: { // Multi-file processing - single API call with all files actions.setStatus('Processing files...'); - const formData = config.buildFormData(params, validFiles); + const formData = config.buildFormData(params, validRegularFiles); const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint; const response = await axios.post(endpoint, formData, { responseType: 'blob' }); @@ -202,11 +201,11 @@ export const useToolOperation = ( // Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs if (config.responseHandler) { // Use custom responseHandler for multi-file (handles ZIP extraction) - processedFiles = await config.responseHandler(response.data, validFiles); + processedFiles = await config.responseHandler(response.data, validRegularFiles); } else if (response.data.type === 'application/pdf' || - (response.headers && response.headers['content-type'] === 'application/pdf')) { + (response.headers && response.headers['content-type'] === 'application/pdf')) { // Single PDF response (e.g. split with merge option) - use original filename - const originalFileName = validFiles[0]?.name || 'document.pdf'; + const originalFileName = validRegularFiles[0]?.name || 'document.pdf'; const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' }); processedFiles = [singleFile]; } else { @@ -219,10 +218,11 @@ export const useToolOperation = ( } } break; + } case ToolType.custom: actions.setStatus('Processing files...'); - processedFiles = await config.customProcessor(params, validFiles); + processedFiles = await config.customProcessor(params, validRegularFiles); break; } @@ -242,46 +242,40 @@ export const useToolOperation = ( // Replace input files with processed files (consumeFiles handles pinning) const inputFileIds: FileId[] = []; - const inputFileRecords: FileRecord[] = []; - + const inputStirlingFileStubs: StirlingFileStub[] = []; + // Build parallel arrays of IDs and records for undo tracking for (const file of validFiles) { - const fileId = findFileId(file); - if (fileId) { - const record = selectors.getFileRecord(fileId); - if (record) { - inputFileIds.push(fileId); - inputFileRecords.push(record); - } else { - console.warn(`No file record found for file: ${file.name}`); - } + const fileId = file.fileId; + const record = selectors.getStirlingFileStub(fileId); + if (record) { + inputFileIds.push(fileId); + inputStirlingFileStubs.push(record); } else { - console.warn(`No file ID found for file: ${file.name}`); + console.warn(`No file stub found for file: ${file.name}`); } } - + const outputFileIds = await consumeFiles(inputFileIds, processedFiles); - + // Store operation data for undo (only store what we need to avoid memory bloat) lastOperationRef.current = { - inputFiles: validFiles, // Keep original File objects for undo - inputFileRecords: inputFileRecords.map(record => ({ ...record })), // Deep copy to avoid reference issues + inputFiles: extractFiles(validFiles), // Convert to File objects for undo + inputStirlingFileStubs: inputStirlingFileStubs.map(record => ({ ...record })), // Deep copy to avoid reference issues outputFileIds }; - markOperationApplied(fileId, operationId); } } catch (error: any) { const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error); actions.setError(errorMessage); actions.setStatus(''); - markOperationFailed(fileId, operationId, errorMessage); } finally { actions.setLoading(false); actions.setProgress(null); } - }, [t, config, actions, recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles, findFileId, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles]); + }, [t, config, actions, addFiles, consumeFiles, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles]); const cancelOperation = useCallback(() => { cancelApiCalls(); @@ -310,10 +304,10 @@ export const useToolOperation = ( return; } - const { inputFiles, inputFileRecords, outputFileIds } = lastOperationRef.current; + const { inputFiles, inputStirlingFileStubs, outputFileIds } = lastOperationRef.current; // Validate that we have data to undo - if (inputFiles.length === 0 || inputFileRecords.length === 0) { + if (inputFiles.length === 0 || inputStirlingFileStubs.length === 0) { actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data')); return; } @@ -325,18 +319,19 @@ export const useToolOperation = ( try { // Undo the consume operation - await undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds); - + await undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds); + + // Clear results and operation tracking resetResults(); lastOperationRef.current = null; - + // Show success message actions.setStatus(t('undoSuccess', 'Operation undone successfully')); - + } catch (error: any) { let errorMessage = extractErrorMessage(error); - + // Provide more specific error messages based on error type if (error.message?.includes('Mismatch between input files')) { errorMessage = t('undoDataMismatch', 'Cannot undo: operation data is corrupted'); @@ -345,9 +340,9 @@ export const useToolOperation = ( } else if (error.name === 'QuotaExceededError') { errorMessage = t('undoQuotaError', 'Cannot undo: insufficient storage space'); } - + actions.setError(`${t('undoFailed', 'Failed to undo operation')}: ${errorMessage}`); - + // Don't clear the operation data if undo failed - user might want to try again } }, [undoConsumeFiles, resetResults, actions, t]); diff --git a/frontend/src/hooks/tools/singleLargePage/useSingleLargePageOperation.ts b/frontend/src/hooks/tools/singleLargePage/useSingleLargePageOperation.ts index ef304fa09..35eaec079 100644 --- a/frontend/src/hooks/tools/singleLargePage/useSingleLargePageOperation.ts +++ b/frontend/src/hooks/tools/singleLargePage/useSingleLargePageOperation.ts @@ -4,7 +4,7 @@ import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { SingleLargePageParameters, defaultParameters } from './useSingleLargePageParameters'; // Static function that can be used by both the hook and automation executor -export const buildSingleLargePageFormData = (parameters: SingleLargePageParameters, file: File): FormData => { +export const buildSingleLargePageFormData = (_parameters: SingleLargePageParameters, file: File): FormData => { const formData = new FormData(); formData.append("fileInput", file); return formData; diff --git a/frontend/src/hooks/tools/split/useSplitOperation.ts b/frontend/src/hooks/tools/split/useSplitOperation.ts index 394fb694d..b18b7c1f5 100644 --- a/frontend/src/hooks/tools/split/useSplitOperation.ts +++ b/frontend/src/hooks/tools/split/useSplitOperation.ts @@ -71,7 +71,7 @@ export const useSplitOperation = () => { // Custom response handler that extracts ZIP files // Can't add to exported config because it requires access to the zip code so must be part of the hook - const responseHandler = useCallback(async (blob: Blob, originalFiles: File[]): Promise => { + const responseHandler = useCallback(async (blob: Blob, _originalFiles: File[]): Promise => { // Split operations return ZIP files with multiple PDF pages return await extractZipFiles(blob); }, [extractZipFiles]); diff --git a/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation.ts b/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation.ts index d47800b34..faaeae428 100644 --- a/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation.ts +++ b/frontend/src/hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation.ts @@ -4,7 +4,7 @@ import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { UnlockPdfFormsParameters, defaultParameters } from './useUnlockPdfFormsParameters'; // Static function that can be used by both the hook and automation executor -export const buildUnlockPdfFormsFormData = (parameters: UnlockPdfFormsParameters, file: File): FormData => { +export const buildUnlockPdfFormsFormData = (_parameters: UnlockPdfFormsParameters, file: File): FormData => { const formData = new FormData(); formData.append("fileInput", file); return formData; diff --git a/frontend/src/hooks/useCookieConsent.ts b/frontend/src/hooks/useCookieConsent.ts index 3eacaa7bd..dd00f4396 100644 --- a/frontend/src/hooks/useCookieConsent.ts +++ b/frontend/src/hooks/useCookieConsent.ts @@ -184,11 +184,6 @@ export const useCookieConsent = ({ analyticsEnabled = false }: CookieConsentConf // Force show after initialization setTimeout(() => { window.CookieConsent.show(); - - // Debug: Check if modal elements exist - const ccMain = document.getElementById('cc-main'); - const consentModal = document.querySelector('.cm-wrapper'); - }, 200); } catch (error) { diff --git a/frontend/src/hooks/useEndpointConfig.ts b/frontend/src/hooks/useEndpointConfig.ts index 5419f3506..7516826ed 100644 --- a/frontend/src/hooks/useEndpointConfig.ts +++ b/frontend/src/hooks/useEndpointConfig.ts @@ -19,17 +19,17 @@ export function useEndpointEnabled(endpoint: string): { setLoading(false); return; } - + try { setLoading(true); setError(null); - + const response = await fetch(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`); - + if (!response.ok) { throw new Error(`Failed to check endpoint: ${response.status} ${response.statusText}`); } - + const isEnabled: boolean = await response.json(); setEnabled(isEnabled); } catch (err) { @@ -72,27 +72,27 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { setLoading(false); return; } - + try { setLoading(true); setError(null); - + // Use batch API for efficiency const endpointsParam = endpoints.join(','); - + const response = await fetch(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`); - + if (!response.ok) { throw new Error(`Failed to check endpoints: ${response.status} ${response.statusText}`); } - + const statusMap: Record = await response.json(); setEndpointStatus(statusMap); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; setError(errorMessage); console.error('Failed to check multiple endpoints:', err); - + // Fallback: assume all endpoints are disabled on error const fallbackStatus = endpoints.reduce((acc, endpoint) => { acc[endpoint] = false; @@ -105,7 +105,6 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { }; useEffect(() => { - const endpointsKey = endpoints.join(','); fetchAllEndpointStatuses(); }, [endpoints.join(',')]); // Re-run when endpoints array changes @@ -115,4 +114,4 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { error, refetch: fetchAllEndpointStatuses, }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useEnhancedProcessedFiles.ts b/frontend/src/hooks/useEnhancedProcessedFiles.ts index ebdff4bf5..2f0ab923d 100644 --- a/frontend/src/hooks/useEnhancedProcessedFiles.ts +++ b/frontend/src/hooks/useEnhancedProcessedFiles.ts @@ -49,7 +49,7 @@ export function useEnhancedProcessedFiles( // Process files when activeFiles changes useEffect(() => { console.log('useEnhancedProcessedFiles: activeFiles changed', activeFiles.length, 'files'); - + if (activeFiles.length === 0) { console.log('useEnhancedProcessedFiles: No active files, clearing processed cache'); setProcessedFiles(new Map()); @@ -60,15 +60,15 @@ export function useEnhancedProcessedFiles( const processFiles = async () => { const newProcessedFiles = new Map(); - + for (const file of activeFiles) { // Generate hash for this file const fileHash = await FileHasher.generateHybridHash(file); fileHashMapRef.current.set(file, fileHash); - + // First, check if we have this exact File object cached let existing = processedFiles.get(file); - + // If not found by File object, try to find by hash in case File was recreated if (!existing) { for (const [cachedFile, processed] of processedFiles.entries()) { @@ -79,7 +79,7 @@ export function useEnhancedProcessedFiles( } } } - + if (existing) { newProcessedFiles.set(file, existing); continue; @@ -94,11 +94,11 @@ export function useEnhancedProcessedFiles( console.error(`Failed to start processing for ${file.name}:`, error); } } - + // Only update if the content actually changed const hasChanged = newProcessedFiles.size !== processedFiles.size || Array.from(newProcessedFiles.keys()).some(file => !processedFiles.has(file)); - + if (hasChanged) { setProcessedFiles(newProcessedFiles); } @@ -112,20 +112,20 @@ export function useEnhancedProcessedFiles( const checkForCompletedFiles = async () => { let hasNewFiles = false; const updatedFiles = new Map(processedFiles); - + // Generate file keys for all files first const fileKeyPromises = activeFiles.map(async (file) => ({ file, key: await FileHasher.generateHybridHash(file) })); - + const fileKeyPairs = await Promise.all(fileKeyPromises); - + for (const { file, key } of fileKeyPairs) { // Only check files that don't have processed results yet if (!updatedFiles.has(file)) { const processingState = processingStates.get(key); - + // Check for both processing and recently completed files // This ensures we catch completed files before they're cleaned up if (processingState?.status === 'processing' || processingState?.status === 'completed') { @@ -135,13 +135,13 @@ export function useEnhancedProcessedFiles( updatedFiles.set(file, processed); hasNewFiles = true; } - } catch (error) { + } catch { // Ignore errors in completion check } } } } - + if (hasNewFiles) { setProcessedFiles(updatedFiles); } @@ -158,11 +158,11 @@ export function useEnhancedProcessedFiles( const currentFiles = new Set(activeFiles); const previousFiles = Array.from(processedFiles.keys()); const removedFiles = previousFiles.filter(file => !currentFiles.has(file)); - + if (removedFiles.length > 0) { // Clean up processing service cache enhancedPDFProcessingService.cleanup(removedFiles); - + // Update local state setProcessedFiles(prev => { const updated = new Map(); @@ -179,10 +179,10 @@ export function useEnhancedProcessedFiles( // Calculate derived state const isProcessing = processingStates.size > 0; const hasProcessingErrors = Array.from(processingStates.values()).some(state => state.status === 'error'); - + // Calculate overall progress const processingProgress = calculateProcessingProgress(processingStates); - + // Get cache stats and metrics const cacheStats = enhancedPDFProcessingService.getCacheStats(); const metrics = enhancedPDFProcessingService.getMetrics(); @@ -192,7 +192,7 @@ export function useEnhancedProcessedFiles( cancelProcessing: (fileKey: string) => { enhancedPDFProcessingService.cancelProcessing(fileKey); }, - + retryProcessing: async (file: File) => { try { await enhancedPDFProcessingService.processFile(file, config); @@ -200,7 +200,7 @@ export function useEnhancedProcessedFiles( console.error(`Failed to retry processing for ${file.name}:`, error); } }, - + clearCache: () => { enhancedPDFProcessingService.clearAll(); } @@ -279,7 +279,7 @@ export function useEnhancedProcessedFile( }; } { const result = useEnhancedProcessedFiles(file ? [file] : [], config); - + const processedFile = file ? result.processedFiles.get(file) || null : null; // Note: This is async but we can't await in hook return - consider refactoring if needed const fileKey = file ? '' : ''; @@ -309,4 +309,4 @@ export function useEnhancedProcessedFile( canRetry, actions }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useFileManager.ts b/frontend/src/hooks/useFileManager.ts index f47430fd2..f3dedf5e4 100644 --- a/frontend/src/hooks/useFileManager.ts +++ b/frontend/src/hooks/useFileManager.ts @@ -1,8 +1,7 @@ import { useState, useCallback } from 'react'; import { useIndexedDB } from '../contexts/IndexedDBContext'; import { FileMetadata } from '../types/file'; -import { generateThumbnailForFile } from '../utils/thumbnailUtils'; -import { FileId } from '../types/file'; +import { FileId } from '../types/fileContext'; export const useFileManager = () => { const [loading, setLoading] = useState(false); diff --git a/frontend/src/hooks/useFileWithUrl.ts b/frontend/src/hooks/useFileWithUrl.ts index fd2f2d604..5176c1225 100644 --- a/frontend/src/hooks/useFileWithUrl.ts +++ b/frontend/src/hooks/useFileWithUrl.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { isFileObject } from '../types/fileContext'; /** * Hook to convert a File object to { file: File; url: string } format @@ -8,8 +9,8 @@ export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; u return useMemo(() => { if (!file) return null; - // Validate that file is a proper File or Blob object - if (!(file instanceof File) && !(file instanceof Blob)) { + // Validate that file is a proper File, StirlingFile, or Blob object + if (!isFileObject(file) && !(file instanceof Blob)) { console.warn('useFileWithUrl: Expected File or Blob, got:', file); return null; } diff --git a/frontend/src/hooks/useIndexedDBThumbnail.ts b/frontend/src/hooks/useIndexedDBThumbnail.ts index 4f0d77c0e..cd497561b 100644 --- a/frontend/src/hooks/useIndexedDBThumbnail.ts +++ b/frontend/src/hooks/useIndexedDBThumbnail.ts @@ -2,21 +2,8 @@ import { useState, useEffect } from "react"; import { FileMetadata } from "../types/file"; import { useIndexedDB } from "../contexts/IndexedDBContext"; import { generateThumbnailForFile } from "../utils/thumbnailUtils"; +import { FileId } from "../types/fileContext"; -/** - * Calculate optimal scale for thumbnail generation - * Ensures high quality while preventing oversized renders - */ -function calculateThumbnailScale(pageViewport: { width: number; height: number }): number { - const maxWidth = 400; // Max thumbnail width - const maxHeight = 600; // Max thumbnail height - - const scaleX = maxWidth / pageViewport.width; - const scaleY = maxHeight / pageViewport.height; - - // Don't upscale, only downscale if needed - return Math.min(scaleX, scaleY, 1.0); -} /** * Hook for IndexedDB-aware thumbnail loading @@ -53,7 +40,7 @@ export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): { // Try to load file from IndexedDB using new context if (file.id && indexedDB) { - const loadedFile = await indexedDB.loadFile(file.id); + const loadedFile = await indexedDB.loadFile(file.id as FileId); if (!loadedFile) { throw new Error('File not found in IndexedDB'); } @@ -66,11 +53,11 @@ export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): { const thumbnail = await generateThumbnailForFile(fileObject); if (!cancelled) { setThumb(thumbnail); - + // Save thumbnail to IndexedDB for persistence if (file.id && indexedDB && thumbnail) { try { - await indexedDB.updateThumbnail(file.id, thumbnail); + await indexedDB.updateThumbnail(file.id as FileId, thumbnail); } catch (error) { console.warn('Failed to save thumbnail to IndexedDB:', error); } diff --git a/frontend/src/hooks/usePDFProcessor.ts b/frontend/src/hooks/usePDFProcessor.ts index ab3b5e007..b88bfbe2b 100644 --- a/frontend/src/hooks/usePDFProcessor.ts +++ b/frontend/src/hooks/usePDFProcessor.ts @@ -1,6 +1,7 @@ import { useState, useCallback } from 'react'; import { PDFDocument, PDFPage } from '../types/pageEditor'; import { pdfWorkerManager } from '../services/pdfWorkerManager'; +import { createQuickKey } from '../types/fileContext'; export function usePDFProcessor() { const [loading, setLoading] = useState(false); @@ -75,7 +76,7 @@ export function usePDFProcessor() { // Create pages without thumbnails initially - load them lazily for (let i = 1; i <= totalPages; i++) { pages.push({ - id: `${file.name}-page-${i}`, + id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, originalPageNumber: i, thumbnail: null, // Will be loaded lazily diff --git a/frontend/src/hooks/usePdfSignatureDetection.ts b/frontend/src/hooks/usePdfSignatureDetection.ts index 17f90f2d9..77b7f79ac 100644 --- a/frontend/src/hooks/usePdfSignatureDetection.ts +++ b/frontend/src/hooks/usePdfSignatureDetection.ts @@ -1,13 +1,13 @@ import { useState, useEffect } from 'react'; -import * as pdfjsLib from 'pdfjs-dist'; import { pdfWorkerManager } from '../services/pdfWorkerManager'; +import { StirlingFile } from '../types/fileContext'; export interface PdfSignatureDetectionResult { hasDigitalSignatures: boolean; isChecking: boolean; } -export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionResult => { +export const usePdfSignatureDetection = (files: StirlingFile[]): PdfSignatureDetectionResult => { const [hasDigitalSignatures, setHasDigitalSignatures] = useState(false); const [isChecking, setIsChecking] = useState(false); @@ -25,7 +25,7 @@ export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionRe for (const file of files) { const arrayBuffer = await file.arrayBuffer(); - + try { const pdf = await pdfWorkerManager.createDocument(arrayBuffer); @@ -41,7 +41,7 @@ export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionRe if (foundSignature) break; } - + // Clean up PDF document using worker manager pdfWorkerManager.destroyDocument(pdf); } catch (error) { @@ -65,4 +65,4 @@ export const usePdfSignatureDetection = (files: File[]): PdfSignatureDetectionRe hasDigitalSignatures, isChecking }; -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/useThumbnailGeneration.ts b/frontend/src/hooks/useThumbnailGeneration.ts index 8eba26214..6a22fbcc9 100644 --- a/frontend/src/hooks/useThumbnailGeneration.ts +++ b/frontend/src/hooks/useThumbnailGeneration.ts @@ -1,5 +1,6 @@ -import { useCallback, useRef } from 'react'; +import { useCallback } from 'react'; import { thumbnailGenerationService } from '../services/thumbnailGenerationService'; +import { createQuickKey } from '../types/fileContext'; import { FileId } from '../types/file'; // Request queue to handle concurrent thumbnail requests @@ -71,8 +72,8 @@ async function processRequestQueue() { console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`); - // Use file name as fileId for PDF document caching - const fileId = file.name + '_' + file.size + '_' + file.lastModified as FileId; + // Use quickKey for PDF document caching (same metadata, consistent format) + const fileId = createQuickKey(file) as FileId; const results = await thumbnailGenerationService.generateThumbnails( fileId, diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index f73f458db..3239cbaaa 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy"; @@ -20,15 +20,6 @@ export const useToolManagement = (): ToolManagementResult => { // Build endpoints list from registry entries with fallback to legacy mapping const baseRegistry = useFlatToolRegistry(); - const registryDerivedEndpoints = useMemo(() => { - const endpointsByTool: Record = {}; - Object.entries(baseRegistry).forEach(([key, entry]) => { - if (entry.endpoints && entry.endpoints.length > 0) { - endpointsByTool[key] = entry.endpoints; - } - }); - return endpointsByTool; - }, [baseRegistry]); const allEndpoints = useMemo(() => getAllEndpoints(baseRegistry), [baseRegistry]); const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints); diff --git a/frontend/src/hooks/useToolParameters.ts b/frontend/src/hooks/useToolParameters.ts index d6eae6d8b..1afd66835 100644 --- a/frontend/src/hooks/useToolParameters.ts +++ b/frontend/src/hooks/useToolParameters.ts @@ -10,8 +10,8 @@ type ToolParameterValues = Record; * Register tool parameters and get current values */ export function useToolParameters( - toolName: string, - parameters: Record + _toolName: string, + _parameters: Record ): [ToolParameterValues, (updates: Partial) => void] { // Return empty values and noop updater @@ -30,9 +30,9 @@ export function useToolParameter( definition: any ): [T, (value: T) => void] { const [allParams, updateParams] = useToolParameters(toolName, { [paramName]: definition }); - + const value = allParams[paramName] as T; - + const setValue = useCallback((newValue: T) => { updateParams({ [paramName]: newValue }); }, [paramName, updateParams]); @@ -48,4 +48,4 @@ export function useGlobalParameters() { const updateParameters = useCallback(() => {}, []); return [currentValues, updateParameters]; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useTooltipPosition.ts b/frontend/src/hooks/useTooltipPosition.ts index 3651c1d47..51ef2e9c3 100644 --- a/frontend/src/hooks/useTooltipPosition.ts +++ b/frontend/src/hooks/useTooltipPosition.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect } from 'react'; import { clamp } from '../utils/genericUtils'; import { getSidebarInfo } from '../utils/sidebarUtils'; import { SidebarRefs, SidebarState } from '../types/sidebar'; @@ -65,10 +65,10 @@ export function useTooltipPosition({ sidebarRefs?: SidebarRefs; sidebarState?: SidebarState; }): PositionState { - const [coords, setCoords] = useState<{ top: number; left: number; arrowOffset: number | null }>({ - top: 0, - left: 0, - arrowOffset: null + const [coords, setCoords] = useState<{ top: number; left: number; arrowOffset: number | null }>({ + top: 0, + left: 0, + arrowOffset: null }); const [positionReady, setPositionReady] = useState(false); @@ -174,4 +174,4 @@ export function useTooltipPosition({ }, [open, sidebarLeft, position, gap, sidebarTooltip]); return { coords, positionReady }; -} \ No newline at end of file +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 55fe7f046..38a0c1923 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -35,8 +35,11 @@ function updatePosthogConsent(){ return; } const optIn = (window.CookieConsent as any).acceptedCategory('analytics'); - optIn? - posthog.opt_in_capturing() : posthog.opt_out_capturing(); + if (optIn) { + posthog.opt_in_capturing(); + } else { + posthog.opt_out_capturing(); + } console.log("Updated analytics consent: ", optIn? "opted in" : "opted out"); } diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 961662123..26d190dfa 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,4 +1,3 @@ -import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useToolWorkflow } from "../contexts/ToolWorkflowContext"; import { Group } from "@mantine/core"; @@ -11,7 +10,6 @@ import Workbench from "../components/layout/Workbench"; import QuickAccessBar from "../components/shared/QuickAccessBar"; import RightRail from "../components/shared/RightRail"; import FileManager from "../components/FileManager"; -import Footer from "../components/shared/Footer"; export default function HomePage() { diff --git a/frontend/src/services/enhancedPDFProcessingService.ts b/frontend/src/services/enhancedPDFProcessingService.ts index f9f067c30..bee6e200a 100644 --- a/frontend/src/services/enhancedPDFProcessingService.ts +++ b/frontend/src/services/enhancedPDFProcessingService.ts @@ -1,10 +1,10 @@ -import * as pdfjsLib from 'pdfjs-dist'; -import { ProcessedFile, ProcessingState, PDFPage, ProcessingStrategy, ProcessingConfig, ProcessingMetrics } from '../types/processing'; +import { ProcessedFile, ProcessingState, PDFPage, ProcessingConfig, ProcessingMetrics } from '../types/processing'; import { ProcessingCache } from './processingCache'; import { FileHasher } from '../utils/fileHash'; import { FileAnalyzer } from './fileAnalyzer'; import { ProcessingErrorHandler } from './processingErrorHandler'; import { pdfWorkerManager } from './pdfWorkerManager'; +import { createQuickKey } from '../types/fileContext'; export class EnhancedPDFProcessingService { private static instance: EnhancedPDFProcessingService; @@ -182,7 +182,7 @@ export class EnhancedPDFProcessingService { ): Promise { const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfWorkerManager.createDocument(arrayBuffer); - + try { const totalPages = pdf.numPages; @@ -201,7 +201,7 @@ export class EnhancedPDFProcessingService { const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality); pages.push({ - id: `${file.name}-page-${i}`, + id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail, rotation: 0, @@ -251,7 +251,7 @@ export class EnhancedPDFProcessingService { const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality); pages.push({ - id: `${file.name}-page-${i}`, + id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail, rotation: 0, @@ -266,7 +266,7 @@ export class EnhancedPDFProcessingService { // Create placeholder pages for remaining pages for (let i = priorityCount + 1; i <= totalPages; i++) { pages.push({ - id: `${file.name}-page-${i}`, + id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail: null, // Will be loaded lazily rotation: 0, @@ -313,7 +313,7 @@ export class EnhancedPDFProcessingService { const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality); pages.push({ - id: `${file.name}-page-${i}`, + id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail, rotation: 0, @@ -334,7 +334,7 @@ export class EnhancedPDFProcessingService { // Create placeholders for remaining pages for (let i = firstChunkEnd + 1; i <= totalPages; i++) { pages.push({ - id: `${file.name}-page-${i}`, + id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail: null, rotation: 0, @@ -354,7 +354,7 @@ export class EnhancedPDFProcessingService { */ private async processMetadataOnly( file: File, - config: ProcessingConfig, + _config: ProcessingConfig, state: ProcessingState ): Promise { const arrayBuffer = await file.arrayBuffer(); @@ -368,7 +368,7 @@ export class EnhancedPDFProcessingService { const pages: PDFPage[] = []; for (let i = 1; i <= totalPages; i++) { pages.push({ - id: `${file.name}-page-${i}`, + id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail: null, rotation: 0, @@ -459,11 +459,12 @@ export class EnhancedPDFProcessingService { case 'failed': this.metrics.failedFiles++; break; - case 'cacheHit': + case 'cacheHit': { // Update cache hit rate const totalAttempts = this.metrics.totalFiles + 1; this.metrics.cacheHitRate = (this.metrics.cacheHitRate * this.metrics.totalFiles + 1) / totalAttempts; break; + } } } @@ -508,7 +509,7 @@ export class EnhancedPDFProcessingService { */ clearAllProcessing(): void { // Cancel all ongoing processing - this.processing.forEach((state, key) => { + this.processing.forEach((state) => { if (state.cancellationToken) { state.cancellationToken.abort(); } @@ -519,10 +520,7 @@ export class EnhancedPDFProcessingService { this.notifyListeners(); // Force memory cleanup hint - if (typeof window !== 'undefined' && window.gc) { - let gc = window.gc; - setTimeout(() => gc(), 100); - } + setTimeout(() => window.gc?.(), 100); } /** diff --git a/frontend/src/services/fileAnalyzer.ts b/frontend/src/services/fileAnalyzer.ts index 537692600..5655902a0 100644 --- a/frontend/src/services/fileAnalyzer.ts +++ b/frontend/src/services/fileAnalyzer.ts @@ -128,7 +128,7 @@ export class FileAnalyzer { * Estimate processing time based on file characteristics and strategy */ private static estimateProcessingTime( - fileSize: number, + _fileSize: number, pageCount: number = 0, strategy: ProcessingStrategy ): number { @@ -148,15 +148,17 @@ export class FileAnalyzer { case 'immediate_full': return pageCount * baseTime; - case 'priority_pages': + case 'priority_pages': { // Estimate time for priority pages (first 10) const priorityPages = Math.min(pageCount, 10); return priorityPages * baseTime; + } - case 'progressive_chunked': + case 'progressive_chunked': { // Estimate time for first chunk (20 pages) const firstChunk = Math.min(pageCount, 20); return firstChunk * baseTime; + } default: return pageCount * baseTime; @@ -232,7 +234,7 @@ export class FileAnalyzer { const headerString = String.fromCharCode(...headerBytes); return headerString.startsWith('%PDF-'); - } catch (error) { + } catch { return false; } } diff --git a/frontend/src/services/fileProcessingService.ts b/frontend/src/services/fileProcessingService.ts index c109cff1f..be822b846 100644 --- a/frontend/src/services/fileProcessingService.ts +++ b/frontend/src/services/fileProcessingService.ts @@ -4,7 +4,6 @@ * Called when files are added to FileContext, before any view sees them */ -import * as pdfjsLib from 'pdfjs-dist'; import { generateThumbnailForFile } from '../utils/thumbnailUtils'; import { pdfWorkerManager } from './pdfWorkerManager'; import { FileId } from '../types/file'; diff --git a/frontend/src/services/fileStorage.ts b/frontend/src/services/fileStorage.ts index e500f54b5..e6dbbb464 100644 --- a/frontend/src/services/fileStorage.ts +++ b/frontend/src/services/fileStorage.ts @@ -496,7 +496,7 @@ class FileStorageService { async updateThumbnail(id: FileId, thumbnail: string): Promise { const db = await this.getDatabase(); - return new Promise((resolve, reject) => { + return new Promise((resolve, _reject) => { try { const transaction = db.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); diff --git a/frontend/src/services/indexedDBManager.ts b/frontend/src/services/indexedDBManager.ts index 2048c021f..9251998a3 100644 --- a/frontend/src/services/indexedDBManager.ts +++ b/frontend/src/services/indexedDBManager.ts @@ -73,7 +73,7 @@ class IndexedDBManager { request.onsuccess = () => { const db = request.result; console.log(`Successfully opened ${config.name}`); - + // Set up close handler to clean up our references db.onclose = () => { console.log(`Database ${config.name} closed`); @@ -87,13 +87,11 @@ class IndexedDBManager { request.onupgradeneeded = (event) => { const db = request.result; const oldVersion = event.oldVersion; - + console.log(`Upgrading ${config.name} from v${oldVersion} to v${config.version}`); // Create or update object stores config.stores.forEach(storeConfig => { - let store: IDBObjectStore; - if (db.objectStoreNames.contains(storeConfig.name)) { // Store exists - for now, just continue (could add migration logic here) console.log(`Object store '${storeConfig.name}' already exists`); @@ -109,7 +107,7 @@ class IndexedDBManager { options.autoIncrement = storeConfig.autoIncrement; } - store = db.createObjectStore(storeConfig.name, options); + const store = db.createObjectStore(storeConfig.name, options); console.log(`Created object store '${storeConfig.name}'`); // Create indexes @@ -168,7 +166,7 @@ class IndexedDBManager { return new Promise((resolve, reject) => { const deleteRequest = indexedDB.deleteDatabase(name); - + deleteRequest.onerror = () => reject(deleteRequest.error); deleteRequest.onsuccess = () => { console.log(`Deleted database: ${name}`); @@ -224,4 +222,4 @@ export const DATABASE_CONFIGS = { } as DatabaseConfig } as const; -export const indexedDBManager = IndexedDBManager.getInstance(); \ No newline at end of file +export const indexedDBManager = IndexedDBManager.getInstance(); diff --git a/frontend/src/services/pdfExportService.ts b/frontend/src/services/pdfExportService.ts index fe3e314e0..aa20f4cfc 100644 --- a/frontend/src/services/pdfExportService.ts +++ b/frontend/src/services/pdfExportService.ts @@ -31,7 +31,7 @@ export class PDFExportService { const originalPDFBytes = await pdfDocument.file.arrayBuffer(); const sourceDoc = await PDFLibDocument.load(originalPDFBytes); const blob = await this.createSingleDocument(sourceDoc, pagesToExport); - const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly, false); + const exportFilename = this.generateFilename(filename || pdfDocument.name); return { blob, filename: exportFilename }; } catch (error) { @@ -62,7 +62,7 @@ export class PDFExportService { } const blob = await this.createMultiSourceDocument(sourceFiles, pagesToExport); - const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly, false); + const exportFilename = this.generateFilename(filename || pdfDocument.name); return { blob, filename: exportFilename }; } catch (error) { @@ -130,7 +130,7 @@ export class PDFExportService { newDoc.setModificationDate(new Date()); const pdfBytes = await newDoc.save(); - return new Blob([pdfBytes], { type: 'application/pdf' }); + return new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); } /** @@ -176,14 +176,14 @@ export class PDFExportService { newDoc.setModificationDate(new Date()); const pdfBytes = await newDoc.save(); - return new Blob([pdfBytes], { type: 'application/pdf' }); + return new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); } /** * Generate appropriate filename for export */ - private generateFilename(originalName: string, selectedOnly: boolean, appendSuffix: boolean): string { + private generateFilename(originalName: string): string { const baseName = originalName.replace(/\.pdf$/i, ''); return `${baseName}.pdf`; } @@ -210,7 +210,7 @@ export class PDFExportService { /** * Download multiple files as a ZIP */ - async downloadAsZip(blobs: Blob[], filenames: string[], zipFilename: string): Promise { + async downloadAsZip(blobs: Blob[], filenames: string[]): Promise { blobs.forEach((blob, index) => { setTimeout(() => { this.downloadFile(blob, filenames[index]); diff --git a/frontend/src/services/pdfProcessingService.ts b/frontend/src/services/pdfProcessingService.ts index 065f53210..7ede9334d 100644 --- a/frontend/src/services/pdfProcessingService.ts +++ b/frontend/src/services/pdfProcessingService.ts @@ -1,6 +1,7 @@ import { ProcessedFile, ProcessingState, PDFPage } from '../types/processing'; import { ProcessingCache } from './processingCache'; import { pdfWorkerManager } from './pdfWorkerManager'; +import { createQuickKey } from '../types/fileContext'; export class PDFProcessingService { private static instance: PDFProcessingService; @@ -113,7 +114,7 @@ export class PDFProcessingService { const thumbnail = canvas.toDataURL(); pages.push({ - id: `${file.name}-page-${i}`, + id: `${createQuickKey(file)}-page-${i}`, pageNumber: i, thumbnail, rotation: 0, diff --git a/frontend/src/services/pdfWorkerManager.ts b/frontend/src/services/pdfWorkerManager.ts index 0999c5c29..dda434049 100644 --- a/frontend/src/services/pdfWorkerManager.ts +++ b/frontend/src/services/pdfWorkerManager.ts @@ -1,6 +1,6 @@ /** * PDF.js Worker Manager - Centralized worker lifecycle management - * + * * Prevents infinite worker creation by managing PDF.js workers globally * and ensuring proper cleanup when operations complete. */ @@ -86,14 +86,15 @@ class PDFWorkerManager { const pdf = await loadingTask.promise; this.activeDocuments.add(pdf); this.workerCount++; - + return pdf; } catch (error) { // If document creation fails, make sure to clean up the loading task if (loadingTask) { try { loadingTask.destroy(); - } catch (destroyError) { + } catch { + // Ignore errors } } throw error; @@ -109,7 +110,7 @@ class PDFWorkerManager { pdf.destroy(); this.activeDocuments.delete(pdf); this.workerCount = Math.max(0, this.workerCount - 1); - } catch (error) { + } catch { // Still remove from tracking even if destroy failed this.activeDocuments.delete(pdf); this.workerCount = Math.max(0, this.workerCount - 1); @@ -125,7 +126,7 @@ class PDFWorkerManager { documentsToDestroy.forEach(pdf => { this.destroyDocument(pdf); }); - + this.activeDocuments.clear(); this.workerCount = 0; } @@ -165,10 +166,11 @@ class PDFWorkerManager { this.activeDocuments.forEach(pdf => { try { pdf.destroy(); - } catch (error) { + } catch { + // Ignore errors } }); - + this.activeDocuments.clear(); this.workerCount = 0; } @@ -182,4 +184,4 @@ class PDFWorkerManager { } // Export singleton instance -export const pdfWorkerManager = PDFWorkerManager.getInstance(); \ No newline at end of file +export const pdfWorkerManager = PDFWorkerManager.getInstance(); diff --git a/frontend/src/services/zipFileService.ts b/frontend/src/services/zipFileService.ts index 0b706cd96..872157d51 100644 --- a/frontend/src/services/zipFileService.ts +++ b/frontend/src/services/zipFileService.ts @@ -277,7 +277,7 @@ export class ZipFileService { bytes[2] === 0x44 && // D bytes[3] === 0x46 && // F bytes[4] === 0x2D; // - - } catch (error) { + } catch { return false; } } @@ -324,7 +324,7 @@ export class ZipFileService { await zip.loadAsync(file); // Check if any files are encrypted - for (const [filename, zipEntry] of Object.entries(zip.files)) { + for (const [_filename, zipEntry] of Object.entries(zip.files)) { if (zipEntry.options?.compression === 'STORE' && getData(zipEntry)?.compressedSize === 0) { // This might indicate encryption, but JSZip doesn't provide direct encryption detection // We'll handle this in the extraction phase diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 3b406a91e..b640be3b6 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -63,7 +63,7 @@ for (let i = 0; i < 32; i++) { Object.defineProperty(globalThis, 'crypto', { value: { subtle: { - digest: vi.fn().mockImplementation(async (algorithm: string, data: any) => { + digest: vi.fn().mockImplementation(async (_algorithm: string, _data: any) => { // Always return the mock hash buffer regardless of input return mockHashBuffer.slice(); }), diff --git a/frontend/src/tests/convert/ConvertE2E.spec.ts b/frontend/src/tests/convert/ConvertE2E.spec.ts index 60e2c4849..5e250030e 100644 --- a/frontend/src/tests/convert/ConvertE2E.spec.ts +++ b/frontend/src/tests/convert/ConvertE2E.spec.ts @@ -17,7 +17,6 @@ import * as fs from 'fs'; // Test configuration const BASE_URL = process.env.BASE_URL || 'http://localhost:5173'; -const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080'; /** * Resolves test fixture paths dynamically based on current working directory. @@ -266,7 +265,6 @@ async function testConversion(page: Page, conversion: ConversionEndpoint) { } // Discover conversions at module level before tests are defined -let allConversions: ConversionEndpoint[] = []; let availableConversions: ConversionEndpoint[] = []; let unavailableConversions: ConversionEndpoint[] = []; @@ -275,7 +273,6 @@ let unavailableConversions: ConversionEndpoint[] = []; try { availableConversions = await conversionDiscovery.getAvailableConversions(); unavailableConversions = await conversionDiscovery.getUnavailableConversions(); - allConversions = [...availableConversions, ...unavailableConversions]; } catch (error) { console.error('Failed to discover conversions during module load:', error); } diff --git a/frontend/src/tests/convert/ConvertIntegration.test.tsx b/frontend/src/tests/convert/ConvertIntegration.test.tsx index 41a768838..bf2c46662 100644 --- a/frontend/src/tests/convert/ConvertIntegration.test.tsx +++ b/frontend/src/tests/convert/ConvertIntegration.test.tsx @@ -11,13 +11,15 @@ import React from 'react'; import { describe, test, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; -import { renderHook, act, waitFor } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react'; import { useConvertOperation } from '../../hooks/tools/convert/useConvertOperation'; import { ConvertParameters } from '../../hooks/tools/convert/useConvertParameters'; import { FileContextProvider } from '../../contexts/FileContext'; import { I18nextProvider } from 'react-i18next'; import i18n from '../../i18n/config'; import axios from 'axios'; +import { createTestStirlingFile } from '../utils/testFileHelpers'; +import { StirlingFile } from '../../types/fileContext'; // Mock axios vi.mock('axios'); @@ -51,13 +53,9 @@ vi.mock('../../services/thumbnailGenerationService', () => ({ })); // Create realistic test files -const createTestFile = (name: string, content: string, type: string): File => { - return new File([content], name, { type }); -}; - -const createPDFFile = (): File => { +const createPDFFile = (): StirlingFile => { const pdfContent = '%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\ntrailer\n<<\n/Size 2\n/Root 1 0 R\n>>\nstartxref\n0\n%%EOF'; - return createTestFile('test.pdf', pdfContent, 'application/pdf'); + return createTestStirlingFile('test.pdf', pdfContent, 'application/pdf'); }; // Test wrapper component @@ -162,7 +160,7 @@ describe('Convert Tool Integration Tests', () => { wrapper: TestWrapper }); - const testFile = createTestFile('invalid.txt', 'not a pdf', 'text/plain'); + const testFile = createTestStirlingFile('invalid.txt', 'not a pdf', 'text/plain'); const parameters: ConvertParameters = { fromExtension: 'pdf', toExtension: 'png', @@ -426,7 +424,7 @@ describe('Convert Tool Integration Tests', () => { }); const files = [ createPDFFile(), - createTestFile('test2.pdf', '%PDF-1.4...', 'application/pdf') + createTestStirlingFile('test2.pdf', '%PDF-1.4...', 'application/pdf') ] const parameters: ConvertParameters = { fromExtension: 'pdf', @@ -527,7 +525,7 @@ describe('Convert Tool Integration Tests', () => { wrapper: TestWrapper }); - const corruptedFile = createTestFile('corrupted.pdf', 'not-a-pdf', 'application/pdf'); + const corruptedFile = createTestStirlingFile('corrupted.pdf', 'not-a-pdf', 'application/pdf'); const parameters: ConvertParameters = { fromExtension: 'pdf', toExtension: 'png', diff --git a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx index 4e9fb7908..52826ce3f 100644 --- a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx +++ b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx @@ -14,6 +14,7 @@ import i18n from '../../i18n/config'; import axios from 'axios'; import { detectFileExtension } from '../../utils/fileUtils'; import { FIT_OPTIONS } from '../../constants/convertConstants'; +import { createTestStirlingFile, createTestFilesWithId } from '../utils/testFileHelpers'; // Mock axios vi.mock('axios'); @@ -81,7 +82,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock DOCX file - const docxFile = new File(['docx content'], 'document.docx', { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); + const docxFile = createTestStirlingFile('document.docx', 'docx content', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); // Test auto-detection act(() => { @@ -117,7 +118,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock unknown file - const unknownFile = new File(['unknown content'], 'document.xyz', { type: 'application/octet-stream' }); + const unknownFile = createTestStirlingFile('document.xyz', 'unknown content', 'application/octet-stream'); // Test auto-detection act(() => { @@ -156,11 +157,11 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock image files - const imageFiles = [ - new File(['jpg content'], 'photo1.jpg', { type: 'image/jpeg' }), - new File(['png content'], 'photo2.png', { type: 'image/png' }), - new File(['gif content'], 'photo3.gif', { type: 'image/gif' }) - ]; + const imageFiles = createTestFilesWithId([ + { name: 'photo1.jpg', content: 'jpg content', type: 'image/jpeg' }, + { name: 'photo2.png', content: 'png content', type: 'image/png' }, + { name: 'photo3.gif', content: 'gif content', type: 'image/gif' } + ]); // Test smart detection for all images act(() => { @@ -202,11 +203,11 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mixed file types - const mixedFiles = [ - new File(['pdf content'], 'document.pdf', { type: 'application/pdf' }), - new File(['docx content'], 'spreadsheet.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), - new File(['pptx content'], 'presentation.pptx', { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }) - ]; + const mixedFiles = createTestFilesWithId([ + { name: 'document.pdf', content: 'pdf content', type: 'application/pdf' }, + { name: 'spreadsheet.xlsx', content: 'docx content', type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, + { name: 'presentation.pptx', content: 'pptx content', type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' } + ]); // Test smart detection for mixed types act(() => { @@ -243,10 +244,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); // Create mock web files - const webFiles = [ - new File(['content'], 'page1.html', { type: 'text/html' }), - new File(['zip content'], 'site.zip', { type: 'application/zip' }) - ]; + const webFiles = createTestFilesWithId([ + { name: 'page1.html', content: 'content', type: 'text/html' }, + { name: 'site.zip', content: 'zip content', type: 'application/zip' } + ]); // Test smart detection for web files act(() => { @@ -288,7 +289,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const htmlFile = new File(['content'], 'page.html', { type: 'text/html' }); + const htmlFile = createTestStirlingFile('page.html', 'content', 'text/html'); // Set up HTML conversion parameters act(() => { @@ -318,7 +319,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const emlFile = new File(['email content'], 'email.eml', { type: 'message/rfc822' }); + const emlFile = createTestStirlingFile('email.eml', 'email content', 'message/rfc822'); // Set up email conversion parameters act(() => { @@ -355,7 +356,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const pdfFile = new File(['pdf content'], 'document.pdf', { type: 'application/pdf' }); + const pdfFile = createTestStirlingFile('document.pdf', 'pdf content', 'application/pdf'); // Set up PDF/A conversion parameters act(() => { @@ -392,10 +393,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const imageFiles = [ - new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }), - new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' }) - ]; + const imageFiles = createTestFilesWithId([ + { name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' }, + { name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' } + ]); // Set up image conversion parameters act(() => { @@ -432,10 +433,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { wrapper: TestWrapper }); - const imageFiles = [ - new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }), - new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' }) - ]; + const imageFiles = createTestFilesWithId([ + { name: 'photo1.jpg', content: 'jpg1', type: 'image/jpeg' }, + { name: 'photo2.jpg', content: 'jpg2', type: 'image/jpeg' } + ]); // Set up for separate processing act(() => { @@ -477,10 +478,10 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }) .mockRejectedValueOnce(new Error('File 2 failed')); - const mixedFiles = [ - new File(['file1'], 'doc1.txt', { type: 'text/plain' }), - new File(['file2'], 'doc2.xyz', { type: 'application/octet-stream' }) - ]; + const mixedFiles = createTestFilesWithId([ + { name: 'doc1.txt', content: 'file1', type: 'text/plain' }, + { name: 'doc2.xyz', content: 'file2', type: 'application/octet-stream' } + ]); // Set up for separate processing (mixed smart detection) act(() => { @@ -505,7 +506,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { describe('Real File Extension Detection', () => { test('should correctly detect various file extensions', async () => { - const { result } = renderHook(() => useConvertParameters(), { + renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); diff --git a/frontend/src/tests/utils/testFileHelpers.ts b/frontend/src/tests/utils/testFileHelpers.ts new file mode 100644 index 000000000..80b3c74cf --- /dev/null +++ b/frontend/src/tests/utils/testFileHelpers.ts @@ -0,0 +1,28 @@ +/** + * Test utilities for creating StirlingFile objects in tests + */ + +import { StirlingFile, createStirlingFile } from '../../types/fileContext'; + +/** + * Create a StirlingFile object for testing purposes + */ +export function createTestStirlingFile( + name: string, + content: string = 'test content', + type: string = 'application/pdf' +): StirlingFile { + const file = new File([content], name, { type }); + return createStirlingFile(file); +} + +/** + * Create multiple StirlingFile objects for testing + */ +export function createTestFilesWithId( + files: Array<{ name: string; content?: string; type?: string }> +): StirlingFile[] { + return files.map(({ name, content = 'test content', type = 'application/pdf' }) => + createTestStirlingFile(name, content, type) + ); +} \ No newline at end of file diff --git a/frontend/src/theme/mantineTheme.ts b/frontend/src/theme/mantineTheme.ts index 2cd70d645..b7cd70a18 100644 --- a/frontend/src/theme/mantineTheme.ts +++ b/frontend/src/theme/mantineTheme.ts @@ -75,7 +75,7 @@ export const mantineTheme = createTheme({ }, variants: { // Custom button variant for PDF tools - pdfTool: (theme: any) => ({ + pdfTool: (_theme: any) => ({ root: { backgroundColor: 'var(--bg-surface)', border: '1px solid var(--border-default)', @@ -108,7 +108,7 @@ export const mantineTheme = createTheme({ }, }, Textarea: { - styles: (theme: any) => ({ + styles: (_theme: any) => ({ input: { backgroundColor: 'var(--bg-surface)', borderColor: 'var(--border-default)', @@ -126,7 +126,7 @@ export const mantineTheme = createTheme({ }, TextInput: { - styles: (theme: any) => ({ + styles: (_theme: any) => ({ input: { backgroundColor: 'var(--bg-surface)', borderColor: 'var(--border-default)', @@ -144,7 +144,7 @@ export const mantineTheme = createTheme({ }, PasswordInput: { - styles: (theme: any) => ({ + styles: (_theme: any) => ({ input: { backgroundColor: 'var(--bg-surface)', borderColor: 'var(--border-default)', diff --git a/frontend/src/tools/AddPassword.tsx b/frontend/src/tools/AddPassword.tsx index adbc25b5a..c29491437 100644 --- a/frontend/src/tools/AddPassword.tsx +++ b/frontend/src/tools/AddPassword.tsx @@ -1,15 +1,14 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useEndpointEnabled } from "../hooks/useEndpointConfig"; import { useFileSelection } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings"; -import { useAddPasswordParameters, defaultParameters } from "../hooks/tools/addPassword/useAddPasswordParameters"; +import { useAddPasswordParameters } from "../hooks/tools/addPassword/useAddPasswordParameters"; import { useAddPasswordOperation } from "../hooks/tools/addPassword/useAddPasswordOperation"; import { useAddPasswordTips } from "../components/tooltips/useAddPasswordTips"; import { useAddPasswordPermissionsTips } from "../components/tooltips/useAddPasswordPermissionsTips"; @@ -17,7 +16,6 @@ import { BaseToolProps, ToolComponent } from "../types/tool"; const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); const { selectedFiles } = useFileSelection(); const [collapsedPermissions, setCollapsedPermissions] = useState(true); diff --git a/frontend/src/tools/AddWatermark.tsx b/frontend/src/tools/AddWatermark.tsx index 7065b2a5b..3bafbd329 100644 --- a/frontend/src/tools/AddWatermark.tsx +++ b/frontend/src/tools/AddWatermark.tsx @@ -1,8 +1,7 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useEndpointEnabled } from "../hooks/useEndpointConfig"; import { useFileSelection } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; @@ -25,7 +24,6 @@ import { BaseToolProps, ToolComponent } from "../types/tool"; const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); const { selectedFiles } = useFileSelection(); const [collapsedType, setCollapsedType] = useState(false); diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index 589c38678..db89db162 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -1,6 +1,5 @@ -import React, { useState, useMemo, useEffect } from "react"; +import React, { useState, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { useFileContext } from "../contexts/FileContext"; import { useFileSelection } from "../contexts/FileContext"; import { useNavigationActions } from "../contexts/NavigationContext"; import { useToolWorkflow } from "../contexts/ToolWorkflowContext"; diff --git a/frontend/src/tools/Convert.tsx b/frontend/src/tools/Convert.tsx index 5ced39670..05fd87531 100644 --- a/frontend/src/tools/Convert.tsx +++ b/frontend/src/tools/Convert.tsx @@ -1,8 +1,7 @@ -import React, { useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useEndpointEnabled } from "../hooks/useEndpointConfig"; import { useFileState, useFileSelection } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; @@ -15,7 +14,6 @@ import { BaseToolProps, ToolComponent } from "../types/tool"; const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); const { selectors } = useFileState(); - const { actions } = useNavigationActions(); const activeFiles = selectors.getFiles(); const { selectedFiles } = useFileSelection(); const scrollContainerRef = useRef(null); diff --git a/frontend/src/tools/OCR.tsx b/frontend/src/tools/OCR.tsx index fcfb96841..c8a30fea4 100644 --- a/frontend/src/tools/OCR.tsx +++ b/frontend/src/tools/OCR.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useEndpointEnabled } from "../hooks/useEndpointConfig"; import { useFileSelection } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index 6a0cef697..f22ee9159 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; import SplitSettings from "../components/tools/split/SplitSettings"; diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 9210f9ce9..f5d4cef0a 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -5,6 +5,9 @@ import { PageOperation } from './pageEditor'; import { FileId, FileMetadata } from './file'; +// Re-export FileId for convenience +export type { FileId }; + export type ModeType = | 'viewer' | 'pageEditor' @@ -41,25 +44,32 @@ export interface ProcessedFileMetadata { [key: string]: any; } -export interface FileRecord { - id: FileId; - name: string; - size: number; - type: string; - lastModified: number; - quickKey?: string; // Fast deduplication key: name|size|lastModified - thumbnailUrl?: string; - blobUrl?: string; - createdAt?: number; - processedFile?: ProcessedFileMetadata; - insertAfterPageId?: string; // Page ID after which this file should be inserted - isPinned?: boolean; +/** + * StirlingFileStub - Metadata record for files in the active workbench session + * + * Contains UI display data and processing state. Actual File objects stored + * separately in refs for memory efficiency. Supports multi-tool workflows + * where files persist across tool operations. + */ +export interface StirlingFileStub { + id: FileId; // UUID primary key for collision-free operations + name: string; // Display name for UI + size: number; // File size for progress indicators + type: string; // MIME type for format validation + lastModified: number; // Original timestamp for deduplication + quickKey?: string; // Fast deduplication key: name|size|lastModified + thumbnailUrl?: string; // Generated thumbnail blob URL for visual display + blobUrl?: string; // File access blob URL for downloads/processing + createdAt?: number; // When added to workbench for sorting + processedFile?: ProcessedFileMetadata; // PDF page data and processing results + insertAfterPageId?: string; // Page ID after which this file should be inserted + isPinned?: boolean; // Protected from tool consumption (replace/remove) // Note: File object stored in provider ref, not in state } export interface FileContextNormalizedFiles { ids: FileId[]; - byId: Record; + byId: Record; } // Helper functions - UUID-based primary keys (zero collisions, synchronous) @@ -82,9 +92,68 @@ export function createQuickKey(file: File): string { return `${file.name}|${file.size}|${file.lastModified}`; } +// Stirling PDF file with embedded UUID - replaces loose File + FileId parameter passing +export interface StirlingFile extends File { + readonly fileId: FileId; + readonly quickKey: string; // Fast deduplication key: name|size|lastModified +} + +// Type guard to check if a File object has an embedded fileId +export function isStirlingFile(file: File): file is StirlingFile { + return 'fileId' in file && typeof (file as any).fileId === 'string' && + 'quickKey' in file && typeof (file as any).quickKey === 'string'; +} + +// Create a StirlingFile from a regular File object +export function createStirlingFile(file: File, id?: FileId): StirlingFile { + const fileId = id || createFileId(); + const quickKey = createQuickKey(file); + + // Use Object.defineProperty to add properties while preserving the original File object + // This maintains proper method binding and avoids "Illegal invocation" errors + Object.defineProperty(file, 'fileId', { + value: fileId, + writable: false, + enumerable: true, + configurable: false + }); + + Object.defineProperty(file, 'quickKey', { + value: quickKey, + writable: false, + enumerable: true, + configurable: false + }); + + return file as StirlingFile; +} + +// Extract FileIds from StirlingFile array +export function extractFileIds(files: StirlingFile[]): FileId[] { + return files.map(file => file.fileId); +} + +// Extract regular File objects from StirlingFile array +export function extractFiles(files: StirlingFile[]): File[] { + return files as File[]; +} + +// Check if an object is a File or StirlingFile (replaces instanceof File checks) +export function isFileObject(obj: any): obj is File | StirlingFile { + return obj && + typeof obj.name === 'string' && + typeof obj.size === 'number' && + typeof obj.type === 'string' && + typeof obj.lastModified === 'number' && + typeof obj.arrayBuffer === 'function'; +} -export function toFileRecord(file: File, id?: FileId): FileRecord { + +export function toStirlingFileStub( + file: File, + id?: FileId +): StirlingFileStub { const fileId = id || createFileId(); return { id: fileId, @@ -97,7 +166,7 @@ export function toFileRecord(file: File, id?: FileId): FileRecord { }; } -export function revokeFileResources(record: FileRecord): void { +export function revokeFileResources(record: StirlingFileStub): void { // Only revoke blob: URLs to prevent errors on other schemes if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) { try { @@ -171,7 +240,7 @@ export interface FileContextState { // Core file management - lightweight file IDs only files: { ids: FileId[]; - byId: Record; + byId: Record; }; // Pinned files - files that won't be consumed by tools @@ -190,16 +259,16 @@ export interface FileContextState { // Action types for reducer pattern export type FileContextAction = // File management actions - | { type: 'ADD_FILES'; payload: { fileRecords: FileRecord[] } } + | { type: 'ADD_FILES'; payload: { stirlingFileStubs: StirlingFileStub[] } } | { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } } - | { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial } } + | { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial } } | { type: 'REORDER_FILES'; payload: { orderedFileIds: FileId[] } } // Pinned files actions | { type: 'PIN_FILE'; payload: { fileId: FileId } } | { type: 'UNPIN_FILE'; payload: { fileId: FileId } } - | { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputFileRecords: FileRecord[] } } - | { type: 'UNDO_CONSUME_FILES'; payload: { inputFileRecords: FileRecord[]; outputFileIds: FileId[] } } + | { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputStirlingFileStubs: StirlingFileStub[] } } + | { type: 'UNDO_CONSUME_FILES'; payload: { inputStirlingFileStubs: StirlingFileStub[]; outputFileIds: FileId[] } } // UI actions | { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } } @@ -215,22 +284,22 @@ export type FileContextAction = export interface FileContextActions { // File management - lightweight actions only - addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise; - addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise; - addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise; + addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise; + addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise; + addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise; removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise; - updateFileRecord: (id: FileId, updates: Partial) => void; + updateStirlingFileStub: (id: FileId, updates: Partial) => void; reorderFiles: (orderedFileIds: FileId[]) => void; clearAllFiles: () => Promise; clearAllData: () => Promise; - // File pinning - pinFile: (file: File) => void; - unpinFile: (file: File) => void; + // File pinning - accepts StirlingFile for safer type checking + pinFile: (file: StirlingFile) => void; + unpinFile: (file: StirlingFile) => void; // File consumption (replace unpinned files with outputs) consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise; - undoConsumeFiles: (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]) => Promise; + undoConsumeFiles: (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]) => Promise; // Selection management setSelectedFiles: (fileIds: FileId[]) => void; setSelectedPages: (pageNumbers: number[]) => void; @@ -253,26 +322,17 @@ export interface FileContextActions { // File selectors (separate from actions to avoid re-renders) export interface FileContextSelectors { - // File access - no state dependency, uses ref - getFile: (id: FileId) => File | undefined; - getFiles: (ids?: FileId[]) => File[]; - - // Record access - uses normalized state - getFileRecord: (id: FileId) => FileRecord | undefined; - getFileRecords: (ids?: FileId[]) => FileRecord[]; - - // Derived selectors + getFile: (id: FileId) => StirlingFile | undefined; + getFiles: (ids?: FileId[]) => StirlingFile[]; + getStirlingFileStub: (id: FileId) => StirlingFileStub | undefined; + getStirlingFileStubs: (ids?: FileId[]) => StirlingFileStub[]; getAllFileIds: () => FileId[]; - getSelectedFiles: () => File[]; - getSelectedFileRecords: () => FileRecord[]; - - // Pinned files selectors + getSelectedFiles: () => StirlingFile[]; + getSelectedStirlingFileStubs: () => StirlingFileStub[]; getPinnedFileIds: () => FileId[]; - getPinnedFiles: () => File[]; - getPinnedFileRecords: () => FileRecord[]; - isFilePinned: (file: File) => boolean; - - // Stable signature for effect dependencies + getPinnedFiles: () => StirlingFile[]; + getPinnedStirlingFileStubs: () => StirlingFileStub[]; + isFilePinned: (file: StirlingFile) => boolean; getFilesSignature: () => string; } @@ -293,6 +353,3 @@ export interface FileContextActionsValue { actions: FileContextActions; dispatch: (action: FileContextAction) => void; } - -// TODO: URL parameter types will be redesigned for new routing system - diff --git a/frontend/src/types/fileIdSafety.d.ts b/frontend/src/types/fileIdSafety.d.ts new file mode 100644 index 000000000..13aac82dd --- /dev/null +++ b/frontend/src/types/fileIdSafety.d.ts @@ -0,0 +1,49 @@ +/** + * Type safety declarations to prevent file.name/UUID confusion + */ + +import { FileId, StirlingFile } from './fileContext'; + +declare global { + namespace FileIdSafety { + // Mark functions that should never accept file.name as parameters + type SafeFileIdFunction any> = T extends (...args: infer P) => infer _R + ? P extends readonly [string, ...any[]] + ? never // Reject string parameters in first position for FileId functions + : T + : T; + + // Mark functions that should only accept StirlingFile, not regular File + type StirlingFileOnlyFunction any> = T extends (...args: infer P) => infer _R + ? P extends readonly [File, ...any[]] + ? never // Reject File parameters in first position for StirlingFile functions + : T + : T; + + // Utility type to enforce StirlingFile usage + type RequireStirlingFile = T extends File ? StirlingFile : T; + } + + // Extend Window interface for debugging + interface Window { + __FILE_ID_DEBUG?: boolean; + } +} + +// Augment FileContext types to prevent bypassing StirlingFile +declare module '../contexts/FileContext' { + export interface StrictFileContextActions { + pinFile: (file: StirlingFile) => void; // Must be StirlingFile + unpinFile: (file: StirlingFile) => void; // Must be StirlingFile + addFiles: (files: File[], options?: { insertAfterPageId?: string }) => Promise; // Returns StirlingFile + consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise; // Returns StirlingFile + } + + export interface StrictFileContextSelectors { + getFile: (id: FileId) => StirlingFile | undefined; // Returns StirlingFile + getFiles: (ids?: FileId[]) => StirlingFile[]; // Returns StirlingFile[] + isFilePinned: (file: StirlingFile) => boolean; // Must be StirlingFile + } +} + +export {}; diff --git a/frontend/src/types/parameters.ts b/frontend/src/types/parameters.ts index 6f8856a8b..91355cb81 100644 --- a/frontend/src/types/parameters.ts +++ b/frontend/src/types/parameters.ts @@ -1,7 +1,6 @@ // Base parameter interfaces for reusable patterns -export interface BaseParameters { - // Base interface that all tool parameters should extend - // Provides a foundation for adding common properties across all tools - // Examples of future additions: userId, sessionId, commonFlags, etc. -} \ No newline at end of file +// Base interface that all tool parameters should extend +// Provides a foundation for adding common properties across all tools +// Examples of future additions: userId, sessionId, commonFlags, etc. +export type BaseParameters = object diff --git a/frontend/src/utils/automationExecutor.ts b/frontend/src/utils/automationExecutor.ts index 124f065a7..3feb9b412 100644 --- a/frontend/src/utils/automationExecutor.ts +++ b/frontend/src/utils/automationExecutor.ts @@ -1,6 +1,5 @@ import axios from 'axios'; import { ToolRegistry } from '../data/toolsTaxonomy'; -import { AutomationConfig, AutomationExecutionCallbacks } from '../types/automation'; import { AUTOMATION_CONSTANTS } from '../constants/automation'; import { AutomationFileProcessor } from './automationFileProcessor'; import { ResourceManager } from './resourceManager'; diff --git a/frontend/src/utils/automationFileProcessor.ts b/frontend/src/utils/automationFileProcessor.ts index 45abbaafc..d81dd3a1b 100644 --- a/frontend/src/utils/automationFileProcessor.ts +++ b/frontend/src/utils/automationFileProcessor.ts @@ -2,7 +2,7 @@ * File processing utilities specifically for automation workflows */ -import axios, { AxiosResponse } from 'axios'; +import axios from 'axios'; import { zipFileService } from '../services/zipFileService'; import { ResourceManager } from './resourceManager'; import { AUTOMATION_CONSTANTS } from '../constants/automation'; diff --git a/frontend/src/utils/fileIdSafety.ts b/frontend/src/utils/fileIdSafety.ts new file mode 100644 index 000000000..3fda8a4a5 --- /dev/null +++ b/frontend/src/utils/fileIdSafety.ts @@ -0,0 +1,14 @@ +/** + * Runtime validation utilities for FileId safety + */ + +import { FileId } from '../types/fileContext'; + +// Validate that a string is a proper FileId (has UUID format) +export function isValidFileId(id: string): id is FileId { + // Check UUID v4 format: 8-4-4-4-12 hex digits + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return uuidRegex.test(id); +} + + diff --git a/frontend/src/utils/fileResponseUtils.ts b/frontend/src/utils/fileResponseUtils.ts index 472cccb05..659c2948d 100644 --- a/frontend/src/utils/fileResponseUtils.ts +++ b/frontend/src/utils/fileResponseUtils.ts @@ -11,11 +11,11 @@ export const getFilenameFromHeaders = (contentDisposition: string = ''): string const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); if (match && match[1]) { const filename = match[1].replace(/['"]/g, ''); - + // Decode URL-encoded characters (e.g., %20 -> space) try { return decodeURIComponent(filename); - } catch (error) { + } catch { // If decoding fails, return the original filename return filename; } @@ -37,9 +37,9 @@ export const createFileFromApiResponse = ( ): File => { const contentType = headers?.['content-type'] || 'application/octet-stream'; const contentDisposition = headers?.['content-disposition'] || ''; - + const filename = getFilenameFromHeaders(contentDisposition) || fallbackFilename; const blob = new Blob([responseData], { type: contentType }); - + return new File([blob], filename, { type: contentType }); -}; \ No newline at end of file +}; diff --git a/frontend/src/utils/thumbnailUtils.ts b/frontend/src/utils/thumbnailUtils.ts index e4a48f9fd..5f4cac3e6 100644 --- a/frontend/src/utils/thumbnailUtils.ts +++ b/frontend/src/utils/thumbnailUtils.ts @@ -346,12 +346,12 @@ export async function generateThumbnailForFile(file: File): Promise { // Handle PDF files if (file.type.startsWith('application/pdf')) { const scale = calculateScaleFromFileSize(file.size); - + // Only read first 2MB for thumbnail generation to save memory const chunkSize = 2 * 1024 * 1024; // 2MB const chunk = file.slice(0, Math.min(chunkSize, file.size)); const arrayBuffer = await chunk.arrayBuffer(); - + try { return await generatePDFThumbnail(arrayBuffer, file, scale); } catch (error) { @@ -361,7 +361,7 @@ export async function generateThumbnailForFile(file: File): Promise { // Try with full file instead of chunk const fullArrayBuffer = await file.arrayBuffer(); return await generatePDFThumbnail(fullArrayBuffer, file, scale); - } catch (fullFileError) { + } catch { console.warn(`Full file PDF processing also failed for ${file.name} - using placeholder`); return generatePlaceholderThumbnail(file); } @@ -392,11 +392,11 @@ export async function generateThumbnailWithMetadata(file: File): Promise