mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			feature-in
			...
			4e0f5dff95
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 4e0f5dff95 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 10ccccc987 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 27d72ebb18 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 909ccebb34 | ||
|   | 4275e18c10 | ||
|   | 0088333360 | ||
|   | ed1d488d6e | ||
|   | b25b15ba32 | 
| @@ -1,11 +1,10 @@ | ||||
| { | ||||
|     "python.testing.pytestArgs": [ | ||||
|         "src" | ||||
|     ], | ||||
|     "python.testing.pytestArgs": [], | ||||
|     "python.testing.unittestEnabled": false, | ||||
|     "python.testing.pytestEnabled": true, | ||||
|     "files.watcherExclude": { | ||||
|         "**/.venv/**": true, | ||||
|         "**/pytest_cache/**": true | ||||
|     } | ||||
|     }, | ||||
|     "python.testing.cwd": "${workspaceFolder}/src" | ||||
| } | ||||
|   | ||||
| @@ -32,7 +32,7 @@ RUN set -eux \ | ||||
| # Purpose: Installs s6-overlay and rootfs | ||||
| # Comments: | ||||
| #  - Don't leave anything extra in here either | ||||
| FROM ghcr.io/astral-sh/uv:0.8.8-python3.12-bookworm-slim AS s6-overlay-base | ||||
| FROM ghcr.io/astral-sh/uv:0.8.13-python3.12-bookworm-slim AS s6-overlay-base | ||||
|  | ||||
| WORKDIR /usr/src/s6 | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| # correct networking for the tests | ||||
| services: | ||||
|   gotenberg: | ||||
|     image: docker.io/gotenberg/gotenberg:8.20 | ||||
|     image: docker.io/gotenberg/gotenberg:8.22 | ||||
|     hostname: gotenberg | ||||
|     container_name: gotenberg | ||||
|     network_mode: host | ||||
|   | ||||
| @@ -35,7 +35,7 @@ services: | ||||
|     volumes: | ||||
|       - redisdata:/data | ||||
|   db: | ||||
|     image: docker.io/library/mariadb:11 | ||||
|     image: docker.io/library/mariadb:12 | ||||
|     restart: unless-stopped | ||||
|     volumes: | ||||
|       - dbdata:/var/lib/mysql | ||||
| @@ -72,7 +72,7 @@ services: | ||||
|       PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 | ||||
|       PAPERLESS_TIKA_ENDPOINT: http://tika:9998 | ||||
|   gotenberg: | ||||
|     image: docker.io/gotenberg/gotenberg:8.20 | ||||
|     image: docker.io/gotenberg/gotenberg:8.22 | ||||
|     restart: unless-stopped | ||||
|     # The gotenberg chromium route is used to convert .eml files. We do not | ||||
|     # want to allow external content like tracking pixels or even javascript. | ||||
|   | ||||
| @@ -31,7 +31,7 @@ services: | ||||
|     volumes: | ||||
|       - redisdata:/data | ||||
|   db: | ||||
|     image: docker.io/library/mariadb:11 | ||||
|     image: docker.io/library/mariadb:12 | ||||
|     restart: unless-stopped | ||||
|     volumes: | ||||
|       - dbdata:/var/lib/mysql | ||||
|   | ||||
| @@ -66,7 +66,7 @@ services: | ||||
|       PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 | ||||
|       PAPERLESS_TIKA_ENDPOINT: http://tika:9998 | ||||
|   gotenberg: | ||||
|     image: docker.io/gotenberg/gotenberg:8.20 | ||||
|     image: docker.io/gotenberg/gotenberg:8.22 | ||||
|     restart: unless-stopped | ||||
|     # The gotenberg chromium route is used to convert .eml files. We do not | ||||
|     # want to allow external content like tracking pixels or even javascript. | ||||
|   | ||||
| @@ -55,7 +55,7 @@ services: | ||||
|       PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 | ||||
|       PAPERLESS_TIKA_ENDPOINT: http://tika:9998 | ||||
|   gotenberg: | ||||
|     image: docker.io/gotenberg/gotenberg:8.20 | ||||
|     image: docker.io/gotenberg/gotenberg:8.22 | ||||
|     restart: unless-stopped | ||||
|     # The gotenberg chromium route is used to convert .eml files. We do not | ||||
|     # want to allow external content like tracking pixels or even javascript. | ||||
|   | ||||
| @@ -2544,11 +2544,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1011</context> | ||||
|           <context context-type="linenumber">1017</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1373</context> | ||||
|           <context context-type="linenumber">1382</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||
| @@ -3156,7 +3156,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">964</context> | ||||
|           <context context-type="linenumber">970</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||
| @@ -6579,7 +6579,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1372</context> | ||||
|           <context context-type="linenumber">1381</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6490688569532630280" datatype="html"> | ||||
| @@ -6869,56 +6869,56 @@ | ||||
|         <source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">412,414</context> | ||||
|           <context context-type="linenumber">410,412</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3200733026060976258" datatype="html"> | ||||
|         <source>Document changes detected</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">435</context> | ||||
|           <context context-type="linenumber">444</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2887155916749964" datatype="html"> | ||||
|         <source>The version of this document in your browser session appears older than the existing version.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">436</context> | ||||
|           <context context-type="linenumber">445</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="237142428785956348" datatype="html"> | ||||
|         <source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">437</context> | ||||
|           <context context-type="linenumber">446</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8720977247725652816" datatype="html"> | ||||
|         <source>Ok</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">439</context> | ||||
|           <context context-type="linenumber">448</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6142395741265832184" datatype="html"> | ||||
|         <source>Next document</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">563</context> | ||||
|           <context context-type="linenumber">573</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="651985345816518480" datatype="html"> | ||||
|         <source>Previous document</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">573</context> | ||||
|           <context context-type="linenumber">583</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2885986061416655600" datatype="html"> | ||||
|         <source>Close document</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">581</context> | ||||
|           <context context-type="linenumber">591</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/services/open-documents.service.ts</context> | ||||
| @@ -6929,67 +6929,67 @@ | ||||
|         <source>Save document</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">588</context> | ||||
|           <context context-type="linenumber">598</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1784543155727940353" datatype="html"> | ||||
|         <source>Save and close / next</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">597</context> | ||||
|           <context context-type="linenumber">607</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5758784066858623886" datatype="html"> | ||||
|         <source>Error retrieving metadata</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">649</context> | ||||
|           <context context-type="linenumber">659</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3456881259945295697" datatype="html"> | ||||
|         <source>Error retrieving suggestions.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">678</context> | ||||
|           <context context-type="linenumber">688</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2194092841814123758" datatype="html"> | ||||
|         <source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">858</context> | ||||
|           <context context-type="linenumber">860</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">882</context> | ||||
|           <context context-type="linenumber">884</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6626387786259219838" datatype="html"> | ||||
|         <source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">888</context> | ||||
|           <context context-type="linenumber">890</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="448882439049417053" datatype="html"> | ||||
|         <source>Error saving document</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">933</context> | ||||
|           <context context-type="linenumber">939</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8410796510716511826" datatype="html"> | ||||
|         <source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">965</context> | ||||
|           <context context-type="linenumber">971</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="282586936710748252" datatype="html"> | ||||
|         <source>Documents can be restored prior to permanent deletion.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">966</context> | ||||
|           <context context-type="linenumber">972</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||
| @@ -7000,7 +7000,7 @@ | ||||
|         <source>Move to trash</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">968</context> | ||||
|           <context context-type="linenumber">974</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||
| @@ -7011,14 +7011,14 @@ | ||||
|         <source>Error deleting document</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">987</context> | ||||
|           <context context-type="linenumber">993</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="619486176823357521" datatype="html"> | ||||
|         <source>Reprocess confirm</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1007</context> | ||||
|           <context context-type="linenumber">1013</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||
| @@ -7029,67 +7029,67 @@ | ||||
|         <source>This operation will permanently recreate the archive file for this document.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1008</context> | ||||
|           <context context-type="linenumber">1014</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="302054111564709516" datatype="html"> | ||||
|         <source>The archive file will be re-generated with the current settings.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1009</context> | ||||
|           <context context-type="linenumber">1015</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8251197608401006898" datatype="html"> | ||||
|         <source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1019</context> | ||||
|           <context context-type="linenumber">1025</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4409560272830824468" datatype="html"> | ||||
|         <source>Error executing operation</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1030</context> | ||||
|           <context context-type="linenumber">1036</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6030453331794586802" datatype="html"> | ||||
|         <source>Error downloading document</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1079</context> | ||||
|           <context context-type="linenumber">1085</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4458954481601077369" datatype="html"> | ||||
|         <source>Page Fit</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1156</context> | ||||
|           <context context-type="linenumber">1162</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4663705961777238777" datatype="html"> | ||||
|         <source>PDF edit operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1391</context> | ||||
|           <context context-type="linenumber">1400</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="9043972994040261999" datatype="html"> | ||||
|         <source>Error executing PDF edit operation</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1403</context> | ||||
|           <context context-type="linenumber">1412</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6085793215710522488" datatype="html"> | ||||
|         <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1463</context> | ||||
|           <context context-type="linenumber">1479</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||
|           <context context-type="linenumber">1467</context> | ||||
|           <context context-type="linenumber">1483</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4958946940233632319" datatype="html"> | ||||
| @@ -7671,7 +7671,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">311</context> | ||||
|           <context context-type="linenumber">313</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1494518490116523821" datatype="html"> | ||||
| @@ -7682,7 +7682,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">304</context> | ||||
|           <context context-type="linenumber">306</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8461842260159597706" datatype="html"> | ||||
| @@ -7925,49 +7925,49 @@ | ||||
|         <source>Reset filters / selection</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">292</context> | ||||
|           <context context-type="linenumber">294</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4135055128446167640" datatype="html"> | ||||
|         <source>Open first [selected] document</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">320</context> | ||||
|           <context context-type="linenumber">322</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3629960544875360046" datatype="html"> | ||||
|         <source>Previous page</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">336</context> | ||||
|           <context context-type="linenumber">338</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3337301694210287595" datatype="html"> | ||||
|         <source>Next page</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">348</context> | ||||
|           <context context-type="linenumber">350</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2155249406916744630" datatype="html"> | ||||
|         <source>View "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>" saved successfully.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">381</context> | ||||
|           <context context-type="linenumber">383</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4646273665293421938" datatype="html"> | ||||
|         <source>Failed to save view "<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>".</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">387</context> | ||||
|           <context context-type="linenumber">389</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6837554170707123455" datatype="html"> | ||||
|         <source>View "<x id="PH" equiv-text="savedView.name"/>" created successfully.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||
|           <context context-type="linenumber">431</context> | ||||
|           <context context-type="linenumber">435</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="739880801667335279" datatype="html"> | ||||
|   | ||||
| @@ -106,6 +106,7 @@ describe('DashboardComponent', () => { | ||||
|               }), | ||||
|             dashboardViews: saved_views.filter((v) => v.show_on_dashboard), | ||||
|             allViews: saved_views, | ||||
|             setDocumentCount: jest.fn(), | ||||
|           }, | ||||
|         }, | ||||
|         provideHttpClient(withInterceptorsFromDi()), | ||||
|   | ||||
| @@ -52,6 +52,7 @@ import { | ||||
| } from 'src/app/services/permissions.service' | ||||
| import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' | ||||
| import { DocumentService } from 'src/app/services/rest/document.service' | ||||
| import { SavedViewService } from 'src/app/services/rest/saved-view.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { WebsocketStatusService } from 'src/app/services/websocket-status.service' | ||||
| import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' | ||||
| @@ -94,6 +95,7 @@ export class SavedViewWidgetComponent | ||||
|   permissionsService = inject(PermissionsService) | ||||
|   private settingsService = inject(SettingsService) | ||||
|   private customFieldService = inject(CustomFieldsService) | ||||
|   private savedViewService = inject(SavedViewService) | ||||
|  | ||||
|   public DisplayMode = DisplayMode | ||||
|   public DisplayField = DisplayField | ||||
| @@ -181,6 +183,7 @@ export class SavedViewWidgetComponent | ||||
|           this.show = true | ||||
|           this.documents = result.results | ||||
|           this.count = result.count | ||||
|           this.savedViewService.setDocumentCount(this.savedView, result.count) | ||||
|         }), | ||||
|         delay(500) | ||||
|       ) | ||||
|   | ||||
| @@ -1,9 +1,4 @@ | ||||
| <pngx-page-header [(title)]="title"> | ||||
|   @if (document?.in_process) { | ||||
|     <span class="badge bg-danger text-dark ms-2 d-flex align-items-center"> | ||||
|       <div class="spinner-border spinner-border-sm me-1" role="status"></div><span i18n>Processing...</span> | ||||
|     </span> | ||||
|   } | ||||
|   @if (archiveContentRenderType === ContentRenderType.PDF && !useNativePdfViewer) { | ||||
|     @if (previewNumPages) { | ||||
|       <div class="input-group input-group-sm d-none d-md-flex"> | ||||
| @@ -55,7 +50,7 @@ | ||||
|       <div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div> | ||||
|     </button> | ||||
|     <div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow"> | ||||
|       <button ngbDropdownItem (click)="reprocess()" [disabled]="!userCanEdit || !userIsOwner || document?.in_process"> | ||||
|       <button ngbDropdownItem (click)="reprocess()" [disabled]="!userCanEdit || !userIsOwner"> | ||||
|         <i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs> <span i18n>Reprocess</span> | ||||
|       </button> | ||||
|  | ||||
| @@ -63,7 +58,7 @@ | ||||
|         <i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span> | ||||
|       </button> | ||||
|  | ||||
|       <button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF || document?.in_process"> | ||||
|       <button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF"> | ||||
|         <i-bs name="pencil"></i-bs> <ng-container i18n>PDF Editor</ng-container> | ||||
|       </button> | ||||
|     </div> | ||||
| @@ -95,6 +90,7 @@ | ||||
|       } | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
| </pngx-page-header> | ||||
|  | ||||
| <div class="row"> | ||||
|   | ||||
| @@ -21,8 +21,9 @@ import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' | ||||
| import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer' | ||||
| import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | ||||
| import { DeviceDetectorService } from 'ngx-device-detector' | ||||
| import { BehaviorSubject, Observable, Subject } from 'rxjs' | ||||
| import { BehaviorSubject, Observable, of, Subject } from 'rxjs' | ||||
| import { | ||||
|   catchError, | ||||
|   debounceTime, | ||||
|   distinctUntilChanged, | ||||
|   filter, | ||||
| @@ -327,19 +328,163 @@ export class DocumentDetailComponent | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private mapDocToForm(doc: Document): any { | ||||
|     return { | ||||
|       ...doc, | ||||
|       permissions_form: { owner: doc.owner, set_permissions: doc.permissions }, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private mapFormToDoc(value: any): any { | ||||
|     const docValues = { ...value } | ||||
|     docValues['owner'] = value['permissions_form']?.owner | ||||
|     docValues['set_permissions'] = value['permissions_form']?.set_permissions | ||||
|     delete docValues['permissions_form'] | ||||
|     return docValues | ||||
|   } | ||||
|  | ||||
|   private prepareForm(doc: Document): void { | ||||
|     this.documentForm.reset(this.mapDocToForm(doc), { emitEvent: false }) | ||||
|     if (!this.userCanEditDoc(doc)) { | ||||
|       this.documentForm.disable({ emitEvent: false }) | ||||
|     } else { | ||||
|       this.documentForm.enable({ emitEvent: false }) | ||||
|     } | ||||
|     if (doc.__changedFields) { | ||||
|       doc.__changedFields.forEach((field) => { | ||||
|         if (field === 'owner' || field === 'set_permissions') { | ||||
|           this.documentForm.get('permissions_form')?.markAsDirty() | ||||
|         } else { | ||||
|           this.documentForm.get(field)?.markAsDirty() | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private setupDirtyTracking( | ||||
|     currentDocument: Document, | ||||
|     originalDocument: Document | ||||
|   ): void { | ||||
|     this.store = new BehaviorSubject({ | ||||
|       title: originalDocument.title, | ||||
|       content: originalDocument.content, | ||||
|       created: originalDocument.created, | ||||
|       correspondent: originalDocument.correspondent, | ||||
|       document_type: originalDocument.document_type, | ||||
|       storage_path: originalDocument.storage_path, | ||||
|       archive_serial_number: originalDocument.archive_serial_number, | ||||
|       tags: [...originalDocument.tags], | ||||
|       permissions_form: { | ||||
|         owner: originalDocument.owner, | ||||
|         set_permissions: originalDocument.permissions, | ||||
|       }, | ||||
|       custom_fields: [...originalDocument.custom_fields], | ||||
|     }) | ||||
|     this.isDirty$ = dirtyCheck(this.documentForm, this.store.asObservable()) | ||||
|     this.isDirty$ | ||||
|       .pipe( | ||||
|         takeUntil(this.unsubscribeNotifier), | ||||
|         takeUntil(this.docChangeNotifier) | ||||
|       ) | ||||
|       .subscribe((dirty) => | ||||
|         this.openDocumentService.setDirty( | ||||
|           currentDocument, | ||||
|           dirty, | ||||
|           this.getChangedFields() | ||||
|         ) | ||||
|       ) | ||||
|   } | ||||
|  | ||||
|   private loadDocument(documentId: number): void { | ||||
|     this.previewUrl = this.documentsService.getPreviewUrl(documentId) | ||||
|     this.http | ||||
|       .get(this.previewUrl, { responseType: 'text' }) | ||||
|       .pipe( | ||||
|         first(), | ||||
|         takeUntil(this.unsubscribeNotifier), | ||||
|         takeUntil(this.docChangeNotifier) | ||||
|       ) | ||||
|       .subscribe({ | ||||
|         next: (res) => (this.previewText = res.toString()), | ||||
|         error: (err) => | ||||
|           (this.previewText = $localize`An error occurred loading content: ${ | ||||
|             err.message ?? err.toString() | ||||
|           }`), | ||||
|       }) | ||||
|     this.thumbUrl = this.documentsService.getThumbUrl(documentId) | ||||
|     this.documentsService | ||||
|       .get(documentId) | ||||
|       .pipe( | ||||
|         catchError(() => { | ||||
|           // 404 is handled in the subscribe below | ||||
|           return of(null) | ||||
|         }), | ||||
|         first(), | ||||
|         takeUntil(this.unsubscribeNotifier), | ||||
|         takeUntil(this.docChangeNotifier) | ||||
|       ) | ||||
|       .subscribe({ | ||||
|         next: (doc) => { | ||||
|           if (!doc) { | ||||
|             this.router.navigate(['404'], { replaceUrl: true }) | ||||
|             return | ||||
|           } | ||||
|           this.documentId = doc.id | ||||
|           this.suggestions = null | ||||
|           const openDocument = this.openDocumentService.getOpenDocument( | ||||
|             this.documentId | ||||
|           ) | ||||
|           const useDoc = openDocument || doc | ||||
|           if (openDocument) { | ||||
|             if ( | ||||
|               new Date(doc.modified) > new Date(openDocument.modified) && | ||||
|               !this.modalService.hasOpenModals() | ||||
|             ) { | ||||
|               const modal = this.modalService.open(ConfirmDialogComponent) | ||||
|               modal.componentInstance.title = $localize`Document changes detected` | ||||
|               modal.componentInstance.messageBold = $localize`The version of this document in your browser session appears older than the existing version.` | ||||
|               modal.componentInstance.message = $localize`Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.` | ||||
|               modal.componentInstance.cancelBtnClass = 'visually-hidden' | ||||
|               modal.componentInstance.btnCaption = $localize`Ok` | ||||
|               modal.componentInstance.confirmClicked.subscribe(() => | ||||
|                 modal.close() | ||||
|               ) | ||||
|             } | ||||
|           } else { | ||||
|             this.openDocumentService | ||||
|               .openDocument(doc) | ||||
|               .pipe( | ||||
|                 first(), | ||||
|                 takeUntil(this.unsubscribeNotifier), | ||||
|                 takeUntil(this.docChangeNotifier) | ||||
|               ) | ||||
|               .subscribe() | ||||
|           } | ||||
|           this.updateComponent(useDoc) | ||||
|           this.titleSubject | ||||
|             .pipe( | ||||
|               debounceTime(1000), | ||||
|               distinctUntilChanged(), | ||||
|               takeUntil(this.docChangeNotifier), | ||||
|               takeUntil(this.unsubscribeNotifier) | ||||
|             ) | ||||
|             .subscribe((titleValue) => { | ||||
|               if (titleValue !== this.titleInput.value) return | ||||
|               this.title = titleValue | ||||
|               this.documentForm.patchValue({ title: titleValue }) | ||||
|             }) | ||||
|           this.setupDirtyTracking(useDoc, doc) | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING)) | ||||
|     this.documentForm.valueChanges | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|       .subscribe((values) => { | ||||
|         this.error = null | ||||
|         const docValues = Object.assign({}, this.documentForm.value) | ||||
|         docValues['owner'] = | ||||
|           this.documentForm.get('permissions_form').value['owner'] | ||||
|         docValues['set_permissions'] = | ||||
|           this.documentForm.get('permissions_form').value['set_permissions'] | ||||
|         delete docValues['permissions_form'] | ||||
|         Object.assign(this.document, docValues) | ||||
|         Object.assign(this.document, this.mapFormToDoc(values)) | ||||
|       }) | ||||
|  | ||||
|     if ( | ||||
| @@ -391,171 +536,36 @@ export class DocumentDetailComponent | ||||
|  | ||||
|     this.route.paramMap | ||||
|       .pipe( | ||||
|         filter((paramMap) => { | ||||
|           // only init when changing docs & section is set | ||||
|           return ( | ||||
|         filter( | ||||
|           (paramMap) => | ||||
|             +paramMap.get('id') !== this.documentId && | ||||
|             paramMap.get('section')?.length > 0 | ||||
|           ) | ||||
|         }), | ||||
|         takeUntil(this.unsubscribeNotifier), | ||||
|         switchMap((paramMap) => { | ||||
|           const documentId = +paramMap.get('id') | ||||
|           this.docChangeNotifier.next(documentId) | ||||
|           // Dont wait to get the preview | ||||
|           this.previewUrl = this.documentsService.getPreviewUrl(documentId) | ||||
|           this.http.get(this.previewUrl, { responseType: 'text' }).subscribe({ | ||||
|             next: (res) => { | ||||
|               this.previewText = res.toString() | ||||
|             }, | ||||
|             error: (err) => { | ||||
|               this.previewText = $localize`An error occurred loading content: ${ | ||||
|                 err.message ?? err.toString() | ||||
|               }` | ||||
|             }, | ||||
|           }) | ||||
|           this.thumbUrl = this.documentsService.getThumbUrl(documentId) | ||||
|           return this.documentsService.get(documentId) | ||||
|         }) | ||||
|         ), | ||||
|         takeUntil(this.unsubscribeNotifier) | ||||
|       ) | ||||
|       .pipe( | ||||
|         switchMap((doc) => { | ||||
|           this.documentId = doc.id | ||||
|           this.suggestions = null | ||||
|           const openDocument = this.openDocumentService.getOpenDocument( | ||||
|             this.documentId | ||||
|           ) | ||||
|  | ||||
|           if (openDocument) { | ||||
|             if ( | ||||
|               new Date(doc.modified) > new Date(openDocument.modified) && | ||||
|               !this.modalService.hasOpenModals() | ||||
|             ) { | ||||
|               let modal = this.modalService.open(ConfirmDialogComponent) | ||||
|               modal.componentInstance.title = $localize`Document changes detected` | ||||
|               modal.componentInstance.messageBold = $localize`The version of this document in your browser session appears older than the existing version.` | ||||
|               modal.componentInstance.message = $localize`Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.` | ||||
|               modal.componentInstance.cancelBtnClass = 'visually-hidden' | ||||
|               modal.componentInstance.btnCaption = $localize`Ok` | ||||
|               modal.componentInstance.confirmClicked.subscribe(() => | ||||
|                 modal.close() | ||||
|               ) | ||||
|             } | ||||
|  | ||||
|             // Prevent mutating stale form values into the next document: only sync if it still matches the active document. | ||||
|             if ( | ||||
|               this.documentForm.dirty && | ||||
|               (this.document?.id === openDocument.id || !this.document) | ||||
|             ) { | ||||
|               Object.assign(openDocument, this.documentForm.value) | ||||
|               openDocument['owner'] = | ||||
|                 this.documentForm.get('permissions_form').value['owner'] | ||||
|               openDocument['permissions'] = | ||||
|                 this.documentForm.get('permissions_form').value[ | ||||
|                   'set_permissions' | ||||
|                 ] | ||||
|               delete openDocument['permissions_form'] | ||||
|             } | ||||
|             if (openDocument.__changedFields) { | ||||
|               openDocument.__changedFields.forEach((field) => { | ||||
|                 if (field === 'owner' || field === 'set_permissions') { | ||||
|                   this.documentForm.get('permissions_form').markAsDirty() | ||||
|                 } else { | ||||
|                   this.documentForm.get(field)?.markAsDirty() | ||||
|                 } | ||||
|               }) | ||||
|             } | ||||
|             this.updateComponent(openDocument) | ||||
|           } else { | ||||
|             this.openDocumentService.openDocument(doc) | ||||
|             this.updateComponent(doc) | ||||
|           } | ||||
|  | ||||
|           this.titleSubject | ||||
|             .pipe( | ||||
|               debounceTime(1000), | ||||
|               distinctUntilChanged(), | ||||
|               takeUntil(this.docChangeNotifier), | ||||
|               takeUntil(this.unsubscribeNotifier) | ||||
|             ) | ||||
|             .subscribe({ | ||||
|               next: (titleValue) => { | ||||
|                 // In the rare case when the field changed just after debounced event was fired. | ||||
|                 // We dont want to overwrite what's actually in the text field, so just return | ||||
|                 if (titleValue !== this.titleInput.value) return | ||||
|  | ||||
|                 this.title = titleValue | ||||
|                 this.documentForm.patchValue({ title: titleValue }) | ||||
|               }, | ||||
|               complete: () => { | ||||
|                 // doc changed so we manually check dirty in case title was changed | ||||
|                 if ( | ||||
|                   this.store.getValue().title !== | ||||
|                   this.documentForm.get('title').value | ||||
|                 ) { | ||||
|                   this.openDocumentService.setDirty( | ||||
|                     doc, | ||||
|                     true, | ||||
|                     this.getChangedFields() | ||||
|                   ) | ||||
|                 } | ||||
|               }, | ||||
|             }) | ||||
|  | ||||
|           // Initialize dirtyCheck | ||||
|           this.store = new BehaviorSubject({ | ||||
|             title: doc.title, | ||||
|             content: doc.content, | ||||
|             created: doc.created, | ||||
|             correspondent: doc.correspondent, | ||||
|             document_type: doc.document_type, | ||||
|             storage_path: doc.storage_path, | ||||
|             archive_serial_number: doc.archive_serial_number, | ||||
|             tags: [...doc.tags], | ||||
|             permissions_form: { | ||||
|               owner: doc.owner, | ||||
|               set_permissions: doc.permissions, | ||||
|             }, | ||||
|             custom_fields: [...doc.custom_fields], | ||||
|           }) | ||||
|  | ||||
|           this.isDirty$ = dirtyCheck( | ||||
|             this.documentForm, | ||||
|             this.store.asObservable() | ||||
|           ) | ||||
|  | ||||
|           return this.isDirty$.pipe( | ||||
|             takeUntil(this.unsubscribeNotifier), | ||||
|             map((dirty) => ({ doc, dirty })) | ||||
|           ) | ||||
|         }) | ||||
|       ) | ||||
|       .subscribe({ | ||||
|         next: ({ doc, dirty }) => { | ||||
|           this.openDocumentService.setDirty(doc, dirty, this.getChangedFields()) | ||||
|         }, | ||||
|         error: (error) => { | ||||
|           this.router.navigate(['404'], { | ||||
|             replaceUrl: true, | ||||
|           }) | ||||
|         }, | ||||
|       .subscribe((paramMap) => { | ||||
|         const documentId = +paramMap.get('id') | ||||
|         this.docChangeNotifier.next(documentId) | ||||
|         this.loadDocument(documentId) | ||||
|       }) | ||||
|  | ||||
|     this.route.paramMap.subscribe((paramMap) => { | ||||
|       const section = paramMap.get('section') | ||||
|       if (section) { | ||||
|         const navIDKey: string = Object.keys(DocumentDetailNavIDs).find( | ||||
|           (navID) => navID.toLowerCase() == section | ||||
|         ) | ||||
|         if (navIDKey) { | ||||
|           this.activeNavID = DocumentDetailNavIDs[navIDKey] | ||||
|     this.route.paramMap | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((paramMap) => { | ||||
|         const section = paramMap.get('section') | ||||
|         if (section) { | ||||
|           const navIDKey: string = Object.keys(DocumentDetailNavIDs).find( | ||||
|             (navID) => navID.toLowerCase() == section | ||||
|           ) | ||||
|           if (navIDKey) { | ||||
|             this.activeNavID = DocumentDetailNavIDs[navIDKey] | ||||
|           } | ||||
|         } else if (paramMap.get('id')) { | ||||
|           this.router.navigate(['documents', +paramMap.get('id'), 'details'], { | ||||
|             replaceUrl: true, | ||||
|           }) | ||||
|         } | ||||
|       } else if (paramMap.get('id')) { | ||||
|         this.router.navigate(['documents', +paramMap.get('id'), 'details'], { | ||||
|           replaceUrl: true, | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
|       }) | ||||
|  | ||||
|     this.hotKeyService | ||||
|       .addShortcut({ | ||||
| @@ -682,19 +692,7 @@ export class DocumentDetailComponent | ||||
|         }) | ||||
|     } | ||||
|     this.title = this.documentTitlePipe.transform(doc.title) | ||||
|     const docFormValues = Object.assign({}, doc) | ||||
|     docFormValues['permissions_form'] = { | ||||
|       owner: doc.owner, | ||||
|       set_permissions: doc.permissions, | ||||
|     } | ||||
|  | ||||
|     this.documentForm.patchValue(docFormValues, { emitEvent: false }) | ||||
|     if (!this.userCanEdit) this.documentForm.disable() | ||||
|     setTimeout(() => { | ||||
|       // check again after a tick in case form was dirty | ||||
|       if (!this.userCanEdit) this.documentForm.disable() | ||||
|       else this.documentForm.enable() | ||||
|     }, 10) | ||||
|     this.prepareForm(doc) | ||||
|   } | ||||
|  | ||||
|   get customFieldFormFields(): FormArray { | ||||
| @@ -797,7 +795,11 @@ export class DocumentDetailComponent | ||||
|   discard() { | ||||
|     this.documentsService | ||||
|       .get(this.documentId) | ||||
|       .pipe(first()) | ||||
|       .pipe( | ||||
|         first(), | ||||
|         takeUntil(this.unsubscribeNotifier), | ||||
|         takeUntil(this.docChangeNotifier) | ||||
|       ) | ||||
|       .subscribe({ | ||||
|         next: (doc) => { | ||||
|           Object.assign(this.document, doc) | ||||
| @@ -900,9 +902,10 @@ export class DocumentDetailComponent | ||||
|       .patch(this.getChangedFields()) | ||||
|       .pipe( | ||||
|         switchMap((updateResult) => { | ||||
|           return this.documentListViewService | ||||
|             .getNext(this.documentId) | ||||
|             .pipe(map((nextDocId) => ({ nextDocId, updateResult }))) | ||||
|           return this.documentListViewService.getNext(this.documentId).pipe( | ||||
|             map((nextDocId) => ({ nextDocId, updateResult })), | ||||
|             takeUntil(this.unsubscribeNotifier) | ||||
|           ) | ||||
|         }) | ||||
|       ) | ||||
|       .pipe( | ||||
| @@ -912,7 +915,10 @@ export class DocumentDetailComponent | ||||
|             return this.openDocumentService | ||||
|               .closeDocument(this.document) | ||||
|               .pipe( | ||||
|                 map((closeResult) => ({ updateResult, nextDocId, closeResult })) | ||||
|                 map( | ||||
|                   (closeResult) => ({ updateResult, nextDocId, closeResult }), | ||||
|                   takeUntil(this.unsubscribeNotifier) | ||||
|                 ) | ||||
|               ) | ||||
|           } | ||||
|         }) | ||||
| @@ -1238,16 +1244,19 @@ export class DocumentDetailComponent | ||||
|     ) { | ||||
|       doc.owner = this.store.value.permissions_form.owner | ||||
|     } | ||||
|     return !this.document || this.userCanEditDoc(doc) | ||||
|   } | ||||
|  | ||||
|   private userCanEditDoc(doc: Document): boolean { | ||||
|     return ( | ||||
|       !this.document || | ||||
|       (this.permissionsService.currentUserCan( | ||||
|       this.permissionsService.currentUserCan( | ||||
|         PermissionAction.Change, | ||||
|         PermissionType.Document | ||||
|       ) && | ||||
|         this.permissionsService.currentUserHasObjectPermissions( | ||||
|           PermissionAction.Change, | ||||
|           doc | ||||
|         )) | ||||
|       this.permissionsService.currentUserHasObjectPermissions( | ||||
|         PermissionAction.Change, | ||||
|         doc | ||||
|       ) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
| @@ -1429,43 +1438,50 @@ export class DocumentDetailComponent | ||||
|   } | ||||
|  | ||||
|   private tryRenderTiff() { | ||||
|     this.http.get(this.previewUrl, { responseType: 'arraybuffer' }).subscribe({ | ||||
|       next: (res) => { | ||||
|         /* istanbul ignore next */ | ||||
|         try { | ||||
|           // See UTIF.js > _imgLoaded | ||||
|           const tiffIfds: any[] = UTIF.decode(res) | ||||
|           var vsns = tiffIfds, | ||||
|             ma = 0, | ||||
|             page = vsns[0] | ||||
|           if (tiffIfds[0].subIFD) vsns = vsns.concat(tiffIfds[0].subIFD) | ||||
|           for (var i = 0; i < vsns.length; i++) { | ||||
|             var img = vsns[i] | ||||
|             if (img['t258'] == null || img['t258'].length < 3) continue | ||||
|             var ar = img['t256'] * img['t257'] | ||||
|             if (ar > ma) { | ||||
|               ma = ar | ||||
|               page = img | ||||
|     this.http | ||||
|       .get(this.previewUrl, { responseType: 'arraybuffer' }) | ||||
|       .pipe( | ||||
|         first(), | ||||
|         takeUntil(this.unsubscribeNotifier), | ||||
|         takeUntil(this.docChangeNotifier) | ||||
|       ) | ||||
|       .subscribe({ | ||||
|         next: (res) => { | ||||
|           /* istanbul ignore next */ | ||||
|           try { | ||||
|             // See UTIF.js > _imgLoaded | ||||
|             const tiffIfds: any[] = UTIF.decode(res) | ||||
|             var vsns = tiffIfds, | ||||
|               ma = 0, | ||||
|               page = vsns[0] | ||||
|             if (tiffIfds[0].subIFD) vsns = vsns.concat(tiffIfds[0].subIFD) | ||||
|             for (var i = 0; i < vsns.length; i++) { | ||||
|               var img = vsns[i] | ||||
|               if (img['t258'] == null || img['t258'].length < 3) continue | ||||
|               var ar = img['t256'] * img['t257'] | ||||
|               if (ar > ma) { | ||||
|                 ma = ar | ||||
|                 page = img | ||||
|               } | ||||
|             } | ||||
|             UTIF.decodeImage(res, page, tiffIfds) | ||||
|             const rgba = UTIF.toRGBA8(page) | ||||
|             const { width: w, height: h } = page | ||||
|             var cnv = document.createElement('canvas') | ||||
|             cnv.width = w | ||||
|             cnv.height = h | ||||
|             var ctx = cnv.getContext('2d'), | ||||
|               imgd = ctx.createImageData(w, h) | ||||
|             for (var i = 0; i < rgba.length; i++) imgd.data[i] = rgba[i] | ||||
|             ctx.putImageData(imgd, 0, 0) | ||||
|             this.tiffURL = cnv.toDataURL() | ||||
|           } catch (err) { | ||||
|             this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}` | ||||
|           } | ||||
|           UTIF.decodeImage(res, page, tiffIfds) | ||||
|           const rgba = UTIF.toRGBA8(page) | ||||
|           const { width: w, height: h } = page | ||||
|           var cnv = document.createElement('canvas') | ||||
|           cnv.width = w | ||||
|           cnv.height = h | ||||
|           var ctx = cnv.getContext('2d'), | ||||
|             imgd = ctx.createImageData(w, h) | ||||
|           for (var i = 0; i < rgba.length; i++) imgd.data[i] = rgba[i] | ||||
|           ctx.putImageData(imgd, 0, 0) | ||||
|           this.tiffURL = cnv.toDataURL() | ||||
|         } catch (err) { | ||||
|         }, | ||||
|         error: (err) => { | ||||
|           this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}` | ||||
|         } | ||||
|       }, | ||||
|       error: (err) => { | ||||
|         this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}` | ||||
|       }, | ||||
|     }) | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -15,13 +15,8 @@ | ||||
|       } | ||||
|  | ||||
|     </div> | ||||
|   <div class="col col-md-10"> | ||||
|     <div class="col col-md-10"> | ||||
|       <div class="card-body"> | ||||
|         @if (document?.in_process) { | ||||
|           <span class="badge bg-secondary text-light mb-2"> | ||||
|             <div class="spinner-border spinner-border-sm me-1" role="status"></div><span i18n>Processing...</span> | ||||
|           </span> | ||||
|         } | ||||
|         <div class="d-flex justify-content-between align-items-center"> | ||||
|           <h5 class="card-title w-100"> | ||||
|             @if (document) { | ||||
|   | ||||
| @@ -37,11 +37,6 @@ | ||||
|     } | ||||
|  | ||||
|     <div class="card-body bg-light p-2"> | ||||
|       @if (document?.in_process) { | ||||
|         <span class="badge bg-secondary text-light mb-2"> | ||||
|           <div class="spinner-border spinner-border-sm me-1" role="status"></div><span i18n>Processing...</span> | ||||
|         </span> | ||||
|       } | ||||
|       <p class="card-text"> | ||||
|         @if (document) { | ||||
|           @if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) { | ||||
|   | ||||
| @@ -301,11 +301,6 @@ | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) { | ||||
|                   <td width="30%"> | ||||
|                     @if (d.in_process) { | ||||
|                       <span class="badge bg-secondary text-light me-1"> | ||||
|                         <div class="spinner-border spinner-border-sm me-1" role="status"></div><span i18n>Processing...</span> | ||||
|                       </span> | ||||
|                     } | ||||
|                     @if (activeDisplayFields.includes(DisplayField.TITLE)) { | ||||
|                       <div class="d-inline-block" (mouseleave)="popupPreview.close()"> | ||||
|                         <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> | ||||
|   | ||||
| @@ -199,6 +199,14 @@ describe('DocumentListComponent', () => { | ||||
|     } | ||||
|     const queryParams = { id: view.id.toString() } | ||||
|     const getSavedViewSpy = jest.spyOn(savedViewService, 'getCached') | ||||
|     const setCountSpy = jest.spyOn(savedViewService, 'setDocumentCount') | ||||
|     jest.spyOn(documentService, 'listFiltered').mockReturnValue( | ||||
|       of({ | ||||
|         results: docs, | ||||
|         count: 3, | ||||
|         all: docs.map((d) => d.id), | ||||
|       }) | ||||
|     ) | ||||
|     getSavedViewSpy.mockReturnValue(of(view)) | ||||
|     const activateSavedViewSpy = jest.spyOn( | ||||
|       documentListService, | ||||
| @@ -215,6 +223,7 @@ describe('DocumentListComponent', () => { | ||||
|       view, | ||||
|       convertToParamMap(queryParams) | ||||
|     ) | ||||
|     expect(setCountSpy).toHaveBeenCalledWith(view, 3) | ||||
|   }) | ||||
|  | ||||
|   it('should 404 on load saved view from URL if no view', () => { | ||||
| @@ -248,6 +257,34 @@ describe('DocumentListComponent', () => { | ||||
|     expect(getSavedViewSpy).toHaveBeenCalledWith(view.id) | ||||
|   }) | ||||
|  | ||||
|   it('should update saved view document count on load saved view from query params', () => { | ||||
|     jest.spyOn(savedViewService, 'getCached').mockReturnValue( | ||||
|       of({ | ||||
|         id: 10, | ||||
|         sort_field: 'added', | ||||
|         sort_reverse: true, | ||||
|         filter_rules: [], | ||||
|       }) | ||||
|     ) | ||||
|     jest.spyOn(documentService, 'listFiltered').mockReturnValue( | ||||
|       of({ | ||||
|         results: docs, | ||||
|         count: 3, | ||||
|         all: docs.map((d) => d.id), | ||||
|       }) | ||||
|     ) | ||||
|     const setCountSpy = jest.spyOn(savedViewService, 'setDocumentCount') | ||||
|     jest.spyOn(documentService, 'listFiltered').mockReturnValue( | ||||
|       of({ | ||||
|         results: docs, | ||||
|         count: 3, | ||||
|         all: docs.map((d) => d.id), | ||||
|       }) | ||||
|     ) | ||||
|     component.loadViewConfig(10) | ||||
|     expect(setCountSpy).toHaveBeenCalledWith(expect.any(Object), 3) | ||||
|   }) | ||||
|  | ||||
|   it('should support 3 different display modes', () => { | ||||
|     jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs) | ||||
|     fixture.detectChanges() | ||||
|   | ||||
| @@ -264,7 +264,9 @@ export class DocumentListComponent | ||||
|           view, | ||||
|           convertToParamMap(this.route.snapshot.queryParams) | ||||
|         ) | ||||
|         this.list.reload() | ||||
|         this.list.reload(() => { | ||||
|           this.savedViewService.setDocumentCount(view, this.list.collectionSize) | ||||
|         }) | ||||
|         this.updateDisplayCustomFields() | ||||
|         this.unmodifiedFilterRules = view.filter_rules | ||||
|       }) | ||||
| @@ -399,7 +401,9 @@ export class DocumentListComponent | ||||
|       .subscribe((view) => { | ||||
|         this.unmodifiedSavedView = view | ||||
|         this.list.activateSavedView(view) | ||||
|         this.list.reload() | ||||
|         this.list.reload(() => { | ||||
|           this.savedViewService.setDocumentCount(view, this.list.collectionSize) | ||||
|         }) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -159,8 +159,6 @@ export interface Document extends ObjectWithPermissions { | ||||
|  | ||||
|   page_count?: number | ||||
|  | ||||
|   in_process?: boolean | ||||
|  | ||||
|   // Frontend only | ||||
|   __changedFields?: string[] | ||||
| } | ||||
|   | ||||
| @@ -140,11 +140,15 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> { | ||||
|         ) | ||||
|         .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|         .subscribe((results: Results<Document>) => { | ||||
|           this.savedViewDocumentCounts.set(view.id, results.count) | ||||
|           this.setDocumentCount(view, results.count) | ||||
|         }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   public setDocumentCount(view: SavedView, count: number) { | ||||
|     this.savedViewDocumentCounts.set(view.id, count) | ||||
|   } | ||||
|  | ||||
|   public getDocumentCount(view: SavedView): number { | ||||
|     return this.savedViewDocumentCounts.get(view.id) | ||||
|   } | ||||
|   | ||||
| @@ -283,7 +283,6 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]: | ||||
|         f"Attempting to rotate {len(doc_ids)} documents by {degrees} degrees.", | ||||
|     ) | ||||
|     qs = Document.objects.filter(id__in=doc_ids) | ||||
|     Document.objects.filter(pk__in=doc_ids).update(in_process=True) | ||||
|     affected_docs: list[int] = [] | ||||
|     import pikepdf | ||||
|  | ||||
| @@ -310,9 +309,7 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]: | ||||
|                     f"Rotated document {doc.id} by {degrees} degrees", | ||||
|                 ) | ||||
|                 affected_docs.append(doc.id) | ||||
|                 Document.objects.filter(pk__in=doc_ids).update(in_process=False) | ||||
|         except Exception as e: | ||||
|             Document.objects.filter(pk__in=doc_ids).update(in_process=False) | ||||
|             logger.exception(f"Error rotating document {doc.id}: {e}") | ||||
|  | ||||
|     if len(affected_docs) > 0: | ||||
| @@ -477,7 +474,6 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]: | ||||
|         f"Attempting to delete pages {pages} from {len(doc_ids)} documents", | ||||
|     ) | ||||
|     doc = Document.objects.get(id=doc_ids[0]) | ||||
|     Document.objects.filter(pk=doc.id).update(in_process=True) | ||||
|     pages = sorted(pages)  # sort pages to avoid index issues | ||||
|     import pikepdf | ||||
|  | ||||
| @@ -496,7 +492,6 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]: | ||||
|             update_document_content_maybe_archive_file.delay(document_id=doc.id) | ||||
|             logger.info(f"Deleted pages {pages} from document {doc.id}") | ||||
|     except Exception as e: | ||||
|         Document.objects.filter(pk=doc.id).update(in_process=False) | ||||
|         logger.exception(f"Error deleting pages from document {doc.id}: {e}") | ||||
|  | ||||
|     return "OK" | ||||
| @@ -523,7 +518,6 @@ def edit_pdf( | ||||
|         f"Editing PDF of document {doc_ids[0]} with {len(operations)} operations", | ||||
|     ) | ||||
|     doc = Document.objects.get(id=doc_ids[0]) | ||||
|     Document.objects.filter(pk=doc.id).update(in_process=True) | ||||
|     import pikepdf | ||||
|  | ||||
|     pdf_docs: list[pikepdf.Pdf] = [] | ||||
| @@ -593,7 +587,6 @@ def edit_pdf( | ||||
|  | ||||
|     except Exception as e: | ||||
|         logger.exception(f"Error editing document {doc.id}: {e}") | ||||
|         Document.objects.filter(pk=doc.id).update(in_process=False) | ||||
|         raise ValueError( | ||||
|             f"An error occurred while editing the document: {e}", | ||||
|         ) from e | ||||
|   | ||||
| @@ -1,23 +0,0 @@ | ||||
| # Generated by Django 5.2.5 on 2025-08-26 07:54 | ||||
|  | ||||
| from django.db import migrations | ||||
| from django.db import models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         ("documents", "1068_alter_document_created"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="document", | ||||
|             name="in_process", | ||||
|             field=models.BooleanField( | ||||
|                 db_index=True, | ||||
|                 default=False, | ||||
|                 help_text="Whether the document is currently being processed.", | ||||
|                 verbose_name="in process", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -289,13 +289,6 @@ class Document(SoftDeleteModel, ModelWithOwner): | ||||
|         ), | ||||
|     ) | ||||
|  | ||||
|     in_process = models.BooleanField( | ||||
|         _("in process"), | ||||
|         default=False, | ||||
|         db_index=True, | ||||
|         help_text=_("Whether the document is currently being processed."), | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         ordering = ("-created",) | ||||
|         verbose_name = _("document") | ||||
|   | ||||
| @@ -935,8 +935,6 @@ class DocumentSerializer( | ||||
|         required=False, | ||||
|     ) | ||||
|  | ||||
|     in_process = serializers.BooleanField(read_only=True) | ||||
|  | ||||
|     def get_page_count(self, obj) -> int | None: | ||||
|         return obj.page_count | ||||
|  | ||||
| @@ -1105,7 +1103,6 @@ class DocumentSerializer( | ||||
|             "remove_inbox_tags", | ||||
|             "page_count", | ||||
|             "mime_type", | ||||
|             "in_process", | ||||
|         ) | ||||
|         list_serializer_class = OwnedObjectListSerializer | ||||
|  | ||||
|   | ||||
| @@ -250,7 +250,6 @@ def update_document_content_maybe_archive_file(document_id): | ||||
|     it exists. | ||||
|     """ | ||||
|     document = Document.objects.get(id=document_id) | ||||
|     Document.objects.filter(pk=document_id).update(in_process=True) | ||||
|  | ||||
|     mime_type = document.mime_type | ||||
|  | ||||
| @@ -350,7 +349,6 @@ def update_document_content_maybe_archive_file(document_id): | ||||
|         ) | ||||
|     finally: | ||||
|         parser.cleanup() | ||||
|         Document.objects.filter(pk=document_id).update(in_process=False) | ||||
|  | ||||
|  | ||||
| @shared_task | ||||
|   | ||||
		Reference in New Issue
	
	Block a user