diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c884272c4..05b3bfb86 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,24 +10,25 @@ "license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", - "@embedpdf/core": "^1.3.14", - "@embedpdf/engines": "^1.3.14", - "@embedpdf/plugin-annotation": "^1.3.14", - "@embedpdf/plugin-export": "^1.3.14", - "@embedpdf/plugin-history": "^1.3.14", - "@embedpdf/plugin-interaction-manager": "^1.3.14", - "@embedpdf/plugin-loader": "^1.3.14", - "@embedpdf/plugin-pan": "^1.3.14", - "@embedpdf/plugin-render": "^1.3.14", - "@embedpdf/plugin-rotate": "^1.3.14", - "@embedpdf/plugin-scroll": "^1.3.14", - "@embedpdf/plugin-search": "^1.3.14", - "@embedpdf/plugin-selection": "^1.3.14", - "@embedpdf/plugin-spread": "^1.3.14", - "@embedpdf/plugin-thumbnail": "^1.3.14", - "@embedpdf/plugin-tiling": "^1.3.14", - "@embedpdf/plugin-viewport": "^1.3.14", - "@embedpdf/plugin-zoom": "^1.3.14", + "@embedpdf/core": "^1.4.1", + "@embedpdf/engines": "^1.4.1", + "@embedpdf/plugin-annotation": "^1.4.1", + "@embedpdf/plugin-export": "^1.4.1", + "@embedpdf/plugin-history": "^1.4.1", + "@embedpdf/plugin-interaction-manager": "^1.4.1", + "@embedpdf/plugin-loader": "^1.4.1", + "@embedpdf/plugin-pan": "^1.4.1", + "@embedpdf/plugin-redaction": "^1.4.1", + "@embedpdf/plugin-render": "^1.4.1", + "@embedpdf/plugin-rotate": "^1.4.1", + "@embedpdf/plugin-scroll": "^1.4.1", + "@embedpdf/plugin-search": "^1.4.1", + "@embedpdf/plugin-selection": "^1.4.1", + "@embedpdf/plugin-spread": "^1.4.1", + "@embedpdf/plugin-thumbnail": "^1.4.1", + "@embedpdf/plugin-tiling": "^1.4.1", + "@embedpdf/plugin-viewport": "^1.4.1", + "@embedpdf/plugin-zoom": "^1.4.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@iconify/react": "^6.0.2", @@ -502,63 +503,65 @@ } }, "node_modules/@embedpdf/core": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.14.tgz", - "integrity": "sha512-lE/vfhA53CxamaCfGWEibrEPr+JeZT42QCF+cOELUwv4+Zt6b+IE6+4wsznx/8wjjJYwllXJ3GJ/un1UzTqARw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.4.1.tgz", + "integrity": "sha512-TGpxn2CvAKRnOJWJ3bsK+dKBiCp75ehxftRUmv7wAmPomhnG5XrDfoWJungvO+zbbqAwso6PocdeXINVt3hlAw==", "license": "MIT", "dependencies": { - "@embedpdf/engines": "1.3.14", - "@embedpdf/models": "1.3.14" + "@embedpdf/engines": "1.4.1", + "@embedpdf/models": "1.4.1" }, "peerDependencies": { "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/engines": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.14.tgz", - "integrity": "sha512-+/FPW2gAzj2lQYvsMH/Oj9+MEXgkyEuyYDC+HFkltTuXvmiP2S/3BD0YslZDX9K4BzcmMxnWB+BiQpNJokbDVg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.4.1.tgz", + "integrity": "sha512-yugIb5OwTI/1VnAaEvSYxAd2DvYBPkV/D7wytagyaOq98o3sqzcY2Q9zHt+LhnawA5KKG1e/FDPjCd4qm8gsvg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14", - "@embedpdf/pdfium": "1.3.14" + "@embedpdf/models": "1.4.1", + "@embedpdf/pdfium": "1.4.1" }, "peerDependencies": { "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/models": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.14.tgz", - "integrity": "sha512-BujY4bmr8b2DQdoZkOge03SzoRVoWxzfIQATLSPPtp4WiFh1U4BPp6cADlGuCwGkp6zBcH/aM4h8PwwA75d/eg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.4.1.tgz", + "integrity": "sha512-2nTg8Q1qpplBvspZJXMCZOA+/OILpfdNRPddlplxZXY/Upx0rzKXx/e6pXWW7AuOgtfGneT4h9tMs3A595/PdQ==", "license": "MIT" }, "node_modules/@embedpdf/pdfium": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.14.tgz", - "integrity": "sha512-TQMZabXzHmzvvfPwopubFcYgQuYV7POvMgjICYu3Pgfn3sgr+UdIUh3aNXR/COcl3q8sXPMFQ2GDuyOHR9QQnA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.4.1.tgz", + "integrity": "sha512-BekKEK4UNCwzj7xOffKn6WpL0FQHxq+mTj2iGI3N7OwAX2J/BO2G+rDOB+lvojQG+Dkpg8uqm427ZKJDRyLgVQ==", "license": "MIT" }, "node_modules/@embedpdf/plugin-annotation": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.3.14.tgz", - "integrity": "sha512-JJYqEWwUKCdBZsXCDq/CW96p3pVLn8N+XZ4W3OyL7djI2fvYC9x6ys9m82vwlSathAVOxk1D7xXiY8AzJQVF0Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.4.1.tgz", + "integrity": "sha512-d4HibNy6ecyDqx2Y2R8VjaqppSdjNofAJmU6VenOd88wn080sAUqvnkeVJ6ehJH5BoND4ymQrcAkcbVeYK0myA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14", - "@embedpdf/utils": "1.3.14" + "@embedpdf/models": "1.4.1", + "@embedpdf/utils": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-history": "1.3.14", - "@embedpdf/plugin-interaction-manager": "1.3.14", - "@embedpdf/plugin-selection": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-history": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-selection": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -566,31 +569,32 @@ } }, "node_modules/@embedpdf/plugin-export": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.14.tgz", - "integrity": "sha512-fMGp2YxvI4uTRIViUKxfnJts2Jw/vktEM45XUNGNSjT/kAW6znVNgdceYjpK++xU8CGs2grAQ1i5UvMd3aRNDA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.4.1.tgz", + "integrity": "sha512-g89fREFM/zkt2Ai2Q5dWwDkhXgC/JmVyUniaMgm1fTG/MZ0Z05E7f34DUzX/CKcJyVjxEgl6tojBTMeUbm15bA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-history": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.14.tgz", - "integrity": "sha512-77hnNLp0W0FHw8lT7SeqzCgp8bOClfeOAPZdcInu/jPDhVASUGYbtE/0fkLhiaqPH7kyMirNCLif4sF6n4b5vg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.4.1.tgz", + "integrity": "sha512-5WLDiNMH6tACkLGGv/lJtNsDeozOhSbrh0mjD1btHun8u7Yscu/Vf8tdJRUOsd+nULivo2nQ2NFNKu0OTbVo8w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -598,49 +602,71 @@ } }, "node_modules/@embedpdf/plugin-interaction-manager": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.14.tgz", - "integrity": "sha512-nR0ZxNoTQtGqOHhweFh6QJ+nUJ4S4Ag1wWur6vAUAi8U95HUOfZhOEa0polZo0zR9WmmblGqRWjFM+mVSOoi1w==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.4.1.tgz", + "integrity": "sha512-Ng02S9SFIAi9JZS5rI+NXSnZZ1Yk9YYRw4MlN2pig49qOyivZdz0oScZaYxQPewo8ccJkLeghjdeWswOBW/6cA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-loader": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.14.tgz", - "integrity": "sha512-KoJX1MacEWE2DrO1OeZeG/Ehz76//u+ida/xb4r9BfwqAp5TfYlksq09cOvcF8LMW5FY4pbAL+AHKI1Hjz+HNA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.4.1.tgz", + "integrity": "sha512-m3ZOk8JygsLxoa4cZ+0BVB5pfRWuBCg2/gPqjhoFZNKTqAFw4J6HGUrhYKg94GRYe+w1cTJl/NbTBYuU5DOrsA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-pan": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.3.14.tgz", - "integrity": "sha512-7EG+I5nn8yDCV8pT4x/g5mv7zJli2t3wPrh6Kt8uIpUorPHNb6J0Z67gl0uc/8rEasNzuKOuT0er46Y6/UYLzQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.4.1.tgz", + "integrity": "sha512-zmOZJ9dUqXiaV0F5GPf/5WTWf3jAEkiv153Tl3x8HT9Rfff+WQhV48NruCIBAy/T4jVt4aH7D1zt/B/ftvcdkA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-interaction-manager": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", + "preact": "^10.26.4", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "svelte": ">=5 <6", + "vue": ">=3.2.0" + } + }, + "node_modules/@embedpdf/plugin-redaction": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-1.4.1.tgz", + "integrity": "sha512-lyVIDxAwU8mUbhxh8J1dhWZ90086YSZ1+RB50hfCAyaxftRF3XpHPhmLZhr6lr3z1lDx+w9VIcoCU6IvhcweTw==", + "license": "MIT", + "dependencies": { + "@embedpdf/models": "1.4.1", + "@embedpdf/utils": "1.4.1" + }, + "peerDependencies": { + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-selection": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -648,182 +674,192 @@ } }, "node_modules/@embedpdf/plugin-render": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.14.tgz", - "integrity": "sha512-IPj7GCQXJBsY++JaU+z7y+FwX5NaDBj4YYV6hsHNtSGf42Y1AdlwJzDYetivG2bA84xmk7KgD1X2Y3eIFBhjwA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.4.1.tgz", + "integrity": "sha512-gKCdNKw6WBHBEpTc2DLBWIWOxzsNnaNbpfeY6C4f2Bum0EO+XW3Hl2oIx1uaRHjIhhnXso1J3QweqelsPwDGwg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-rotate": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.3.14.tgz", - "integrity": "sha512-OroEm11x/fPPXI9C0X+nm9LOjwaI0MvsToZRH+HpV60/FbQeOJvt6D8wThCDVLK95Na6A+JeYIMEu+Hiix7H+A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.4.1.tgz", + "integrity": "sha512-hVzHkKwMNH3tUhxqJGsj5qTLpYZXbj6E74AEcG0w/fz5FrK7EnofPqt0gRfYmIzxnQGIh+39BRtcp8gmx8UNnw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-scroll": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.14.tgz", - "integrity": "sha512-fQbt7OlRMLQJMuZj/Bzh0qpRxMw1ld5Qe/OTw8N54b/plljnFA52joE7cITl3H03huWWyHS3NKOScbw7f34dog==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.4.1.tgz", + "integrity": "sha512-Y9O+matB4j4fLim5s/jn7qIi+lMC9vmDJRpJhiWe8bvD9oYLP2xfD/DdhFgAjRKcNhPoxC+j8q8QN5BMeGAv2Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-search": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.14.tgz", - "integrity": "sha512-tlZEgR2tG+GSNnh2u1SjCxhUHfTDgcr38sE/xRK1bRLDGPZWlr6Ln7qP7JSWqeYBGni75sGrj0iZqcZbPWyJag==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.4.1.tgz", + "integrity": "sha512-8JG4CbOcUsLuT0vHJJ4cECmu+Yn53EokWFUVXi2Mo/XvHjhrQuWmD7+y6s/qQPEpctFYWmUCXTDAX9ynPud+2Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-loader": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-loader": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-selection": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.14.tgz", - "integrity": "sha512-EXENuaAsse3rT6cjA1nYzyrNvoy62ojJl28wblCng6zcs3HSlGPemIQZAvaYKPUxoY608M+6nKlcMQ5neRnk/A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.4.1.tgz", + "integrity": "sha512-lo5Ytk1PH0PrRKv6zKVupm4t02VGsqIrnSIeP6NO8Ujx0wfqEhj//sqIuO/EwfFVJD8lcQIP9UUo9y8baCrEog==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-interaction-manager": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-spread": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.3.14.tgz", - "integrity": "sha512-DVlk6tDgUoDRkp2S4Jc3LrRTuf4DPMlph9vywJw5z6Qpbh0vgcMnObg896/S0Eu5FgACNAj0WGcXpLrcrn5b9Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.4.1.tgz", + "integrity": "sha512-l+SrDVGTiiItkt2cEtzv7V/X5HhmLbYHcQ8CFobGeIKdJtzKS1Nu/JSKqg7Ki7eCNgyPL1yMNfNE92bNKYVN4w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-loader": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-loader": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-thumbnail": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.3.14.tgz", - "integrity": "sha512-cnwb5dG8Jph8XSArys1WFCQ6kK2R5FKoO0B5mDrHFv9Fcm2pKszlmZC/NDoskX4pgNUgSnwhI1X3cP37ebF9Ng==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.4.1.tgz", + "integrity": "sha512-bN3msjI0PovazgbPK3LyugYVTwIDo0RyBUhBaG42FgJxeY3hmFOWTPgfUH1QF7twHlySnksIvHRFYR3nViryVw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-render": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-render": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-tiling": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.14.tgz", - "integrity": "sha512-SaCTo2LdZwGeE6jCqkwJxvwt8YKbsI3QGxa9S7Ez+5OcBchlhHeTfLQswcErDQ3WH2p8WHtGuucAcOLrVVOm0A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.4.1.tgz", + "integrity": "sha512-wgTfj5T8HV6KP61iiR63DVNrbVp8sPxTqa1Sm+2/D0jY+EPSSCmpt1/qYWiAXd1X+t78foOjCnfbo7fEMn5/pg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-render": "1.3.14", - "@embedpdf/plugin-scroll": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-render": "1.4.1", + "@embedpdf/plugin-scroll": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-viewport": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.14.tgz", - "integrity": "sha512-mfJ7EbbU68eKk6oFvQ4ozGJNpxUxWbjQ5Gm3uuB+Gj5/tWgBocBOX36k/9LgivEEeX7g2S0tOgyErljApmH8Vg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.4.1.tgz", + "integrity": "sha512-+TgFHKPCLTBiDYe2DdsmTS37hwQgcZ3dYIc7bE0l5cp+GVwouu1h0MTmjL+90loizeWwCiu10E/zXR6hz+CUaQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-zoom": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.14.tgz", - "integrity": "sha512-/N5tyMk+8OzhObrS3O9yPkcmX8EPiuTo+WaT2QCVSmIUqKnOO4AnKpHJ6Vl0uVhcuXHCMwLucZKyhJ7tRqavwg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.4.1.tgz", + "integrity": "sha512-9HocmXnPZxqN06q7kyNAmLjgDHOEW8/8QfgNE3nMpRyNHIgnAjxvsWc9lApgp5ErDPG0cSDt0Cduil6nB3wSBQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14", + "@embedpdf/models": "1.4.1", "hammerjs": "^2.0.8" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-interaction-manager": "1.3.14", - "@embedpdf/plugin-scroll": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-scroll": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/utils": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.3.14.tgz", - "integrity": "sha512-gxEJD12nageCMqAjdbicNfDQolXU3nvnV0EX96OdZITRNj0Q1tisutVYoaxcCiJu3vvIEOzipjsAnQOubbFCEA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.4.1.tgz", + "integrity": "sha512-vvJ51Qsz3PyJWR2YvDMMpJXg4+YqdV7Vn2cusmW9sx+4EnAiBiw0HevEE+FepgFV8k+A0WbwXzmsujDIQJ7R4A==", "license": "MIT", "peerDependencies": { "preact": "^10.26.4", @@ -3045,6 +3081,16 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", + "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^8.9.0" + } + }, "node_modules/@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -3802,7 +3848,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/gapi": { @@ -4743,7 +4788,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5147,6 +5191,16 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", @@ -7176,6 +7230,13 @@ "node": "*" } }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT", + "peer": true + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -7234,6 +7295,16 @@ "node": ">=0.10" } }, + "node_modules/esrap": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", + "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -8747,6 +8818,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.6" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9623,6 +9704,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT", + "peer": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -12959,6 +13047,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svelte": { + "version": "5.43.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.3.tgz", + "integrity": "sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^2.1.0", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -14447,6 +14571,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT", + "peer": true + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/frontend/package.json b/frontend/package.json index 892e48569..39f1a23b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,24 +6,25 @@ "proxy": "http://localhost:8080", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", - "@embedpdf/core": "^1.3.14", - "@embedpdf/engines": "^1.3.14", - "@embedpdf/plugin-annotation": "^1.3.14", - "@embedpdf/plugin-export": "^1.3.14", - "@embedpdf/plugin-history": "^1.3.14", - "@embedpdf/plugin-interaction-manager": "^1.3.14", - "@embedpdf/plugin-loader": "^1.3.14", - "@embedpdf/plugin-pan": "^1.3.14", - "@embedpdf/plugin-render": "^1.3.14", - "@embedpdf/plugin-rotate": "^1.3.14", - "@embedpdf/plugin-scroll": "^1.3.14", - "@embedpdf/plugin-search": "^1.3.14", - "@embedpdf/plugin-selection": "^1.3.14", - "@embedpdf/plugin-spread": "^1.3.14", - "@embedpdf/plugin-thumbnail": "^1.3.14", - "@embedpdf/plugin-tiling": "^1.3.14", - "@embedpdf/plugin-viewport": "^1.3.14", - "@embedpdf/plugin-zoom": "^1.3.14", + "@embedpdf/core": "^1.4.1", + "@embedpdf/engines": "^1.4.1", + "@embedpdf/plugin-annotation": "^1.4.1", + "@embedpdf/plugin-export": "^1.4.1", + "@embedpdf/plugin-history": "^1.4.1", + "@embedpdf/plugin-interaction-manager": "^1.4.1", + "@embedpdf/plugin-loader": "^1.4.1", + "@embedpdf/plugin-pan": "^1.4.1", + "@embedpdf/plugin-render": "^1.4.1", + "@embedpdf/plugin-rotate": "^1.4.1", + "@embedpdf/plugin-scroll": "^1.4.1", + "@embedpdf/plugin-search": "^1.4.1", + "@embedpdf/plugin-selection": "^1.4.1", + "@embedpdf/plugin-spread": "^1.4.1", + "@embedpdf/plugin-thumbnail": "^1.4.1", + "@embedpdf/plugin-tiling": "^1.4.1", + "@embedpdf/plugin-viewport": "^1.4.1", + "@embedpdf/plugin-zoom": "^1.4.1", + "@embedpdf/plugin-redaction": "^1.4.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@iconify/react": "^6.0.2", diff --git a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx index ba49ffb70..8dd8bc980 100644 --- a/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/core/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -64,7 +64,7 @@ export default function ViewerAnnotationControls({ currentView, disabled = false disabled={disabled || currentView !== 'viewer' || viewerContext?.isAnnotationMode || isPlacementMode} > @@ -136,6 +136,10 @@ export default function ViewerAnnotationControls({ currentView, disabled = false radius="md" className="right-rail-icon" onClick={() => { + // Disable redaction tool while entering draw mode + try { viewerContext?.redactionActions.commitAllPending?.(); } catch {} + try { viewerContext?.redactionActions.deactivate?.(); } catch {} + try { viewerContext?.panActions.disablePan?.(); } catch {} viewerContext?.toggleAnnotationMode(); // Activate ink drawing tool when entering annotation mode if (signatureApiRef?.current && currentView === 'viewer') { @@ -146,6 +150,10 @@ export default function ViewerAnnotationControls({ currentView, disabled = false console.log('Signature API not ready:', error); } } + // Notify after state settles so right rail updates reliably + setTimeout(() => { + try { viewerContext?.triggerToolModeUpdate?.(); } catch {} + }, 0); }} disabled={disabled} aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'} @@ -164,6 +172,8 @@ export default function ViewerAnnotationControls({ currentView, disabled = false onClick={async () => { if (viewerContext?.exportActions?.saveAsCopy && currentView === 'viewer') { try { + // Commit any pending redactions before exporting + try { await viewerContext.redactionActions.commitAllPending(); } catch {} const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy(); if (pdfArrayBuffer) { // Create new File object with flattened annotations @@ -196,6 +206,8 @@ export default function ViewerAnnotationControls({ currentView, disabled = false // Replace the original file with the saved version await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]); + // Clear redaction dirty flag after save + sessionStorage.removeItem('redaction:dirty'); } } } diff --git a/frontend/src/core/components/tools/redact/RedactManualControls.tsx b/frontend/src/core/components/tools/redact/RedactManualControls.tsx new file mode 100644 index 000000000..49065e888 --- /dev/null +++ b/frontend/src/core/components/tools/redact/RedactManualControls.tsx @@ -0,0 +1,48 @@ +import { Stack } from '@mantine/core'; +import ButtonSelector from '@app/components/shared/ButtonSelector'; +import React, { useEffect, useState } from 'react'; + +export type ManualRedactionType = 'redactSelection' | 'marqueeRedact'; + +interface Props { + value: ManualRedactionType; // reflect current plugin state if you like + onChange?: (v: ManualRedactionType) => void; + disabled?: boolean; +} + +const LS_KEY = 'redaction:lastChoice'; + +export default function RedactManualControls({ value, onChange, disabled = false }: Props) { + const [internal, setInternal] = useState(value); + + useEffect(() => { + if (value && value !== internal) setInternal(value); + }, [value]); // keep UI in sync + + const apply = (v: ManualRedactionType) => { + localStorage.setItem(LS_KEY, v); + (document as any)._embedpdf_redactMode = v; + setInternal(v); + onChange?.(v); + // tell the bridge immediately + const epdf = (window as any).__EMBEDPDF__?.bridges?.redaction; + const apiBridge = epdf?.apiBridge; + apiBridge?.setLastClicked?.(v); + apiBridge?.setMode?.(v); + }; + + return ( + + + + ); +} diff --git a/frontend/src/core/components/tools/redact/RedactModeSelector.tsx b/frontend/src/core/components/tools/redact/RedactModeSelector.tsx index 47f15da66..3d781558b 100644 --- a/frontend/src/core/components/tools/redact/RedactModeSelector.tsx +++ b/frontend/src/core/components/tools/redact/RedactModeSelector.tsx @@ -24,7 +24,6 @@ export default function RedactModeSelector({ mode, onModeChange, disabled }: Red { value: 'manual' as const, label: t('redact.modeSelector.manual', 'Manual'), - disabled: true, // Keep manual mode disabled until implemented }, ]} disabled={disabled} diff --git a/frontend/src/core/components/tooltips/useRedactTips.ts b/frontend/src/core/components/tooltips/useRedactTips.ts index 7689a6a2a..0da458dd1 100644 --- a/frontend/src/core/components/tooltips/useRedactTips.ts +++ b/frontend/src/core/components/tooltips/useRedactTips.ts @@ -1,10 +1,11 @@ import { useTranslation } from 'react-i18next'; +import { useMemo } from 'react'; import { TooltipContent } from '@app/types/tips'; export const useRedactModeTips = (): TooltipContent => { const { t } = useTranslation(); - return { + return useMemo(() => ({ header: { title: t("redact.tooltip.mode.header.title", "Redaction Method") }, @@ -18,13 +19,13 @@ export const useRedactModeTips = (): TooltipContent => { description: t("redact.tooltip.mode.manual.text", "Click and drag to manually select specific areas to redact. Gives you precise control over what gets redacted. (Coming soon)") } ] - }; + }), [t]); }; export const useRedactWordsTips = (): TooltipContent => { const { t } = useTranslation(); - return { + return useMemo(() => ({ header: { title: t("redact.tooltip.words.header.title", "Words to Redact") }, @@ -43,13 +44,13 @@ export const useRedactWordsTips = (): TooltipContent => { description: t("redact.tooltip.words.examples.text", "Typical words to redact include: bank details, email addresses, or specific names.") } ] - }; + }), [t]); }; export const useRedactAdvancedTips = (): TooltipContent => { const { t } = useTranslation(); - return { + return useMemo(() => ({ header: { title: t("redact.tooltip.advanced.header.title", "Advanced Redaction Settings") }, @@ -75,5 +76,25 @@ export const useRedactAdvancedTips = (): TooltipContent => { description: t("redact.tooltip.advanced.convert.text", "Converts the PDF to an image-based PDF after redaction. This ensures text behind redaction boxes is completely removed and unrecoverable.") } ] - }; + }), [t]); +}; + +export const useRedactManualTips = (): TooltipContent => { + const { t } = useTranslation(); + + return useMemo(() => ({ + header: { + title: t("redact.tooltip.manual.header.title", "Manual Redaction") + }, + tips: [ + { + title: t("redact.tooltip.manual.selectionByText.title", "Redact by Text"), + description: t("redact.tooltip.manual.selectionByText.text", "Select and redact specific text in the document. Click and drag to select text, then apply the redaction to permanently remove it."), + }, + { + title: t("redact.tooltip.manual.selectionByArea.title", "Redact by Area"), + description: t("redact.tooltip.manual.selectionByArea.text", "Draw a box to redact any area of the document, regardless of content. Click and drag to create a redaction box that covers the area you want to remove."), + } + ] + }), [t]); }; diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index f85058a73..bd4edda41 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -35,7 +35,7 @@ const EmbedPdfViewerContent = ({ const viewerRef = React.useRef(null); const [isViewerHovered, setIsViewerHovered] = React.useState(false); - const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, getRotationState, isAnnotationMode, isAnnotationsVisible, exportActions } = useViewer(); + const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, getRotationState, isAnnotationMode, isAnnotationsVisible, exportActions, getRedactionState, redactionActions } = useViewer(); // Register viewer right-rail buttons useViewerRightRailButtons(); @@ -184,6 +184,17 @@ const EmbedPdfViewerContent = ({ }, [isViewerHovered]); // Register checker for unsaved changes (annotations only for now) + // Use refs for stable references to avoid re-registration + const registerUnsavedChangesCheckerRef = useRef(registerUnsavedChangesChecker); + const unregisterUnsavedChangesCheckerRef = useRef(unregisterUnsavedChangesChecker); + const getRedactionStateRef = useRef(getRedactionState); + + useEffect(() => { + registerUnsavedChangesCheckerRef.current = registerUnsavedChangesChecker; + unregisterUnsavedChangesCheckerRef.current = unregisterUnsavedChangesChecker; + getRedactionStateRef.current = getRedactionState; + }, [registerUnsavedChangesChecker, unregisterUnsavedChangesChecker, getRedactionState]); + useEffect(() => { if (previewFile) { return; @@ -192,21 +203,25 @@ const EmbedPdfViewerContent = ({ const checkForChanges = () => { // Check for annotation changes via history const hasAnnotationChanges = historyApiRef.current?.canUndo() || false; + // Check for redaction changes: any pending marks + const redactionState = getRedactionStateRef.current(); + const hasRedactionChanges = redactionState.pendingCount > 0 || sessionStorage.getItem('redaction:dirty') === 'true'; console.log('[Viewer] Checking for unsaved changes:', { - hasAnnotationChanges + hasAnnotationChanges, + hasRedactionChanges }); - return hasAnnotationChanges; + return hasAnnotationChanges || hasRedactionChanges; }; console.log('[Viewer] Registering unsaved changes checker'); - registerUnsavedChangesChecker(checkForChanges); + registerUnsavedChangesCheckerRef.current(checkForChanges); return () => { console.log('[Viewer] Unregistering unsaved changes checker'); - unregisterUnsavedChangesChecker(); + unregisterUnsavedChangesCheckerRef.current(); }; - }, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]); + }, [previewFile, historyApiRef]); // Removed function dependencies - use refs instead // Apply changes - save annotations to new file version const applyChanges = useCallback(async () => { @@ -215,6 +230,9 @@ const EmbedPdfViewerContent = ({ try { console.log('[Viewer] Applying changes - exporting PDF with annotations'); + // Commit any pending redactions before exporting + try { await redactionActions.commitAllPending(); } catch (e) { /* ignore */ } + // Step 1: Export PDF with annotations using EmbedPDF const arrayBuffer = await exportActions.saveAsCopy(); if (!arrayBuffer) { @@ -238,6 +256,7 @@ const EmbedPdfViewerContent = ({ await actions.consumeFiles(activeFileIds, stirlingFiles, stubs); setHasUnsavedChanges(false); + sessionStorage.removeItem('redaction:dirty'); } catch (error) { console.error('Apply changes failed:', error); } @@ -298,6 +317,17 @@ const EmbedPdfViewerContent = ({ }} /> + {/* Overlay root for redaction menus - rendered above all PDF pages */} +
)} diff --git a/frontend/src/core/components/viewer/HoverToSelectPending.tsx b/frontend/src/core/components/viewer/HoverToSelectPending.tsx new file mode 100644 index 000000000..fd732e815 --- /dev/null +++ b/frontend/src/core/components/viewer/HoverToSelectPending.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef } from 'react'; +import { useRedaction } from '@embedpdf/plugin-redaction/react'; + +type Props = { + pageIndex: number; + scale: number; + getPageEl: () => HTMLElement | null; +}; + +/** + * When hovering a pending box and the user is in marquee (area) mode, + * temporarily pause drawing so clicks select the box. + * When leaving the box (and nothing is selected), restore the last clicked mode. + */ +export default function HoverToSelectPending({ pageIndex, scale, getPageEl }: Props) { + const { state, provides } = useRedaction(); + const pausedRef = useRef(false); + + useEffect(() => { + const el = getPageEl(); + if (!el || !provides) return; + + const restoreIfIdle = () => { + if (!pausedRef.current) return; + if (state.selected) return; // keep paused while a box is selected + pausedRef.current = false; + + const desired = (document as any)._embedpdf_redactMode as 'marqueeRedact' | 'redactSelection' | undefined; + const anyProv = provides as any; + if (desired && typeof anyProv.setActiveType === 'function') { + anyProv.setActiveType(desired); + } else if (desired === 'marqueeRedact' && state.activeType !== 'marqueeRedact') { + provides.toggleMarqueeRedact?.(); + } else if (desired === 'redactSelection' && state.activeType !== 'redactSelection') { + provides.toggleRedactSelection?.(); + } + }; + + const onMove = (e: PointerEvent) => { + const list: any[] = (state.pending as any)?.[pageIndex] || []; + if (!list.length) { restoreIfIdle(); return; } + + const r = el.getBoundingClientRect(); + const px = (e.clientX - r.left) / scale; + const py = (e.clientY - r.top) / scale; + + const hit = list.find((it) => { + if (!it?.rect) return false; + const { pos, size } = it.rect; + if (!pos || !size) return false; + return px >= pos.x && px <= pos.x + size.width && py >= pos.y && py <= pos.y + size.height; + }); + + // Only pause auto-draw when we're in marquee mode and actually hovering a box + if (hit && state.activeType === 'marqueeRedact' && !pausedRef.current) { + pausedRef.current = true; + provides.toggleMarqueeRedact?.(); // turn OFF drawing so clicks select the box + } else if (!hit) { + restoreIfIdle(); + } + }; + + const onLeave = () => restoreIfIdle(); + + el.addEventListener('pointermove', onMove, { passive: true }); + el.addEventListener('pointerleave', onLeave, { passive: true }); + return () => { + el.removeEventListener('pointermove', onMove); + el.removeEventListener('pointerleave', onLeave); + }; + }, [provides, state.pending, state.selected, state.activeType, pageIndex, scale]); + + return null; +} diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index bfe75df2f..bf8bc1751 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createPluginRegistration } from '@embedpdf/core'; import { EmbedPDF } from '@embedpdf/core/react'; import { usePdfiumEngine } from '@embedpdf/engines/react'; @@ -11,6 +11,7 @@ import { RenderPluginPackage } from '@embedpdf/plugin-render/react'; import { ZoomPluginPackage } from '@embedpdf/plugin-zoom/react'; import { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react'; import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selection/react'; +import { RedactionLayer, RedactionPluginPackage } from '@embedpdf/plugin-redaction/react'; import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react'; import { PanPluginPackage } from '@embedpdf/plugin-pan/react'; import { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react'; @@ -38,6 +39,9 @@ import { SignatureAPIBridge } from '@app/components/viewer/SignatureAPIBridge'; import { HistoryAPIBridge } from '@app/components/viewer/HistoryAPIBridge'; import type { SignatureAPI, HistoryAPI } from '@app/components/viewer/viewerTypes'; import { ExportAPIBridge } from '@app/components/viewer/ExportAPIBridge'; +import { RedactionAPIBridge } from './RedactionAPIBridge'; +import RedactionSelectionMenu from '@app/components/viewer/RedactionSelectionMenu'; +import HoverToSelectPending from '@app/components/viewer/HoverToSelectPending'; interface LocalEmbedPDFProps { file?: File | Blob; @@ -51,6 +55,7 @@ interface LocalEmbedPDFProps { export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) { const [pdfUrl, setPdfUrl] = useState(null); const [, setAnnotations] = useState>([]); + const pageElRef = useRef>({}); // Convert File to URL if needed useEffect(() => { @@ -96,6 +101,11 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur // Register selection plugin (depends on InteractionManager) createPluginRegistration(SelectionPluginPackage), + // Register redaction plugin (depends on InteractionManager and Selection) + createPluginRegistration(RedactionPluginPackage, { + drawBlackBoxes: true, + }), + // Register history plugin for undo/redo (recommended for annotations) ...(enableAnnotations ? [createPluginRegistration(HistoryPluginPackage)] : []), @@ -273,6 +283,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur {enableAnnotations && } {enableAnnotations && } + { + renderPage={({ width, height, pageIndex, scale, rotation }) => { return ( - +
e.preventDefault()} onDrop={(e) => e.preventDefault()} onDragOver={(e) => e.preventDefault()} + ref={(node) => { pageElRef.current[pageIndex] = node; }} > {/* High-resolution tile layer */} @@ -318,6 +330,18 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur {/* Selection layer for text interaction */} + {/* Redaction layer handles pending marks and UI */} + } + /> + pageElRef.current[pageIndex] ?? null} + /> {/* Annotation layer for signatures (only when enabled) */} {enableAnnotations && ( pan.makePanDefault(), } }); + // Notify listeners whenever pan state changes + triggerToolModeUpdate(); } - }, [pan, isPanning]); + }, [pan, isPanning, triggerToolModeUpdate, registerBridge]); return null; } diff --git a/frontend/src/core/components/viewer/RedactionAPIBridge.tsx b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx new file mode 100644 index 000000000..6988e0bbe --- /dev/null +++ b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx @@ -0,0 +1,149 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { useRedaction } from '@embedpdf/plugin-redaction/react'; +import { useViewer } from '@app/contexts/ViewerContext'; + +/** + * Behavior contract: + * - The *only* authority for the desired mode is the last toolbar button the user clicked. + * - We persist that in localStorage and mirror it on document for quick reads. + * - We never invert the user's choice during "create/pending/select" frames. + * - After the plugin finishes its internal flip (end of draw, commit, etc), + * we nudge it back to the user's choice using a deferred restore (microtask + 2x rAF). + */ + +const LS_KEY = 'redaction:lastChoice'; // 'redactSelection' | 'marqueeRedact' +type Mode = 'redactSelection' | 'marqueeRedact'; + +export function RedactionAPIBridge() { + const { state, provides } = useRedaction(); + const { registerBridge } = useViewer(); + + // live state ref + const stateRef = useRef(state); + useEffect(() => { stateRef.current = state; }, [state]); + + // desired mode (last clicked) + const lastChoiceRef = useRef( + (localStorage.getItem(LS_KEY) as Mode | null) ?? null + ); + + const setLastChoice = useCallback((mode: Mode) => { + lastChoiceRef.current = mode; + (document as any)._embedpdf_redactMode = mode; // local mirror only + }, []); + + // bridge surface for other UI + const bridgeRef = useRef<{ state: any; api: any } | null>(null); + useEffect(() => { + if (!provides) return; + const bridge = { + state: { + isRedacting: stateRef.current.isRedacting, + activeType: stateRef.current.activeType, + pendingCount: stateRef.current.pendingCount, + selected: stateRef.current.selected, + }, + api: provides, + }; + bridgeRef.current = bridge; + registerBridge('redaction', bridge); + }, [registerBridge, provides]); + + // keep bridge.state fresh + const prev = useRef(bridgeRef.current?.state); + useEffect(() => { + if (!bridgeRef.current) return; + const s = { + isRedacting: state.isRedacting, + activeType: state.activeType, + pendingCount: state.pendingCount, + selected: state.selected, + }; + if (!prev.current || + prev.current.isRedacting !== s.isRedacting || + prev.current.activeType !== s.activeType || + prev.current.pendingCount !== s.pendingCount || + prev.current.selected !== s.selected) { + bridgeRef.current.state = s; + prev.current = s; + } + }, [state.isRedacting, state.activeType, state.pendingCount, state.selected]); + + // idempotent "set" using toggles (there is no official setter) + const setMode = useCallback((target: Mode | null) => { + if (!provides || !target) return; + if (stateRef.current.activeType === target) return; + + const anyProv = provides as any; + if (typeof anyProv.setActiveType === 'function') { + anyProv.setActiveType(target); + return; + } + target === 'marqueeRedact' + ? provides.toggleMarqueeRedact?.() + : provides.toggleRedactSelection?.(); + }, [provides]); + + // public mini-API for your toolbar + useEffect(() => { + if (!bridgeRef.current) return; + (bridgeRef.current as any).apiBridge = { + setLastClicked: (mode: Mode) => setLastChoice(mode), + setMode, + getLastClicked: () => lastChoiceRef.current, + }; + }, [setMode, setLastChoice]); + + // defer helper: run after the plugin's own state churn + const restoringRef = useRef(false); + const deferRestore = useCallback(() => { + if (restoringRef.current) return; + restoringRef.current = true; + // microtask + double rAF to reliably run *after* the plugin's flip + Promise.resolve().then(() => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const desired = lastChoiceRef.current; + const s = stateRef.current; + if (desired && s.activeType !== desired) { + setMode(desired); // idempotent + } + setTimeout(() => { restoringRef.current = false; }, 60); + }); + }); + }); + }, [setMode]); + + // remember user-initiated switches (when plugin reports a mode) + useEffect(() => { + if (!restoringRef.current) return; + if (state.activeType === lastChoiceRef.current) { + restoringRef.current = false; + } + }, [state.activeType]); + + // steer back to user's choice after *any* redaction event + useEffect(() => { + if (!provides) return; + const off = provides.onRedactionEvent?.((_evt: any) => { + // Mark dirty for your nav guard if you use one + try { sessionStorage.setItem('redaction:dirty', 'true'); } catch {} + // Always defer a restore — hover helper will temporarily pause drawing when needed + deferRestore(); + }); + return () => { off && off(); }; + }, [provides, deferRestore]); + + // seed once on tool open + useEffect(() => { + if (!provides) return; + const remembered = (localStorage.getItem(LS_KEY) as Mode | null) ?? null; + if (remembered) { + setLastChoice(remembered); + // set immediately so first interaction is correct + setMode(remembered); + } + }, [provides, setMode, setLastChoice]); + + return null; +} diff --git a/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx b/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx new file mode 100644 index 000000000..f93d6212c --- /dev/null +++ b/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx @@ -0,0 +1,201 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import { SelectionMenuProps, useRedaction } from '@embedpdf/plugin-redaction/react'; + +/** + * Small inline menu rendered by RedactionLayer when a pending mark is selected. + * Shows Accept (commit) and Remove controls. + * Uses a portal to render at document body level to ensure it's always above PDF pages. + */ +export default function RedactionSelectionMenu({ item, selected, menuWrapperProps }: SelectionMenuProps) { + const { provides, state } = useRedaction(); + const [position, setPosition] = useState<{ top: number; left: number } | null>(null); + const wrapperRef = useRef(null); + + // Track the current mode before committing to ensure it's preserved + const modeBeforeCommitRef = useRef<'redactSelection' | 'marqueeRedact' | null>(null); + + // Store current mode when menu is shown + useEffect(() => { + if (selected && state.activeType) { + modeBeforeCommitRef.current = state.activeType as 'redactSelection' | 'marqueeRedact' | null; + } + }, [selected, state.activeType]); + + // Extract ref from menuWrapperProps - must happen before any conditional returns + const { ref: wrapperPropsRef, ...restWrapperProps } = menuWrapperProps || {}; + + // Merge refs - must be called unconditionally + const mergedRef = React.useCallback((node: HTMLDivElement | null) => { + wrapperRef.current = node; + if (typeof wrapperPropsRef === 'function') { + wrapperPropsRef(node); + } else if (wrapperPropsRef) { + (wrapperPropsRef as React.MutableRefObject).current = node; + } + }, [wrapperPropsRef]); + + // Get overlay - must be called unconditionally + const overlay = typeof document !== 'undefined' ? document.getElementById('pdf-overlay-root') : null; + + // All hooks must be called before any conditional returns + useEffect(() => { + if (!selected) { + setPosition(null); + return; + } + + const updatePosition = () => { + // Get the overlay root + const overlayEl = document.getElementById('pdf-overlay-root'); + if (!overlayEl || !wrapperRef.current) { + setPosition(null); + return; + } + + // Get the wrapper's bounding rect in viewport coordinates + const wrapperRect = wrapperRef.current.getBoundingClientRect(); + const overlayRect = overlayEl.getBoundingClientRect(); + + // Calculate position relative to overlay + const menuHeight = item?.rect?.size?.height || 0; + const top = wrapperRect.top - overlayRect.top + menuHeight + 10; + const left = wrapperRect.left - overlayRect.left; + + setPosition({ top, left }); + }; + + // Initial position calculation + updatePosition(); + + // Update on scroll, resize, and zoom changes + const handleUpdate = () => { + requestAnimationFrame(updatePosition); + }; + + window.addEventListener('scroll', handleUpdate, true); + window.addEventListener('resize', handleUpdate); + + // Use a mutation observer to catch DOM changes that might affect position + const observer = new MutationObserver(handleUpdate); + if (wrapperRef.current) { + observer.observe(document.body, { childList: true, subtree: true }); + } + + // Also use intersection observer as fallback + const intersectionObserver = new IntersectionObserver( + () => handleUpdate(), + { threshold: 0, root: null } + ); + if (wrapperRef.current) { + intersectionObserver.observe(wrapperRef.current); + } + + return () => { + window.removeEventListener('scroll', handleUpdate, true); + window.removeEventListener('resize', handleUpdate); + observer.disconnect(); + intersectionObserver.disconnect(); + }; + }, [selected, item?.rect?.size?.height]); + + // Now we can conditionally render - but all hooks have been called + if (!selected) { + return ( +
+ ); + } + + const menuContent = ( +
+ + +
+ ); + + return ( + <> +
+ {overlay && position && createPortal(menuContent, overlay)} + + ); +} + + diff --git a/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx b/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx index 95153f5d2..c4ae45316 100644 --- a/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx +++ b/frontend/src/core/components/viewer/useViewerRightRailButtons.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect, useRef } from 'react'; import { ActionIcon, Popover } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useViewer } from '@app/contexts/ViewerContext'; @@ -7,18 +7,57 @@ import LocalIcon from '@app/components/shared/LocalIcon'; import { Tooltip } from '@app/components/shared/Tooltip'; import { SearchInterface } from '@app/components/viewer/SearchInterface'; import ViewerAnnotationControls from '@app/components/shared/rightRail/ViewerAnnotationControls'; +import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext'; +import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; export function useViewerRightRailButtons() { const { t } = useTranslation(); const viewer = useViewer(); - const [isPanning, setIsPanning] = useState(() => viewer.getPanState()?.isPanning ?? false); + const { actions: navActions } = useNavigationActions(); + const { handleToolSelect } = useToolWorkflow(); + const { workbench, selectedTool } = useNavigationState(); - // Lift i18n labels out of memo for clarity - const searchLabel = t('rightRail.search', 'Search PDF'); - const panLabel = t('rightRail.panMode', 'Pan Mode'); - const rotateLeftLabel = t('rightRail.rotateLeft', 'Rotate Left'); - const rotateRightLabel = t('rightRail.rotateRight', 'Rotate Right'); - const sidebarLabel = t('rightRail.toggleSidebar', 'Toggle Sidebar'); + // Extract stable references to viewer methods to avoid re-renders from context changes + const getToolModeRef = useRef(viewer.getToolMode); + const registerToolModeListenerRef = useRef(viewer.registerToolModeListener); + const unregisterToolModeListenerRef = useRef(viewer.unregisterToolModeListener); + const setAnnotationModeRef = useRef(viewer.setAnnotationMode); + const redactionActionsRef = useRef(viewer.redactionActions); + const panActionsRef = useRef(viewer.panActions); + const rotationActionsRef = useRef(viewer.rotationActions); + const toggleThumbnailSidebarRef = useRef(viewer.toggleThumbnailSidebar); + const triggerToolModeUpdateRef = useRef(viewer.triggerToolModeUpdate); + + // Update refs when viewer context changes (but don't cause re-renders) + useEffect(() => { + getToolModeRef.current = viewer.getToolMode; + registerToolModeListenerRef.current = viewer.registerToolModeListener; + unregisterToolModeListenerRef.current = viewer.unregisterToolModeListener; + setAnnotationModeRef.current = viewer.setAnnotationMode; + redactionActionsRef.current = viewer.redactionActions; + panActionsRef.current = viewer.panActions; + rotationActionsRef.current = viewer.rotationActions; + toggleThumbnailSidebarRef.current = viewer.toggleThumbnailSidebar; + triggerToolModeUpdateRef.current = viewer.triggerToolModeUpdate; + }, [viewer]); + + // Single source of truth for active tool mode (none | pan | redact | draw) + const [activeMode, setActiveMode] = useState<'none' | 'pan' | 'redact' | 'draw'>(() => getToolModeRef.current()); + useEffect(() => { + registerToolModeListenerRef.current((mode) => setActiveMode(mode)); + return () => unregisterToolModeListenerRef.current(); + }, []); // Empty deps - refs are stable + + // Memoize i18n labels to prevent re-renders + const searchLabel = useMemo(() => t('rightRail.search', 'Search PDF'), [t]); + const panLabel = useMemo(() => t('rightRail.panMode', 'Pan Mode'), [t]); + const rotateLeftLabel = useMemo(() => t('rightRail.rotateLeft', 'Rotate Left'), [t]); + const rotateRightLabel = useMemo(() => t('rightRail.rotateRight', 'Rotate Right'), [t]); + const sidebarLabel = useMemo(() => t('rightRail.toggleSidebar', 'Toggle Sidebar'), [t]); + const redactLabel = useMemo(() => t('rightRail.redact', 'Redact'), [t]); + + const isPanning = activeMode === 'pan'; + const isRedacting = activeMode === 'redact'; const viewerButtons = useMemo(() => { return [ @@ -67,8 +106,24 @@ export function useViewerRightRailButtons() { radius="md" className="right-rail-icon" onClick={() => { - viewer.panActions.togglePan(); - setIsPanning(prev => !prev); + // Entering pan should disable draw and redaction; leaving pan just toggles off + if (!isPanning) { + try { setAnnotationModeRef.current(false); } catch {} + try { redactionActionsRef.current.deactivate(); } catch {} + const enable = () => { + try { panActionsRef.current.enablePan(); } catch {} + try { triggerToolModeUpdateRef.current(); } catch {} + }; + if (typeof window !== 'undefined' && 'requestAnimationFrame' in window) { + requestAnimationFrame(() => setTimeout(enable, 0)); + } else { + setTimeout(enable, 0); + } + } else { + try { panActionsRef.current.disablePan(); } catch {} + try { triggerToolModeUpdateRef.current(); } catch {} + } + // activeMode will update via listener }} disabled={disabled} > @@ -85,7 +140,7 @@ export function useViewerRightRailButtons() { section: 'top' as const, order: 30, onClick: () => { - viewer.rotationActions.rotateBackward(); + rotationActionsRef.current.rotateBackward(); } }, { @@ -96,7 +151,7 @@ export function useViewerRightRailButtons() { section: 'top' as const, order: 40, onClick: () => { - viewer.rotationActions.rotateForward(); + rotationActionsRef.current.rotateForward(); } }, { @@ -107,9 +162,56 @@ export function useViewerRightRailButtons() { section: 'top' as const, order: 50, onClick: () => { - viewer.toggleThumbnailSidebar(); + toggleThumbnailSidebarRef.current(); } }, + { + id: 'viewer-redaction', + tooltip: redactLabel, + ariaLabel: redactLabel, + section: 'top' as const, + order: 55, + render: ({ disabled }) => ( + + { + // Ensure the left sidebar opens the Redact tool in viewer with manual mode + sessionStorage.setItem('redaction:init', 'manual'); + // Navigate to viewer with the redact tool if we're not already there + if (workbench !== 'viewer' || selectedTool !== 'redact') { + handleToolSelect('redact' as any); + } + // Disable draw and pan when activating redaction + try { setAnnotationModeRef.current(false); } catch {} + try { panActionsRef.current.disablePan(); } catch {} + // Activate last used manual mode inside viewer. + // Defer to next frame to allow annotation plugin to fully release interaction. + const last = (sessionStorage.getItem('redaction:lastManualType') as 'redactSelection' | 'marqueeRedact' | null) || 'redactSelection'; + const activate = () => { + if (last === 'marqueeRedact') { + redactionActionsRef.current.activateArea(); + } else { + redactionActionsRef.current.activateText(); + } + try { triggerToolModeUpdateRef.current(); } catch {} + }; + if (typeof window !== 'undefined' && 'requestAnimationFrame' in window) { + requestAnimationFrame(() => setTimeout(activate, 0)); + } else { + setTimeout(activate, 0); + } + }} + disabled={disabled} + > + + + + ) + }, { id: 'viewer-annotation-controls', section: 'top' as const, @@ -119,7 +221,7 @@ export function useViewerRightRailButtons() { ) } ]; - }, [t, viewer, isPanning, searchLabel, panLabel, rotateLeftLabel, rotateRightLabel, sidebarLabel]); + }, [activeMode, searchLabel, panLabel, rotateLeftLabel, rotateRightLabel, sidebarLabel, redactLabel, handleToolSelect, workbench, selectedTool, isPanning, isRedacting]); useRightRailButtons(viewerButtons); } diff --git a/frontend/src/core/contexts/ViewerContext.tsx b/frontend/src/core/contexts/ViewerContext.tsx index 8e0bea44a..ef7dfdde0 100644 --- a/frontend/src/core/contexts/ViewerContext.tsx +++ b/frontend/src/core/contexts/ViewerContext.tsx @@ -57,6 +57,19 @@ interface ExportAPIWrapper { saveAsCopy: () => { toPromise: () => Promise }; } +// Redaction bridge wrappers +interface RedactionAPIWrapper { + toggleRedactSelection: () => void; + toggleMarqueeRedact: () => void; + clearPending: () => void; + commitAllPending: () => { toPromise: () => Promise } | Promise | void; +} + +interface RedactionState { + isRedacting: boolean; + activeType: 'redactSelection' | 'marqueeRedact' | null; + pendingCount: number; +} // State interfaces - represent the shape of data from each bridge interface ScrollState { @@ -103,6 +116,8 @@ interface ExportState { canExport: boolean; } +type ToolMode = 'none' | 'pan' | 'redact' | 'draw'; + // Bridge registration interface - bridges register with state and API interface BridgeRef { state: TState; @@ -146,6 +161,8 @@ interface ViewerContextType { getSearchState: () => SearchState; getThumbnailAPI: () => ThumbnailAPIWrapper | null; getExportState: () => ExportState; + getToolMode: () => ToolMode; + getRedactionState: () => RedactionState; // Immediate update callbacks registerImmediateZoomUpdate: (callback: (percent: number) => void) => void; @@ -208,6 +225,20 @@ interface ViewerContextType { saveAsCopy: () => Promise; }; + redactionActions: { + activateText: () => void; + activateArea: () => void; + deactivate: () => void; + commitAllPending: () => Promise; + clearPending: () => void; + isActive: () => boolean; + }; + + // Live updates for right-rail highlighting + registerToolModeListener: (callback: (mode: ToolMode) => void) => void; + unregisterToolModeListener: () => void; + triggerToolModeUpdate: () => void; + // Bridge registration - internal use by bridges registerBridge: (type: string, ref: BridgeRef) => void; } @@ -239,8 +270,11 @@ export const ViewerProvider: React.FC = ({ children }) => { rotation: null as BridgeRef | null, thumbnail: null as BridgeRef | null, export: null as BridgeRef | null, + redaction: null as BridgeRef | null, }); + const toolModeListenerRef = useRef<((mode: ToolMode) => void) | null>(null); + // Immediate zoom callback for responsive display updates const immediateZoomUpdateCallback = useRef<((percent: number) => void) | null>(null); @@ -277,6 +311,9 @@ export const ViewerProvider: React.FC = ({ children }) => { case 'export': bridgeRefs.current.export = ref as BridgeRef; break; + case 'redaction': + bridgeRefs.current.redaction = ref as BridgeRef; + break; } }; @@ -290,10 +327,14 @@ export const ViewerProvider: React.FC = ({ children }) => { const setAnnotationMode = (enabled: boolean) => { setIsAnnotationModeState(enabled); + // Notify listeners when draw mode changes + triggerToolModeUpdate(); }; const toggleAnnotationMode = () => { setIsAnnotationModeState(prev => !prev); + // Notify listeners when draw mode changes + setTimeout(() => triggerToolModeUpdate(), 0); }; // State getters - read from bridge refs @@ -333,6 +374,25 @@ export const ViewerProvider: React.FC = ({ children }) => { return bridgeRefs.current.export?.state || { canExport: false }; }; + const getToolMode = (): ToolMode => { + if (isAnnotationMode) return 'draw'; + const redactionActive = bridgeRefs.current.redaction?.state?.isRedacting; + if (redactionActive) return 'redact'; + const panActive = bridgeRefs.current.pan?.state?.isPanning; + if (panActive) return 'pan'; + return 'none'; + }; + + const getRedactionState = (): RedactionState => { + return ( + bridgeRefs.current.redaction?.state || { + isRedacting: false, + activeType: null, + pendingCount: 0, + } + ); + }; + // Action handlers - call APIs directly const scrollActions = { scrollToPage: (page: number) => { @@ -550,6 +610,66 @@ export const ViewerProvider: React.FC = ({ children }) => { } }; + // Track redaction dirty state (any commit or pending marks) + const redactionActions = { + activateText: () => { + const api = bridgeRefs.current.redaction?.api; + const state = bridgeRefs.current.redaction?.state; + if (!api) return; + if (state?.activeType === 'redactSelection') return; // already active + // If other mode active, turn it off first + if (state?.activeType === 'marqueeRedact' && api.toggleMarqueeRedact) api.toggleMarqueeRedact(); + if (api.toggleRedactSelection) api.toggleRedactSelection(); + }, + activateArea: () => { + const api = bridgeRefs.current.redaction?.api; + const state = bridgeRefs.current.redaction?.state; + if (!api) return; + if (state?.activeType === 'marqueeRedact') return; // already active + if (state?.activeType === 'redactSelection' && api.toggleRedactSelection) api.toggleRedactSelection(); + if (api.toggleMarqueeRedact) api.toggleMarqueeRedact(); + }, + deactivate: () => { + const state = bridgeRefs.current.redaction?.state; + const api = bridgeRefs.current.redaction?.api; + if (!state || !api) return; + // If text is active, toggling text will deactivate; same for area + if (state.activeType === 'redactSelection' && api.toggleRedactSelection) { + api.toggleRedactSelection(); + } else if (state.activeType === 'marqueeRedact' && api.toggleMarqueeRedact) { + api.toggleMarqueeRedact(); + } + }, + commitAllPending: async () => { + const api = bridgeRefs.current.redaction?.api; + if (!api?.commitAllPending) return; + const result = api.commitAllPending(); + if (result && typeof (result as any).toPromise === 'function') { + await (result as any).toPromise(); + } + }, + clearPending: () => { + const api = bridgeRefs.current.redaction?.api; + if (api?.clearPending) api.clearPending(); + }, + isActive: () => { + return Boolean(bridgeRefs.current.redaction?.state?.isRedacting); + }, + }; + + const registerToolModeListener = (callback: (mode: ToolMode) => void) => { + toolModeListenerRef.current = callback; + }; + + const unregisterToolModeListener = () => { + toolModeListenerRef.current = null; + }; + + const triggerToolModeUpdate = () => { + const cb = toolModeListenerRef.current; + if (cb) cb(getToolMode()); + }; + const registerImmediateZoomUpdate = (callback: (percent: number) => void) => { immediateZoomUpdateCallback.current = callback; }; @@ -596,6 +716,8 @@ export const ViewerProvider: React.FC = ({ children }) => { getSearchState, getThumbnailAPI, getExportState, + getToolMode, + getRedactionState, // Immediate updates registerImmediateZoomUpdate, @@ -612,6 +734,10 @@ export const ViewerProvider: React.FC = ({ children }) => { rotationActions, searchActions, exportActions, + redactionActions, + registerToolModeListener, + unregisterToolModeListener, + triggerToolModeUpdate, // Bridge registration registerBridge, diff --git a/frontend/src/core/hooks/tools/redact/useRedactOperation.ts b/frontend/src/core/hooks/tools/redact/useRedactOperation.ts index bf2a05121..63a9cb20f 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactOperation.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactOperation.ts @@ -17,8 +17,9 @@ export const buildRedactFormData = (parameters: RedactParameters, file: File): F formData.append("customPadding", parameters.customPadding.toString()); formData.append("convertPDFToImage", parameters.convertPDFToImage.toString()); } else { - // Manual mode parameters would go here when implemented - throw new Error('Manual redaction not yet implemented'); + // Manual mode is handled interactively in the viewer via EmbedPDF redaction plugin. + // The sidebar tool never posts a request in manual mode, so return minimal form data. + formData.append('mode', 'manual'); } return formData; @@ -33,8 +34,8 @@ export const redactOperationConfig = { if (parameters.mode === 'automatic') { return '/api/v1/security/auto-redact'; } else { - // Manual redaction endpoint would go here when implemented - throw new Error('Manual redaction not yet implemented'); + // Manual mode does not call backend; return a placeholder that will not be invoked. + return '/noop/manual-redaction'; } }, defaultParameters, diff --git a/frontend/src/core/hooks/tools/redact/useRedactParameters.ts b/frontend/src/core/hooks/tools/redact/useRedactParameters.ts index f29f56f96..37ecaa312 100644 --- a/frontend/src/core/hooks/tools/redact/useRedactParameters.ts +++ b/frontend/src/core/hooks/tools/redact/useRedactParameters.ts @@ -34,15 +34,15 @@ export const useRedactParameters = (): RedactParametersHook => { if (params.mode === 'automatic') { return '/api/v1/security/auto-redact'; } - // Manual redaction endpoint would go here when implemented - throw new Error('Manual redaction not yet implemented'); + // Manual mode is handled in the viewer; this endpoint will not be called + return '/noop/manual-redaction'; }, validateFn: (params) => { if (params.mode === 'automatic') { return params.wordsToRedact.length > 0 && params.wordsToRedact.some(word => word.trim().length > 0); } - // Manual mode validation would go here when implemented - return false; + // For manual, validation is not needed for network calls; allow switching + return true; } }); }; diff --git a/frontend/src/core/tools/Redact.tsx b/frontend/src/core/tools/Redact.tsx index 27604da0b..d4d6774bc 100644 --- a/frontend/src/core/tools/Redact.tsx +++ b/frontend/src/core/tools/Redact.tsx @@ -1,14 +1,19 @@ import { useTranslation } from "react-i18next"; -import { useState } from "react"; +import { useState, useEffect, useRef, useMemo, useCallback } from "react"; import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; import RedactModeSelector from "@app/components/tools/redact/RedactModeSelector"; import { useRedactParameters } from "@app/hooks/tools/redact/useRedactParameters"; import { useRedactOperation } from "@app/hooks/tools/redact/useRedactOperation"; import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "@app/types/tool"; -import { useRedactModeTips, useRedactWordsTips, useRedactAdvancedTips } from "@app/components/tooltips/useRedactTips"; +import { useRedactModeTips, useRedactWordsTips, useRedactAdvancedTips, useRedactManualTips } from "@app/components/tooltips/useRedactTips"; import RedactAdvancedSettings from "@app/components/tools/redact/RedactAdvancedSettings"; import WordsToRedactInput from "@app/components/tools/redact/WordsToRedactInput"; +import RedactManualControls, { ManualRedactionType } from "@app/components/tools/redact/RedactManualControls"; +import { useNavigationActions, useNavigationState } from "@app/contexts/NavigationContext"; +import { useViewer } from "@app/contexts/ViewerContext"; +import { Stack, Alert, Button, Text } from "@mantine/core"; +import WarningIcon from '@mui/icons-material/Warning'; const Redact = (props: BaseToolProps) => { const { t } = useTranslation(); @@ -29,6 +34,110 @@ const Redact = (props: BaseToolProps) => { const modeTips = useRedactModeTips(); const wordsTips = useRedactWordsTips(); const advancedTips = useRedactAdvancedTips(); + const manualTips = useRedactManualTips(); + + // Navigation for switching to viewer on Manual + const { actions: navActions } = useNavigationActions(); + const { workbench } = useNavigationState(); + const viewer = useViewer(); + + // Extract stable references to viewer methods to avoid re-renders from context changes + const getRedactionStateRef = useRef(viewer.getRedactionState); + const registerToolModeListenerRef = useRef(viewer.registerToolModeListener); + const unregisterToolModeListenerRef = useRef(viewer.unregisterToolModeListener); + const redactionActionsRef = useRef(viewer.redactionActions); + const setAnnotationModeRef = useRef(viewer.setAnnotationMode); + const panActionsRef = useRef(viewer.panActions); + const triggerToolModeUpdateRef = useRef(viewer.triggerToolModeUpdate); + + // Update refs when viewer context changes (but don't cause re-renders) + useEffect(() => { + getRedactionStateRef.current = viewer.getRedactionState; + registerToolModeListenerRef.current = viewer.registerToolModeListener; + unregisterToolModeListenerRef.current = viewer.unregisterToolModeListener; + redactionActionsRef.current = viewer.redactionActions; + setAnnotationModeRef.current = viewer.setAnnotationMode; + panActionsRef.current = viewer.panActions; + triggerToolModeUpdateRef.current = viewer.triggerToolModeUpdate; + }, [viewer]); + + // Force re-render when tool mode changes to ensure buttons reflect current state + const [updateCounter, setUpdateCounter] = useState(0); + useEffect(() => { + const handleToolModeUpdate = () => { + setUpdateCounter(prev => prev + 1); + }; + registerToolModeListenerRef.current(handleToolModeUpdate); + return () => { + unregisterToolModeListenerRef.current(); + }; + }, []); // Empty deps - refs are stable + + // Track redaction state to ensure UI stays in sync with plugin state + // Use a ref to track the last known state and only update when it changes + const [redactionStateCheck, setRedactionStateCheck] = useState(0); + const lastRedactionStateRef = useRef<{ activeType: string | null; pendingCount: number } | null>(null); + + useEffect(() => { + if (base.params.parameters.mode !== 'manual') return; + + // Check redaction state periodically, but only update if it actually changed + const interval = setInterval(() => { + const currentState = getRedactionStateRef.current(); + const lastState = lastRedactionStateRef.current; + + // Only trigger update if activeType changed (not just pendingCount) + if (!lastState || lastState.activeType !== currentState.activeType) { + lastRedactionStateRef.current = { + activeType: currentState.activeType, + pendingCount: currentState.pendingCount, + }; + setRedactionStateCheck(prev => prev + 1); + } else { + // Update ref even if we don't trigger re-render + lastRedactionStateRef.current = { + activeType: currentState.activeType, + pendingCount: currentState.pendingCount, + }; + } + }, 500); // Check less frequently - only when mode actually changes + + return () => clearInterval(interval); + }, [base.params.parameters.mode]); // Removed viewer dependency + + // Check if we need to show the viewer warning + const isManualModeOutsideViewer = base.params.parameters.mode === 'manual' && workbench !== 'viewer'; + + const handleEnterManual = useCallback(() => { + // Mark that manual redaction should be initialized and activate last mode + sessionStorage.setItem('redaction:init', 'manual'); + // Persist current choice if any + const last = (sessionStorage.getItem('redaction:lastManualType') as ManualRedactionType | null) || 'redactSelection'; + sessionStorage.setItem('redaction:lastManualType', last); + // Switch to viewer and show the Redact tool in sidebar + navActions.setToolAndWorkbench('redact' as any, 'viewer'); + // Defer activation to ensure viewer is ready and other tools are disabled + const activate = () => { + try { setAnnotationModeRef.current(false); } catch {} + try { panActionsRef.current.disablePan(); } catch {} + const activateRedaction = () => { + if (last === 'marqueeRedact') { + redactionActionsRef.current.activateArea(); + } else { + redactionActionsRef.current.activateText(); + } + try { triggerToolModeUpdateRef.current(); } catch {} + }; + // Use double deferral to ensure state is settled + if (typeof window !== 'undefined' && 'requestAnimationFrame' in window) { + requestAnimationFrame(() => setTimeout(activateRedaction, 0)); + } else { + setTimeout(activateRedaction, 0); + } + }; + // Defer activation slightly to allow navigation to complete + setTimeout(activate, 50); + }, [navActions]); const isExecuteDisabled = () => { if (base.params.parameters.mode === 'manual') { @@ -38,12 +147,20 @@ const Redact = (props: BaseToolProps) => { }; // Compute actual collapsed state based on results and user state - const getActualCollapsedState = (userCollapsed: boolean) => { + const getActualCollapsedState = useCallback((userCollapsed: boolean) => { return (!base.hasFiles || base.hasResults) ? true : userCollapsed; // Force collapse when results are shown - }; + }, [base.hasFiles, base.hasResults]); + + // Memoize redaction state to avoid calling getRedactionState on every render + const redactionState = useMemo(() => { + if (base.params.parameters.mode !== 'manual') return null; + // Reference check counter to ensure re-evaluation when it changes + void redactionStateCheck; + return getRedactionStateRef.current(); + }, [base.params.parameters.mode, redactionStateCheck]); // Build conditional steps based on redaction mode - const buildSteps = () => { + const buildSteps = useCallback(() => { const steps = [ // Method selection step (always present) { @@ -52,11 +169,41 @@ const Redact = (props: BaseToolProps) => { onCollapsedClick: () => base.settingsCollapsed ? base.handleSettingsReset() : setMethodCollapsed(!methodCollapsed), tooltip: modeTips, content: ( - base.params.updateParameter('mode', mode)} - disabled={base.endpointLoading} - /> + + { + base.params.updateParameter('mode', mode); + if (mode === 'manual') { + handleEnterManual(); + } + }} + disabled={base.endpointLoading} + /> + {isManualModeOutsideViewer && ( + } + > + + + {t("redact.manual.viewerWarning.message", "Manual redaction can only be used in the viewer view. Please switch to the viewer to use this feature.")} + + + + + )} + ), } ]; @@ -88,11 +235,60 @@ const Redact = (props: BaseToolProps) => { }, ); } else if (base.params.parameters.mode === 'manual') { - // Manual mode steps would go here when implemented + steps.push({ + title: t("redact.manual.settings.title", "Manual Redaction"), + isCollapsed: getActualCollapsedState(false), + onCollapsedClick: () => {}, + tooltip: manualTips, + content: ( + { + sessionStorage.setItem('redaction:lastManualType', val); + // Ensure we're in viewer and activate chosen tool + handleEnterManual(); + if (val === 'marqueeRedact') redactionActionsRef.current.activateArea(); + else redactionActionsRef.current.activateText(); + // Trigger a state check to update UI + setRedactionStateCheck(prev => prev + 1); + }} + disabled={base.endpointLoading} + /> + ) + }); } return steps; - }; + }, [ + base.params.parameters, // Required for RedactAdvancedSettings prop + base.hasFiles, + base.hasResults, + base.endpointLoading, + base.params.updateParameter, + methodCollapsed, + wordsCollapsed, + advancedCollapsed, + modeTips, + wordsTips, + advancedTips, + manualTips, + isManualModeOutsideViewer, + handleEnterManual, + redactionState, + t, + getActualCollapsedState, + base.settingsCollapsed, + base.handleSettingsReset, + setMethodCollapsed, + setWordsCollapsed, + setAdvancedCollapsed, + ]); return createToolFlow({ files: {