Compare commits

..
Author SHA1 Message Date
Crowdin Bot acaf908ca4 New Crowdin translations by GitHub Action 2026-08-13 19:49:43 +00:00
GitHub Actions 01c12d9ea4 Auto translate strings 2026-08-13 19:48:40 +00:00
Max TruxaandGitHub f5c0d118f7 Fix: fix validation of workflow title assignment (#13659) 2026-08-13 12:46:57 -07:00
ff13847d0a Feature: Allow selection of compression type and and level during export (#13661)
* Feature: Allow configuring the compression type and compression levels during export

Building on the zip export improvements, this now allows users to further configure the
zip to fit their needs.  A simple stored zip for speed, or a high compression zstd for
the smallest archive.  Full validation of the method and levels at the command line

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-08-13 18:29:33 +00:00
shamoonandGitHub 634f803872 Fix: dont clip search dropdown on mobile (#13675) 2026-08-13 10:57:16 -07:00
GitHub Actions 639d566a7c Auto translate strings 2026-08-13 16:50:00 +00:00
shamoonandGitHub 0a94f8f0d4 Fix: use proper signal for modal buttonsEnabled (#13672) 2026-08-13 09:48:23 -07:00
184 changed files with 107928 additions and 77132 deletions
+15
View File
@@ -299,6 +299,8 @@ optional arguments:
-sm, --split-manifest -sm, --split-manifest
-z, --zip -z, --zip
-zn, --zip-name -zn, --zip-name
--zip-compression
--zip-compression-level
--data-only --data-only
--no-progress-bar --no-progress-bar
--passphrase --passphrase
@@ -361,6 +363,19 @@ If `-z` or `--zip` is provided, the export will be a zip file
in the target directory, named according to the current local date or the in the target directory, named according to the current local date or the
value set in `-zn` or `--zip-name`. value set in `-zn` or `--zip-name`.
The compression method for the zip can be set with `--zip-compression`
(`stored`, `deflated` (default), `bzip2`, `lzma`, or `zstd`) and tuned with
`--zip-compression-level` (deflated: 09, bzip2: 19, zstd: -2222; ignored
for `stored` and `lzma`). Both options require `--zip`.
!!! warning
`zstd` compression requires Python 3.14 or newer on **both** the machine
creating the export and any machine importing it. An archive compressed with
`zstd` (or `lzma`/`bzip2` where those modules are unavailable) cannot be
imported on a runtime that lacks the codec; the importer will refuse it with
a clear error. The default `deflated` is universally readable.
If `--data-only` is provided, only the database will be exported. This option is intended If `--data-only` is provided, only the database will be exported. This option is intended
to facilitate database upgrades without needing to clean documents and thumbnails from the media directory. to facilitate database upgrades without needing to clean documents and thumbnails from the media directory.
+1 -2
View File
@@ -301,8 +301,7 @@ The following methods are supported:
- `delete` - `delete`
- No `parameters` required - No `parameters` required
- `reprocess` - `reprocess`
- Optional `parameters`: `{ "remote_ocr": true }` to send the documents to the - No `parameters` required
remote OCR engine, see [Remote OCR](usage.md#remote-ocr). Defaults to false.
- `set_permissions` - `set_permissions`
- Requires `parameters`: - Requires `parameters`:
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or - `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
-12
View File
@@ -2048,18 +2048,6 @@ password. All of these options come from their similarly-named [Django settings]
Defaults to None. Defaults to None.
#### [`PAPERLESS_REMOTE_OCR_MODE=<str>`](#PAPERLESS_REMOTE_OCR_MODE) {#PAPERLESS_REMOTE_OCR_MODE}
: Which documents are sent to the remote OCR engine.
- `always`: every document of a supported file type is sent to the remote
engine, bypassing the local OCR engine.
- `workflow_only`: documents are processed locally unless a workflow
explicitly enables remote OCR for them, letting you use the remote engine
selectively.
Defaults to "always".
## AI {#ai} ## AI {#ai}
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED} #### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
-14
View File
@@ -456,20 +456,6 @@ def score(
return 10 return 10
``` ```
**Remote services**
If your parser sends document content to a remote service, declare it:
```python
class MyCustomParser:
uses_remote_service = True
```
Paperless-ngx excludes such parsers when the document being consumed has not
been marked for remote processing, so users can keep remote OCR off by default
and enable it selectively with a workflow. Parsers that do not declare the
attribute are treated as fully local and are always considered.
**Archive and rendition flags** **Archive and rendition flags**
```python ```python
+1 -22
View File
@@ -650,19 +650,6 @@ happened while it was still encrypted, that original version will likewise be mi
**Current limitation**: Passwords are stored as a simple list without descriptions. To handle **Current limitation**: Passwords are stored as a simple list without descriptions. To handle
multiple PDF types with different passwords, create separate workflows for each use case. multiple PDF types with different passwords, create separate workflows for each use case.
##### Remote OCR {#workflow-action-remote-ocr}
"Remote OCR" actions send the document to the configured remote OCR engine instead of processing it
locally. To use remote OCR selectively, set the [remote OCR mode](configuration.md#PAPERLESS_REMOTE_OCR_MODE)
to `workflow_only` then add this action to a workflow that matches only the documents you
want sent to the remote engine. See [Remote OCR](#remote-ocr) for the engine setup. The action only works with
a **Consumption Started** trigger.
The action takes no options, its presence is what enables remote OCR for a matching document.
If the remote engine is not configured, or does not support the document's file type, the document is
processed locally instead and a warning is written to the log.
#### Workflow placeholders #### Workflow placeholders
Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/). Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
@@ -1099,19 +1086,11 @@ Paperless-ngx supports performing OCR on documents using remote services. At the
[Microsoft's Azure "Document Intelligence" service](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence). [Microsoft's Azure "Document Intelligence" service](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence).
This is of course a paid service (with a free tier) which requires an Azure account and subscription. Azure AI is not affiliated with This is of course a paid service (with a free tier) which requires an Azure account and subscription. Azure AI is not affiliated with
Paperless-ngx in any way. When enabled, Paperless-ngx will automatically send appropriate documents to Azure for OCR processing, bypassing Paperless-ngx in any way. When enabled, Paperless-ngx will automatically send appropriate documents to Azure for OCR processing, bypassing
the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details. These the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details.
settings can be supplied as environment variables or via **Application Configuration**.
Additionally, when using a commercial service with this feature, consider both potential costs as well as any associated file size Additionally, when using a commercial service with this feature, consider both potential costs as well as any associated file size
or page limitations (e.g. with a free tier). or page limitations (e.g. with a free tier).
By default, every document of a supported file type is sent to the remote engine. To use it more selectively, set the
[remote OCR mode](configuration.md#PAPERLESS_REMOTE_OCR_MODE) to `workflow_only`. Documents are then processed locally
unless a [remote OCR workflow action](#workflow-action-remote-ocr) enables it for them, so you can limit the remote
engine to particular documents.
Setting the mode to `workflow_only` also allows the **Reprocess** actions to selectively use remote OCR for individual documents.
## Architecture ## Architecture
Paperless-ngx consists of the following components: Paperless-ngx consists of the following components:
+53 -53
View File
@@ -539,7 +539,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
<context context-type="linenumber">47</context> <context context-type="linenumber">54</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
@@ -3098,15 +3098,15 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">919</context> <context context-type="linenumber">910</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">955</context> <context context-type="linenumber">946</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">978</context> <context context-type="linenumber">969</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/document-attributes/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/document-attributes/custom-fields/custom-fields.component.ts</context>
@@ -3684,14 +3684,14 @@
<source>Confirmation</source> <source>Confirmation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
<context context-type="linenumber">23</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9178182467454450952" datatype="html"> <trans-unit id="9178182467454450952" datatype="html">
<source>Confirm</source> <source>Confirm</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
<context context-type="linenumber">35</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -3703,27 +3703,27 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">556</context> <context context-type="linenumber">547</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">596</context> <context context-type="linenumber">587</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">634</context> <context context-type="linenumber">625</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">672</context> <context context-type="linenumber">663</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">734</context> <context context-type="linenumber">725</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">867</context> <context context-type="linenumber">858</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="994016933065248559" datatype="html"> <trans-unit id="994016933065248559" datatype="html">
@@ -5743,7 +5743,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">871</context> <context context-type="linenumber">862</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4522609911791833187" datatype="html"> <trans-unit id="4522609911791833187" datatype="html">
@@ -7613,7 +7613,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">501</context> <context context-type="linenumber">492</context>
</context-group> </context-group>
<note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note> <note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note>
</trans-unit> </trans-unit>
@@ -8148,7 +8148,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">869</context> <context context-type="linenumber">860</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7295637485862454066" datatype="html"> <trans-unit id="7295637485862454066" datatype="html">
@@ -8166,7 +8166,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">915</context> <context context-type="linenumber">906</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2951161989614003846" datatype="html"> <trans-unit id="2951161989614003846" datatype="html">
@@ -8523,18 +8523,18 @@
<source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot;</source> <source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot;</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">493</context> <context context-type="linenumber">484</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">499</context> <context context-type="linenumber">490</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8639884465898458690" datatype="html"> <trans-unit id="8639884465898458690" datatype="html">
<source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</source> <source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">495</context> <context context-type="linenumber">486</context>
</context-group> </context-group>
<note priority="1" from="description">This is for messages like &apos;modify &quot;tag1&quot; and &quot;tag2&quot;&apos;</note> <note priority="1" from="description">This is for messages like &apos;modify &quot;tag1&quot; and &quot;tag2&quot;&apos;</note>
</trans-unit> </trans-unit>
@@ -8542,7 +8542,7 @@
<source><x id="PH" equiv-text="list"/> and &quot;<x id="PH_1" equiv-text="items[items.length - 1].name"/>&quot;</source> <source><x id="PH" equiv-text="list"/> and &quot;<x id="PH_1" equiv-text="items[items.length - 1].name"/>&quot;</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">503,505</context> <context context-type="linenumber">494,496</context>
</context-group> </context-group>
<note priority="1" from="description">this is for messages like &apos;modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;&apos;</note> <note priority="1" from="description">this is for messages like &apos;modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;&apos;</note>
</trans-unit> </trans-unit>
@@ -8550,14 +8550,14 @@
<source>Confirm tags assignment</source> <source>Confirm tags assignment</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">520</context> <context context-type="linenumber">511</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6619516195038467207" datatype="html"> <trans-unit id="6619516195038467207" datatype="html">
<source>This operation will add the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will add the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">526</context> <context context-type="linenumber">517</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1894412783609570695" datatype="html"> <trans-unit id="1894412783609570695" datatype="html">
@@ -8566,14 +8566,14 @@
)"/> to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> )"/> to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">531,533</context> <context context-type="linenumber">522,524</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7181166515756808573" datatype="html"> <trans-unit id="7181166515756808573" datatype="html">
<source>This operation will remove the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will remove the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">539</context> <context context-type="linenumber">530</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3819792277998068944" datatype="html"> <trans-unit id="3819792277998068944" datatype="html">
@@ -8582,7 +8582,7 @@
)"/> from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> )"/> from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">544,546</context> <context context-type="linenumber">535,537</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2739066218579571288" datatype="html"> <trans-unit id="2739066218579571288" datatype="html">
@@ -8593,84 +8593,84 @@
)"/> on <x id="PH_2" equiv-text="this.getSelectionSize()"/> selected document(s).</source> )"/> on <x id="PH_2" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">548,552</context> <context context-type="linenumber">539,543</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2996713129519325161" datatype="html"> <trans-unit id="2996713129519325161" datatype="html">
<source>Confirm correspondent assignment</source> <source>Confirm correspondent assignment</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">589</context> <context context-type="linenumber">580</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6900893559485781849" datatype="html"> <trans-unit id="6900893559485781849" datatype="html">
<source>This operation will assign the correspondent &quot;<x id="PH" equiv-text="correspondent.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will assign the correspondent &quot;<x id="PH" equiv-text="correspondent.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">591</context> <context context-type="linenumber">582</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1257522660364398440" datatype="html"> <trans-unit id="1257522660364398440" datatype="html">
<source>This operation will remove the correspondent from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will remove the correspondent from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">593</context> <context context-type="linenumber">584</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5393409374423140648" datatype="html"> <trans-unit id="5393409374423140648" datatype="html">
<source>Confirm document type assignment</source> <source>Confirm document type assignment</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">627</context> <context context-type="linenumber">618</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="332180123895325027" datatype="html"> <trans-unit id="332180123895325027" datatype="html">
<source>This operation will assign the document type &quot;<x id="PH" equiv-text="documentType.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will assign the document type &quot;<x id="PH" equiv-text="documentType.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">629</context> <context context-type="linenumber">620</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2236642492594872779" datatype="html"> <trans-unit id="2236642492594872779" datatype="html">
<source>This operation will remove the document type from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will remove the document type from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">631</context> <context context-type="linenumber">622</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6386555513013840736" datatype="html"> <trans-unit id="6386555513013840736" datatype="html">
<source>Confirm storage path assignment</source> <source>Confirm storage path assignment</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">665</context> <context context-type="linenumber">656</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8750527458618415924" datatype="html"> <trans-unit id="8750527458618415924" datatype="html">
<source>This operation will assign the storage path &quot;<x id="PH" equiv-text="storagePath.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will assign the storage path &quot;<x id="PH" equiv-text="storagePath.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">667</context> <context context-type="linenumber">658</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="60728365335056946" datatype="html"> <trans-unit id="60728365335056946" datatype="html">
<source>This operation will remove the storage path from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will remove the storage path from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">669</context> <context context-type="linenumber">660</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4187352575310415704" datatype="html"> <trans-unit id="4187352575310415704" datatype="html">
<source>Confirm custom field assignment</source> <source>Confirm custom field assignment</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">698</context> <context context-type="linenumber">689</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7966494636326273856" datatype="html"> <trans-unit id="7966494636326273856" datatype="html">
<source>This operation will assign the custom field &quot;<x id="PH" equiv-text="customField.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will assign the custom field &quot;<x id="PH" equiv-text="customField.name"/>&quot; to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">704</context> <context context-type="linenumber">695</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5789455969634598553" datatype="html"> <trans-unit id="5789455969634598553" datatype="html">
@@ -8679,14 +8679,14 @@
)"/> to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> )"/> to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">709,711</context> <context context-type="linenumber">700,702</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5648572354333199245" datatype="html"> <trans-unit id="5648572354333199245" datatype="html">
<source>This operation will remove the custom field &quot;<x id="PH" equiv-text="customField.name"/>&quot; from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will remove the custom field &quot;<x id="PH" equiv-text="customField.name"/>&quot; from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">717</context> <context context-type="linenumber">708</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6666899594015948817" datatype="html"> <trans-unit id="6666899594015948817" datatype="html">
@@ -8695,7 +8695,7 @@
)"/> from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source> )"/> from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">722,724</context> <context context-type="linenumber">713,715</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8050047262594964176" datatype="html"> <trans-unit id="8050047262594964176" datatype="html">
@@ -8706,91 +8706,91 @@
)"/> on <x id="PH_2" equiv-text="this.getSelectionSize()"/> selected document(s).</source> )"/> on <x id="PH_2" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">726,730</context> <context context-type="linenumber">717,721</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8615059324209654051" datatype="html"> <trans-unit id="8615059324209654051" datatype="html">
<source>Move <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s) to the trash?</source> <source>Move <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s) to the trash?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">868</context> <context context-type="linenumber">859</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8585195717323764335" datatype="html"> <trans-unit id="8585195717323764335" datatype="html">
<source>This operation will permanently recreate the archive files for <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source> <source>This operation will permanently recreate the archive files for <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">916</context> <context context-type="linenumber">907</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7366623494074776040" datatype="html"> <trans-unit id="7366623494074776040" datatype="html">
<source>The archive files will be re-generated with the current settings.</source> <source>The archive files will be re-generated with the current settings.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">917</context> <context context-type="linenumber">908</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6555329262222566158" datatype="html"> <trans-unit id="6555329262222566158" datatype="html">
<source>Rotate confirm</source> <source>Rotate confirm</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">952</context> <context context-type="linenumber">943</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5203024009814367559" datatype="html"> <trans-unit id="5203024009814367559" datatype="html">
<source>This operation will add rotated versions of the <x id="PH" equiv-text="this.getSelectionSize()"/> document(s).</source> <source>This operation will add rotated versions of the <x id="PH" equiv-text="this.getSelectionSize()"/> document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">953</context> <context context-type="linenumber">944</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7910756456450124185" datatype="html"> <trans-unit id="7910756456450124185" datatype="html">
<source>Merge confirm</source> <source>Merge confirm</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">976</context> <context context-type="linenumber">967</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7643543647233874431" datatype="html"> <trans-unit id="7643543647233874431" datatype="html">
<source>This operation will merge <x id="PH" equiv-text="this.getSelectionSize()"/> selected documents into a new document.</source> <source>This operation will merge <x id="PH" equiv-text="this.getSelectionSize()"/> selected documents into a new document.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">977</context> <context context-type="linenumber">968</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7869008840945899895" datatype="html"> <trans-unit id="7869008840945899895" datatype="html">
<source>Merged document will be queued for consumption.</source> <source>Merged document will be queued for consumption.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">1000</context> <context context-type="linenumber">991</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="476913782630693351" datatype="html"> <trans-unit id="476913782630693351" datatype="html">
<source>Custom fields updated.</source> <source>Custom fields updated.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">1025</context> <context context-type="linenumber">1016</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3873496751167944011" datatype="html"> <trans-unit id="3873496751167944011" datatype="html">
<source>Error updating custom fields.</source> <source>Error updating custom fields.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">1034</context> <context context-type="linenumber">1025</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6144801143088984138" datatype="html"> <trans-unit id="6144801143088984138" datatype="html">
<source>Share link bundle creation requested.</source> <source>Share link bundle creation requested.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">1082</context> <context context-type="linenumber">1073</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="46019676931295023" datatype="html"> <trans-unit id="46019676931295023" datatype="html">
<source>Share link bundle creation is not available yet.</source> <source>Share link bundle creation is not available yet.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">1089</context> <context context-type="linenumber">1080</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6307402210351946694" datatype="html"> <trans-unit id="6307402210351946694" datatype="html">
@@ -14,12 +14,8 @@
<a ngbNavLink>{{category}}</a> <a ngbNavLink>{{category}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div class="p-3"> <div class="p-3">
@for (section of getCategorySections(category); track section) {
@if (section) {
<h5 class="mt-4 mb-3">{{section}}</h5>
}
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
@for (option of getCategoryOptions(category, section); track option.key) { @for (option of getCategoryOptions(category); track option.key) {
<div class="col"> <div class="col">
<div class="card bg-light"> <div class="card bg-light">
<div class="card-body"> <div class="card-body">
@@ -55,7 +51,6 @@
</div> </div>
} }
</div> </div>
}
</div> </div>
</ng-template> </ng-template>
</li> </li>
@@ -8,11 +8,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { import { OutputTypeConfig } from 'src/app/data/paperless-config'
ConfigCategory,
ConfigSection,
OutputTypeConfig,
} from 'src/app/data/paperless-config'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
@@ -162,24 +158,4 @@ describe('ConfigComponent', () => {
component.resetOption('barcodes_enabled') component.resetOption('barcodes_enabled')
expect(component.configForm.get('barcodes_enabled').value).toBeNull() expect(component.configForm.get('barcodes_enabled').value).toBeNull()
}) })
it('should group options into sections within a category, or not', () => {
const sections = component.getCategorySections(ConfigCategory.OCR)
expect(sections).toEqual([null, ConfigSection.RemoteOCR])
expect(
component
.getCategoryOptions(ConfigCategory.OCR)
.map((option) => option.key)
).toContain('output_type')
expect(
component
.getCategoryOptions(ConfigCategory.OCR, ConfigSection.RemoteOCR)
.map((option) => option.key)
).toEqual([
'remote_ocr_engine',
'remote_ocr_api_key',
'remote_ocr_endpoint',
'remote_ocr_mode',
])
})
}) })
@@ -74,20 +74,8 @@ export class ConfigComponent
return Object.values(ConfigCategory) return Object.values(ConfigCategory)
} }
getCategorySections(category: string): string[] { getCategoryOptions(category: string): ConfigOption[] {
return [ return PaperlessConfigOptions.filter((o) => o.category === category)
...new Set(
PaperlessConfigOptions.filter((o) => o.category === category).map(
(o) => o.section ?? null // null means no section
)
),
]
}
getCategoryOptions(category: string, section: string = null): ConfigOption[] {
return PaperlessConfigOptions.filter(
(o) => o.category === category && (o.section ?? null) === section
)
} }
initialConfig: PaperlessConfig initialConfig: PaperlessConfig
@@ -576,7 +576,7 @@ describe('TasksComponent', () => {
expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id, tasks[1].id])) expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id, tasks[1].id]))
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error) expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
expect(modal.componentInstance.buttonsEnabled).toBe(true) expect(modal.componentInstance.buttonsEnabled()).toBe(true)
expect(component.selectedTasks.size).toBe(0) expect(component.selectedTasks.size).toBe(0)
}) })
@@ -642,7 +642,7 @@ describe('TasksComponent', () => {
expect(dismissSpy).toHaveBeenCalled() expect(dismissSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error) expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
expect(modal.componentInstance.buttonsEnabled).toBe(true) expect(modal.componentInstance.buttonsEnabled()).toBe(true)
}) })
it('should dismiss the currently visible scoped and filtered tasks', () => { it('should dismiss the currently visible scoped and filtered tasks', () => {
@@ -316,7 +316,7 @@ export class TasksComponent
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Dismiss` modal.componentInstance.btnCaption = $localize`Dismiss`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
modal.close() modal.close()
this.tasksService.dismissTasks(tasks).subscribe({ this.tasksService.dismissTasks(tasks).subscribe({
next: () => { next: () => {
@@ -324,7 +324,7 @@ export class TasksComponent
}, },
error: (e) => { error: (e) => {
this.toastService.showError($localize`Error dismissing tasks`, e) this.toastService.showError($localize`Error dismissing tasks`, e)
modal.componentInstance.buttonsEnabled = true modal.componentInstance.buttonsEnabled.set(true)
}, },
}) })
this.clearSelection() this.clearSelection()
@@ -350,7 +350,7 @@ export class TasksComponent
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Dismiss` modal.componentInstance.btnCaption = $localize`Dismiss`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
modal.close() modal.close()
this.tasksService.dismissAllTasks().subscribe({ this.tasksService.dismissAllTasks().subscribe({
next: () => { next: () => {
@@ -358,7 +358,7 @@ export class TasksComponent
}, },
error: (e) => { error: (e) => {
this.toastService.showError($localize`Error dismissing tasks`, e) this.toastService.showError($localize`Error dismissing tasks`, e)
modal.componentInstance.buttonsEnabled = true modal.componentInstance.buttonsEnabled.set(true)
}, },
}) })
this.clearSelection() this.clearSelection()
@@ -82,7 +82,7 @@ export class TrashComponent
modal.componentInstance.confirmClicked modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => { .subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.trashService.emptyTrash([document.id]).subscribe({ this.trashService.emptyTrash([document.id]).subscribe({
next: () => { next: () => {
this.toastService.showInfo( this.toastService.showInfo(
@@ -146,7 +146,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.usersService.delete(user).subscribe({ this.usersService.delete(user).subscribe({
next: () => { next: () => {
modal.close() modal.close()
@@ -199,7 +199,7 @@ export class UsersAndGroupsComponent
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.groupsService.delete(group).subscribe({ this.groupsService.delete(group).subscribe({
next: () => { next: () => {
modal.close() modal.close()
@@ -47,11 +47,12 @@
.search-container { .search-container {
max-height: 4.5rem; max-height: 4.5rem;
overflow: hidden; overflow: visible;
transition: max-height .2s ease, opacity .2s ease, padding-top .2s ease, padding-bottom .2s ease; transition: max-height .2s ease, opacity .2s ease, padding-top .2s ease, padding-bottom .2s ease;
&.mobile-hidden { &.mobile-hidden {
max-height: 0; max-height: 0;
overflow: hidden;
opacity: 0; opacity: 0;
padding-top: 0 !important; padding-top: 0 !important;
padding-bottom: 0 !important; padding-bottom: 0 !important;
@@ -12,10 +12,10 @@
} }
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled"> <button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span> <span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button> </button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled()">
<span> <span>
{{btnCaption}} {{btnCaption}}
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span> <span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
@@ -25,7 +25,7 @@
} }
</button> </button>
@if (alternativeBtnCaption) { @if (alternativeBtnCaption) {
<button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled"> <button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled()">
{{alternativeBtnCaption}} {{alternativeBtnCaption}}
</button> </button>
} }
@@ -64,6 +64,22 @@ describe('ConfirmDialogComponent', () => {
expect(confirmSubjectResult).toBeTruthy() expect(confirmSubjectResult).toBeTruthy()
}) })
it('should re-render the buttons when they are toggled from outside', async () => {
const confirmButton: HTMLButtonElement =
fixture.nativeElement.querySelectorAll('.modal-footer button')[1]
expect(confirmButton.disabled).toBeFalsy()
// Deliberately no detectChanges: a request callback toggling this is all
// that happens, and nothing else schedules a render for the modal
component.buttonsEnabled.set(false)
await fixture.whenStable()
expect(confirmButton.disabled).toBeTruthy()
component.buttonsEnabled.set(true)
await fixture.whenStable()
expect(confirmButton.disabled).toBeFalsy()
})
it('should support cancel & close modal', () => { it('should support cancel & close modal', () => {
let confirmSubjectResult let confirmSubjectResult
const closeModalSpy = jest.spyOn(modal, 'close') const closeModalSpy = jest.spyOn(modal, 'close')
@@ -1,5 +1,12 @@
import { DecimalPipe } from '@angular/common' import { DecimalPipe } from '@angular/common'
import { Component, EventEmitter, Input, Output, inject } from '@angular/core' import {
Component,
EventEmitter,
Input,
Output,
inject,
signal,
} from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@@ -46,8 +53,7 @@ export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
@Input() @Input()
cancelBtnCaption = $localize`Cancel` cancelBtnCaption = $localize`Cancel`
@Input() readonly buttonsEnabled = signal(true)
buttonsEnabled = true
confirmButtonEnabled = true confirmButtonEnabled = true
alternativeButtonEnabled = true alternativeButtonEnabled = true
@@ -56,10 +56,10 @@
} }
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled"> <button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span> <span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button> </button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled()">
{{btnCaption}} {{btnCaption}}
</button> </button>
</div> </div>
@@ -57,7 +57,7 @@
class="btn" class="btn"
[class]="cancelBtnClass" [class]="cancelBtnClass"
(click)="cancel()" (click)="cancel()"
[disabled]="!buttonsEnabled" [disabled]="!buttonsEnabled()"
> >
<span class="d-inline-block" style="padding-bottom: 1px;"> <span class="d-inline-block" style="padding-bottom: 1px;">
{{cancelBtnCaption}} {{cancelBtnCaption}}
@@ -68,7 +68,7 @@
class="btn" class="btn"
[class]="btnClass" [class]="btnClass"
(click)="confirm()" (click)="confirm()"
[disabled]="!confirmButtonEnabled || !buttonsEnabled" [disabled]="!confirmButtonEnabled || !buttonsEnabled()"
> >
{{btnCaption}} {{btnCaption}}
</button> </button>
@@ -1,28 +0,0 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
@if (messageBold) {
<p class="text-break"><b>{{messageBold}}</b></p>
}
@if (message) {
<p class="mb-0 text-break" [innerHTML]="message"></p>
}
@if (showRemoteOcr) {
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="reprocessRemoteOcr" [(ngModel)]="remoteOcr" />
<label class="form-check-label" for="reprocessRemoteOcr" i18n>Use remote OCR</label>
<div class="form-text" i18n>Sends the document to the configured remote OCR service, which may incur costs.</div>
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}}
</button>
</div>
@@ -1,72 +0,0 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { RemoteOCRModeConfig } from 'src/app/data/paperless-config'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from 'src/app/services/settings.service'
import { ReprocessConfirmDialogComponent } from './reprocess-confirm-dialog.component'
describe('ReprocessConfirmDialogComponent', () => {
let component: ReprocessConfirmDialogComponent
let fixture: ComponentFixture<ReprocessConfirmDialogComponent>
let settingsService: SettingsService
const createComponent = (configured: boolean, mode: string) => {
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED, configured)
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_MODE, mode)
fixture = TestBed.createComponent(ReprocessConfirmDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
}
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
imports: [ReprocessConfirmDialogComponent],
}).compileComponents()
settingsService = TestBed.inject(SettingsService)
})
it('should not request remote OCR by default', () => {
createComponent(true, RemoteOCRModeConfig.WORKFLOW_ONLY)
expect(component.remoteOcr).toBeFalsy()
})
it('should not offer remote OCR when no engine is configured', () => {
createComponent(false, RemoteOCRModeConfig.WORKFLOW_ONLY)
expect(component.showRemoteOcr).toBeFalsy()
expect(
fixture.nativeElement.querySelector('#reprocessRemoteOcr')
).toBeNull()
})
it('should not offer remote OCR when it already handles every document', () => {
createComponent(true, RemoteOCRModeConfig.ALWAYS)
expect(component.showRemoteOcr).toBeFalsy()
expect(
fixture.nativeElement.querySelector('#reprocessRemoteOcr')
).toBeNull()
})
it('should offer remote OCR when configured and selective', () => {
createComponent(true, RemoteOCRModeConfig.WORKFLOW_ONLY)
expect(component.showRemoteOcr).toBeTruthy()
const checkbox = fixture.nativeElement.querySelector('#reprocessRemoteOcr')
expect(checkbox).not.toBeNull()
checkbox.click()
fixture.detectChanges()
expect(component.remoteOcr).toBeTruthy()
})
})
@@ -1,20 +0,0 @@
import { Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { SettingsService } from 'src/app/services/settings.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-reprocess-confirm-dialog',
templateUrl: './reprocess-confirm-dialog.component.html',
imports: [FormsModule],
})
export class ReprocessConfirmDialogComponent extends ConfirmDialogComponent {
private settings = inject(SettingsService)
remoteOcr: boolean = false
public get showRemoteOcr(): boolean {
// Hidden when it is not configured, or when it already handles every document anyway.
return this.settings.remoteOCRIsSelectable
}
}
@@ -34,10 +34,10 @@
<p class="mb-0 small"><b>{{messageBold}}</b></p> <p class="mb-0 small"><b>{{messageBold}}</b></p>
} }
</div> </div>
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled"> <button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span> <span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button> </button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled || degrees === 0"> <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled() || degrees === 0">
{{btnCaption}} {{btnCaption}}
@if (!confirmButtonEnabled) { @if (!confirmButtonEnabled) {
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar> <ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
@@ -455,13 +455,6 @@
</div> </div>
</div> </div>
} }
@case (WorkflowActionType.RemoteOcr) {
<div class="row">
<div class="col">
<p class="text-muted small" i18n>The document will be sent to the configured remote OCR service. May incur costs.</p>
</div>
</div>
}
} }
</div> </div>
</ng-template> </ng-template>
@@ -29,7 +29,6 @@ import {
DocumentSource, DocumentSource,
WorkflowTriggerType, WorkflowTriggerType,
} from 'src/app/data/workflow-trigger' } from 'src/app/data/workflow-trigger'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
@@ -225,12 +224,7 @@ describe('WorkflowEditDialogComponent', () => {
).toEqual('Document Added') ).toEqual('Document Added')
expect(component.getTriggerTypeOptionName(null)).toEqual('') expect(component.getTriggerTypeOptionName(null)).toEqual('')
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS) expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
// Remote OCR is absent until the workflow has a consumption trigger expect(component.actionTypeOptions).toEqual(WORKFLOW_ACTION_OPTIONS)
expect(component.actionTypeOptions).toEqual(
WORKFLOW_ACTION_OPTIONS.filter(
(a) => a.id !== WorkflowActionType.RemoteOcr
)
)
expect( expect(
component.getActionTypeOptionName(WorkflowActionType.Assignment) component.getActionTypeOptionName(WorkflowActionType.Assignment)
).toEqual('Assignment') ).toEqual('Assignment')
@@ -243,104 +237,7 @@ describe('WorkflowEditDialogComponent', () => {
jest.spyOn(settingsService, 'get').mockReturnValue(false) jest.spyOn(settingsService, 'get').mockReturnValue(false)
component.ngOnInit() component.ngOnInit()
expect(component.actionTypeOptions).toEqual( expect(component.actionTypeOptions).toEqual(
WORKFLOW_ACTION_OPTIONS.filter( WORKFLOW_ACTION_OPTIONS.filter((a) => a.id !== WorkflowActionType.Email)
(a) =>
a.id !== WorkflowActionType.Email &&
a.id !== WorkflowActionType.RemoteOcr
)
)
})
it('should offer remote OCR only for consumption workflows', () => {
jest.spyOn(settingsService, 'get').mockReturnValue(true)
// A consumption trigger makes the action reachable
component.object = {
name: 'Workflow 1',
order: 0,
enabled: true,
triggers: [{ type: WorkflowTriggerType.Consumption }],
actions: [],
} as Workflow
component.ngOnInit()
expect(component.actionTypeOptions.map((a) => a.id)).toContain(
WorkflowActionType.RemoteOcr
)
// Any other trigger type runs after the document has been parsed
component.object = {
name: 'Workflow 2',
order: 0,
enabled: true,
triggers: [{ type: WorkflowTriggerType.DocumentAdded }],
actions: [],
} as Workflow
component.ngOnInit()
expect(component.actionTypeOptions.map((a) => a.id)).not.toContain(
WorkflowActionType.RemoteOcr
)
})
it('should offer remote OCR on a trigger added to a new workflow', () => {
jest.spyOn(settingsService, 'get').mockReturnValue(true)
component.ngOnInit()
// Nothing for the action to apply to yet
expect(component.actionTypeOptions.map((a) => a.id)).not.toContain(
WorkflowActionType.RemoteOcr
)
// addTrigger creates the form field with emitEvent false, so the options
// have to be computed on read rather than cached from valueChanges
component.addTrigger()
expect(component.actionTypeOptions.map((a) => a.id)).toContain(
WorkflowActionType.RemoteOcr
)
// Switching that trigger to a type that runs after parsing removes it
component.triggerFields
.at(0)
.get('type')
.setValue(WorkflowTriggerType.DocumentAdded)
expect(component.actionTypeOptions.map((a) => a.id)).not.toContain(
WorkflowActionType.RemoteOcr
)
})
it('should keep remote OCR listed when an action already uses it', () => {
jest.spyOn(settingsService, 'get').mockReturnValue(true)
// Otherwise changing the trigger would silently blank the selection
component.object = {
name: 'Workflow 1',
order: 0,
enabled: true,
triggers: [{ type: WorkflowTriggerType.DocumentAdded }],
actions: [{ type: WorkflowActionType.RemoteOcr }],
} as Workflow
component.ngOnInit()
expect(component.actionTypeOptions.map((a) => a.id)).toContain(
WorkflowActionType.RemoteOcr
)
})
it('should not offer remote OCR when no engine is configured', () => {
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) => key !== SETTINGS_KEYS.REMOTE_OCR_CONFIGURED)
component.object = {
name: 'Workflow 1',
order: 0,
enabled: true,
triggers: [{ type: WorkflowTriggerType.Consumption }],
actions: [],
} as Workflow
component.ngOnInit()
expect(component.actionTypeOptions.map((a) => a.id)).not.toContain(
WorkflowActionType.RemoteOcr
) )
}) })
@@ -148,10 +148,6 @@ export const WORKFLOW_ACTION_OPTIONS = [
id: WorkflowActionType.MoveToTrash, id: WorkflowActionType.MoveToTrash,
name: $localize`Move to trash`, name: $localize`Move to trash`,
}, },
{
id: WorkflowActionType.RemoteOcr,
name: $localize`Remote OCR`,
},
] ]
export enum TriggerFilterType { export enum TriggerFilterType {
@@ -508,6 +504,8 @@ export class WorkflowEditDialogComponent
expandedItem: number = null expandedItem: number = null
readonly allowedActionTypes = signal([])
private readonly triggerFilterOptionsMap = new WeakMap< private readonly triggerFilterOptionsMap = new WeakMap<
FormArray, FormArray,
TriggerFilterOption[] TriggerFilterOption[]
@@ -550,40 +548,13 @@ export class WorkflowEditDialogComponent
this.checkRemovalActionFields.bind(this) this.checkRemovalActionFields.bind(this)
) )
this.checkRemovalActionFields(this.objectForm.value) this.checkRemovalActionFields(this.objectForm.value)
} this.allowedActionTypes.set(
this.settingsService.get(SETTINGS_KEYS.EMAIL_ENABLED)
private allowedActionTypes: typeof WORKFLOW_ACTION_OPTIONS = null ? WORKFLOW_ACTION_OPTIONS
: WORKFLOW_ACTION_OPTIONS.filter(
private getAllowedActionTypes() { (a) => a.id !== WorkflowActionType.Email
let allowed = WORKFLOW_ACTION_OPTIONS )
)
if (!this.settingsService.get(SETTINGS_KEYS.EMAIL_ENABLED)) {
allowed = allowed.filter((a) => a.id !== WorkflowActionType.Email)
}
// Remote OCR is decided before the document is parsed, so it is only
// offered for workflows that run at consumption.
const formWorkflow: Workflow = this.objectForm?.value
const remoteOcrUsable =
this.settingsService.get(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED) &&
(formWorkflow?.triggers?.some(
(trigger) => trigger.type === WorkflowTriggerType.Consumption
) ||
formWorkflow?.actions?.some(
(action) => action.type === WorkflowActionType.RemoteOcr
))
if (!remoteOcrUsable) {
allowed = allowed.filter((a) => a.id !== WorkflowActionType.RemoteOcr)
}
if (
this.allowedActionTypes?.length === allowed.length &&
this.allowedActionTypes.every((a, i) => a.id === allowed[i].id)
) {
return this.allowedActionTypes
}
this.allowedActionTypes = allowed
return allowed
} }
private checkRemovalActionFields(formWorkflow: Workflow) { private checkRemovalActionFields(formWorkflow: Workflow) {
@@ -1308,8 +1279,7 @@ export class WorkflowEditDialogComponent
get actionTypeOptions() { get actionTypeOptions() {
this.settingsService.trackChanges() this.settingsService.trackChanges()
// Computed on read rather than cached return this.allowedActionTypes()
return this.getAllowedActionTypes()
} }
getActionTypeOptionName(type: WorkflowActionType): string { getActionTypeOptionName(type: WorkflowActionType): string {
@@ -100,7 +100,7 @@
</div> </div>
} }
<div class="form-group ms-md-auto"> <div class="form-group ms-md-auto">
<button type="button" class="btn me-2" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">{{ cancelBtnCaption }}</button> <button type="button" class="btn me-2" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">{{ cancelBtnCaption }}</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button> <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
</div> </div>
</div> </div>
@@ -119,7 +119,7 @@
type="button" type="button"
class="btn btn-primary btn-sm d-inline-flex align-items-center gap-2 text-nowrap" class="btn btn-primary btn-sm d-inline-flex align-items-center gap-2 text-nowrap"
(click)="submit()" (click)="submit()"
[disabled]="loading() || !buttonsEnabled"> [disabled]="loading() || !buttonsEnabled()">
@if (loading()) { @if (loading()) {
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
} }
@@ -69,7 +69,7 @@ describe('ShareLinkBundleDialogComponent', () => {
file_version: FileVersion.Original, file_version: FileVersion.Original,
expiration_days: 3, expiration_days: 3,
}) })
expect(component.buttonsEnabled).toBe(false) expect(component.buttonsEnabled()).toBe(false)
expect(confirmSpy).toHaveBeenCalled() expect(confirmSpy).toHaveBeenCalled()
component.form.setValue({ component.form.setValue({
@@ -78,7 +78,7 @@ export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent {
: FileVersion.Original, : FileVersion.Original,
expiration_days: this.form.value.expirationDays, expiration_days: this.form.value.expirationDays,
} }
this.buttonsEnabled = false this.buttonsEnabled.set(false)
super.confirm() super.confirm()
} }
@@ -963,24 +963,12 @@ describe('DocumentDetailComponent', () => {
component.reprocess() component.reprocess()
const modalCloseSpy = jest.spyOn(openModal, 'close') const modalCloseSpy = jest.spyOn(openModal, 'close')
openModal.componentInstance.confirmClicked.next() openModal.componentInstance.confirmClicked.next()
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] }, false) expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] })
expect(modalSpy).toHaveBeenCalled() expect(modalSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled()
expect(modalCloseSpy).toHaveBeenCalled() expect(modalCloseSpy).toHaveBeenCalled()
}) })
it('should pass remote OCR choice when reprocessing', () => {
initNormally()
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
reprocessSpy.mockReturnValue(of(true))
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
component.reprocess()
openModal.componentInstance.remoteOcr = true
openModal.componentInstance.confirmClicked.next()
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] }, true)
})
it('should show error if redo ocr call fails', () => { it('should show error if redo ocr call fails', () => {
initNormally() initNormally()
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments') const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
@@ -1576,7 +1564,7 @@ describe('DocumentDetailComponent', () => {
dialog.confirmClicked.next() dialog.confirmClicked.next()
await openModal.result await openModal.result
expect(dialog.buttonsEnabled).toBe(false) expect(dialog.buttonsEnabled()).toBe(false)
expect(reloadSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled()
expect((component as any).incomingUpdateModal).toBeNull() expect((component as any).incomingUpdateModal).toBeNull()
}) })
@@ -1801,7 +1789,7 @@ describe('DocumentDetailComponent', () => {
expect(errorSpy).toHaveBeenCalled() expect(errorSpy).toHaveBeenCalled()
expect(component.networkActive()).toBe(false) expect(component.networkActive()).toBe(false)
expect(dialog.buttonsEnabled).toBe(true) expect(dialog.buttonsEnabled()).toBe(true)
}) })
it('should refresh the document when removing password in update mode', () => { it('should refresh the document when removing password in update mode', () => {
@@ -97,7 +97,6 @@ import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import * as UTIF from 'utif' import * as UTIF from 'utif'
import { DocumentDetailFieldID } from '../admin/settings/settings.component' import { DocumentDetailFieldID } from '../admin/settings/settings.component'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
import { ReprocessConfirmDialogComponent } from '../common/confirm-dialog/reprocess-confirm-dialog/reprocess-confirm-dialog.component'
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component' import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component' import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
@@ -660,7 +659,7 @@ export class DocumentDetailComponent
modal.componentInstance.cancelBtnCaption = $localize`Dismiss` modal.componentInstance.cancelBtnCaption = $localize`Dismiss`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
modal.close() modal.close()
this.reloadRemoteVersion() this.reloadRemoteVersion()
}) })
@@ -1375,7 +1374,7 @@ export class DocumentDetailComponent
modal.componentInstance.confirmClicked modal.componentInstance.confirmClicked
.pipe( .pipe(
switchMap(() => { switchMap(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
return this.documentsService.delete(this.document()) return this.documentsService.delete(this.document())
}) })
) )
@@ -1387,7 +1386,7 @@ export class DocumentDetailComponent
}, },
error: (error) => { error: (error) => {
this.toastService.showError($localize`Error deleting document`, error) this.toastService.showError($localize`Error deleting document`, error)
modal.componentInstance.buttonsEnabled = true modal.componentInstance.buttonsEnabled.set(true)
this.subscribeModalDelete(modal) this.subscribeModalDelete(modal)
}, },
}) })
@@ -1403,7 +1402,7 @@ export class DocumentDetailComponent
} }
reprocess() { reprocess() {
let modal = this.modalService.open(ReprocessConfirmDialogComponent, { let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
}) })
modal.componentInstance.title = $localize`Reprocess confirm` modal.componentInstance.title = $localize`Reprocess confirm`
@@ -1412,12 +1411,9 @@ export class DocumentDetailComponent
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.documentsService this.documentsService
.reprocessDocuments( .reprocessDocuments({ documents: [this.document().id] })
{ documents: [this.document().id] },
modal.componentInstance.remoteOcr
)
.subscribe({ .subscribe({
next: () => { next: () => {
this.toastService.showInfo( this.toastService.showInfo(
@@ -1429,7 +1425,7 @@ export class DocumentDetailComponent
}, },
error: (error) => { error: (error) => {
if (modal) { if (modal) {
modal.componentInstance.buttonsEnabled = true modal.componentInstance.buttonsEnabled.set(true)
} }
this.toastService.showError( this.toastService.showError(
$localize`Error executing operation`, $localize`Error executing operation`,
@@ -1802,7 +1798,7 @@ export class DocumentDetailComponent
modal.componentInstance.confirmClicked modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => { .subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.documentsService this.documentsService
.editPdfDocuments([sourceDocumentId], { .editPdfDocuments([sourceDocumentId], {
operations: modal.componentInstance.getOperations(), operations: modal.componentInstance.getOperations(),
@@ -1825,7 +1821,7 @@ export class DocumentDetailComponent
}, },
error: (error) => { error: (error) => {
if (modal) { if (modal) {
modal.componentInstance.buttonsEnabled = true modal.componentInstance.buttonsEnabled.set(true)
} }
this.toastService.showError( this.toastService.showError(
$localize`Error executing PDF edit operation`, $localize`Error executing PDF edit operation`,
@@ -1859,7 +1855,7 @@ export class DocumentDetailComponent
const sourceDocumentId = this.selectedVersionId() ?? this.document().id const sourceDocumentId = this.selectedVersionId() ?? this.document().id
const dialog = const dialog =
modal.componentInstance as PasswordRemovalConfirmDialogComponent modal.componentInstance as PasswordRemovalConfirmDialogComponent
dialog.buttonsEnabled = false dialog.buttonsEnabled.set(false)
this.networkActive.set(true) this.networkActive.set(true)
this.documentsService this.documentsService
.removePasswordDocuments([sourceDocumentId], { .removePasswordDocuments([sourceDocumentId], {
@@ -1884,7 +1880,7 @@ export class DocumentDetailComponent
} }
}, },
error: (error) => { error: (error) => {
dialog.buttonsEnabled = true dialog.buttonsEnabled.set(true)
this.networkActive.set(false) this.networkActive.set(false)
this.toastService.showError( this.toastService.showError(
$localize`Error executing password removal operation`, $localize`Error executing password removal operation`,
@@ -1122,7 +1122,6 @@ describe('BulkEditorComponent', () => {
req.flush(true) req.flush(true)
expect(req.request.body).toEqual({ expect(req.request.body).toEqual({
documents: [3, 4], documents: [3, 4],
remote_ocr: false,
}) })
httpTestingController.match( httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
@@ -1684,7 +1683,7 @@ describe('BulkEditorComponent', () => {
expiration_days: 7, expiration_days: 7,
}, },
loading: signal(false), loading: signal(false),
buttonsEnabled: true, buttonsEnabled: signal(true),
copied: signal(false), copied: signal(false),
}, },
} }
@@ -1716,7 +1715,7 @@ describe('BulkEditorComponent', () => {
expiration_days: 7, expiration_days: 7,
}) })
expect(dialogInstance.loading()).toBe(false) expect(dialogInstance.loading()).toBe(false)
expect(dialogInstance.buttonsEnabled).toBe(false) expect(dialogInstance.buttonsEnabled()).toBe(false)
expect(dialogInstance.createdBundle).toEqual({ id: 42 }) expect(dialogInstance.createdBundle).toEqual({ id: 42 })
expect(typeof dialogInstance.onOpenManage).toBe('function') expect(typeof dialogInstance.onOpenManage).toBe('function')
expect(toastInfoSpy).toHaveBeenCalledWith( expect(toastInfoSpy).toHaveBeenCalledWith(
@@ -1756,7 +1755,7 @@ describe('BulkEditorComponent', () => {
expiration_days: null, expiration_days: null,
}, },
loading: signal(false), loading: signal(false),
buttonsEnabled: true, buttonsEnabled: signal(true),
}, },
} }
@@ -1778,7 +1777,7 @@ describe('BulkEditorComponent', () => {
expect.any(Error) expect.any(Error)
) )
expect(dialogInstance.loading()).toBe(false) expect(dialogInstance.loading()).toBe(false)
expect(dialogInstance.buttonsEnabled).toBe(true) expect(dialogInstance.buttonsEnabled()).toBe(true)
openSpy.mockRestore() openSpy.mockRestore()
}) })
@@ -51,7 +51,6 @@ import { ToastService } from 'src/app/services/toast.service'
import { flattenTags } from 'src/app/utils/flatten-tags' import { flattenTags } from 'src/app/utils/flatten-tags'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params' import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component' import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
import { ReprocessConfirmDialogComponent } from '../../common/confirm-dialog/reprocess-confirm-dialog/reprocess-confirm-dialog.component'
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@@ -274,7 +273,7 @@ export class BulkEditorComponent
overrideSelection?: DocumentSelectionQuery overrideSelection?: DocumentSelectionQuery
) { ) {
if (modal) { if (modal) {
this.setModalButtonsEnabled(modal, false) modal.componentInstance.buttonsEnabled.set(false)
} }
this.documentService this.documentService
.bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args) .bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args)
@@ -291,7 +290,7 @@ export class BulkEditorComponent
options: { deleteOriginals?: boolean } = {} options: { deleteOriginals?: boolean } = {}
) { ) {
if (modal) { if (modal) {
this.setModalButtonsEnabled(modal, false) modal.componentInstance.buttonsEnabled.set(false)
} }
request.pipe(first()).subscribe({ request.pipe(first()).subscribe({
next: () => { next: () => {
@@ -321,7 +320,7 @@ export class BulkEditorComponent
private handleOperationError(modal: NgbModalRef, error: any) { private handleOperationError(modal: NgbModalRef, error: any) {
if (modal) { if (modal) {
this.setModalButtonsEnabled(modal, true) modal.componentInstance.buttonsEnabled.set(true)
} }
this.toastService.showError( this.toastService.showError(
$localize`Error executing bulk operation`, $localize`Error executing bulk operation`,
@@ -329,15 +328,6 @@ export class BulkEditorComponent
) )
} }
private setModalButtonsEnabled(modal: NgbModalRef, enabled: boolean) {
const buttonsEnabled = modal.componentInstance.buttonsEnabled
if (typeof buttonsEnabled?.set === 'function') {
buttonsEnabled.set(enabled)
} else {
modal.componentInstance.buttonsEnabled = enabled
}
}
private applySelectionData( private applySelectionData(
items: SelectionDataItem[], items: SelectionDataItem[],
selectionModel: FilterableDropdownSelectionModel selectionModel: FilterableDropdownSelectionModel
@@ -873,7 +863,7 @@ export class BulkEditorComponent
modal.componentInstance.confirmClicked modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => { .subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.executeDocumentAction( this.executeDocumentAction(
modal, modal,
this.documentService.deleteDocuments(this.getSelectionQuery()) this.documentService.deleteDocuments(this.getSelectionQuery())
@@ -910,7 +900,7 @@ export class BulkEditorComponent
} }
reprocessSelected() { reprocessSelected() {
let modal = this.modalService.open(ReprocessConfirmDialogComponent, { let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
}) })
modal.componentInstance.title = $localize`Reprocess confirm` modal.componentInstance.title = $localize`Reprocess confirm`
@@ -921,13 +911,10 @@ export class BulkEditorComponent
modal.componentInstance.confirmClicked modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => { .subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.executeDocumentAction( this.executeDocumentAction(
modal, modal,
this.documentService.reprocessDocuments( this.documentService.reprocessDocuments(this.getSelectionQuery())
this.getSelectionQuery(),
modal.componentInstance.remoteOcr
)
) )
}) })
} }
@@ -961,7 +948,7 @@ export class BulkEditorComponent
rotateDialog.confirmClicked rotateDialog.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => { .subscribe(() => {
rotateDialog.buttonsEnabled = false rotateDialog.buttonsEnabled.set(false)
this.executeDocumentAction( this.executeDocumentAction(
modal, modal,
this.documentService.rotateDocuments( this.documentService.rotateDocuments(
@@ -994,7 +981,7 @@ export class BulkEditorComponent
if (mergeDialog.archiveFallback()) { if (mergeDialog.archiveFallback()) {
args.archive_fallback = true args.archive_fallback = true
} }
mergeDialog.buttonsEnabled = false mergeDialog.buttonsEnabled.set(false)
this.executeDocumentAction( this.executeDocumentAction(
modal, modal,
this.documentService.mergeDocuments(mergeDialog.documentIDs(), args), this.documentService.mergeDocuments(mergeDialog.documentIDs(), args),
@@ -1067,14 +1054,14 @@ export class BulkEditorComponent
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => { .subscribe(() => {
dialog.loading.set(true) dialog.loading.set(true)
dialog.buttonsEnabled = false dialog.buttonsEnabled.set(false)
this.shareLinkBundleService this.shareLinkBundleService
.createBundle(dialog.payload) .createBundle(dialog.payload)
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
next: (result) => { next: (result) => {
dialog.loading.set(false) dialog.loading.set(false)
dialog.buttonsEnabled = false dialog.buttonsEnabled.set(false)
dialog.createdBundle = result dialog.createdBundle = result
dialog.copied.set(false) dialog.copied.set(false)
dialog.payload = null dialog.payload = null
@@ -1088,7 +1075,7 @@ export class BulkEditorComponent
}, },
error: (error) => { error: (error) => {
dialog.loading.set(false) dialog.loading.set(false)
dialog.buttonsEnabled = true dialog.buttonsEnabled.set(true)
this.toastService.showError( this.toastService.showError(
$localize`Share link bundle creation is not available yet.`, $localize`Share link bundle creation is not available yet.`,
error error
@@ -105,7 +105,7 @@ export class CustomFieldsComponent
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.customFieldsService.delete(field).subscribe({ this.customFieldsService.delete(field).subscribe({
next: () => { next: () => {
modal.close() modal.close()
@@ -274,7 +274,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
activeModal.componentInstance.btnClass = 'btn-danger' activeModal.componentInstance.btnClass = 'btn-danger'
activeModal.componentInstance.btnCaption = $localize`Delete` activeModal.componentInstance.btnCaption = $localize`Delete`
activeModal.componentInstance.confirmClicked.subscribe(() => { activeModal.componentInstance.confirmClicked.subscribe(() => {
activeModal.componentInstance.buttonsEnabled = false activeModal.componentInstance.buttonsEnabled.set(false)
this.service this.service
.delete(object) .delete(object)
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
@@ -284,7 +284,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
this.reloadData() this.reloadData()
}, },
error: (error) => { error: (error) => {
activeModal.componentInstance.buttonsEnabled = true activeModal.componentInstance.buttonsEnabled.set(true)
this.toastService.showError( this.toastService.showError(
$localize`Error while deleting element`, $localize`Error while deleting element`,
error error
@@ -455,7 +455,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.service this.service
.bulk_edit_objects( .bulk_edit_objects(
this.allSelectionActive ? [] : Array.from(this.selectedObjects), this.allSelectionActive ? [] : Array.from(this.selectedObjects),
@@ -472,7 +472,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
this.reloadData() this.reloadData()
}, },
error: (error) => { error: (error) => {
modal.componentInstance.buttonsEnabled = true modal.componentInstance.buttonsEnabled.set(true)
this.toastService.showError( this.toastService.showError(
$localize`Error deleting objects`, $localize`Error deleting objects`,
error error
@@ -196,7 +196,7 @@ export class MailComponent
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.mailAccountService.delete(account).subscribe({ this.mailAccountService.delete(account).subscribe({
next: () => { next: () => {
modal.close() modal.close()
@@ -298,7 +298,7 @@ export class MailComponent
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.mailRuleService.delete(rule).subscribe({ this.mailRuleService.delete(rule).subscribe({
next: () => { next: () => {
modal.close() modal.close()
@@ -134,7 +134,7 @@ export class WorkflowsComponent
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
this.workflowService.delete(workflow).subscribe({ this.workflowService.delete(workflow).subscribe({
next: () => { next: () => {
modal.close() modal.close()
-55
View File
@@ -54,10 +54,6 @@ export const ConfigCategory = {
AI: $localize`AI Settings`, AI: $localize`AI Settings`,
} }
export const ConfigSection = {
RemoteOCR: $localize`Remote OCR`,
}
export const LLMEmbeddingBackendConfig = { export const LLMEmbeddingBackendConfig = {
OPENAI_LIKE: 'openai-like', OPENAI_LIKE: 'openai-like',
HUGGINGFACE: 'huggingface', HUGGINGFACE: 'huggingface',
@@ -69,15 +65,6 @@ export const LLMBackendConfig = {
OLLAMA: 'ollama', OLLAMA: 'ollama',
} }
export const RemoteOCREngineConfig = {
AZURE_AI: 'azureai',
}
export const RemoteOCRModeConfig = {
ALWAYS: 'always',
WORKFLOW_ONLY: 'workflow_only',
}
export interface ConfigOption { export interface ConfigOption {
key: string key: string
title: string title: string
@@ -85,7 +72,6 @@ export interface ConfigOption {
choices?: Array<{ id: string; name: string }> choices?: Array<{ id: string; name: string }>
config_key?: string config_key?: string
category: string category: string
section?: string
note?: string note?: string
} }
@@ -195,43 +181,6 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_OCR_USER_ARGS', config_key: 'PAPERLESS_OCR_USER_ARGS',
category: ConfigCategory.OCR, category: ConfigCategory.OCR,
}, },
{
key: 'remote_ocr_engine',
title: $localize`Remote OCR Engine`,
type: ConfigOptionType.Select,
choices: mapToItems(RemoteOCREngineConfig),
config_key: 'PAPERLESS_REMOTE_OCR_ENGINE',
category: ConfigCategory.OCR,
section: ConfigSection.RemoteOCR,
note: $localize`Enabling remote OCR sends documents to a third-party service for processing. Consider the privacy implications as well as potential costs before enabling.`,
},
{
key: 'remote_ocr_api_key',
title: $localize`Remote OCR API Key`,
type: ConfigOptionType.Password,
config_key: 'PAPERLESS_REMOTE_OCR_API_KEY',
category: ConfigCategory.OCR,
section: ConfigSection.RemoteOCR,
},
{
key: 'remote_ocr_endpoint',
title: $localize`Remote OCR Endpoint`,
type: ConfigOptionType.String,
config_key: 'PAPERLESS_REMOTE_OCR_ENDPOINT',
category: ConfigCategory.OCR,
section: ConfigSection.RemoteOCR,
note: $localize`Required when using the Azure AI engine.`,
},
{
key: 'remote_ocr_mode',
title: $localize`Remote OCR Mode`,
type: ConfigOptionType.Select,
choices: mapToItems(RemoteOCRModeConfig),
config_key: 'PAPERLESS_REMOTE_OCR_MODE',
category: ConfigCategory.OCR,
section: ConfigSection.RemoteOCR,
note: $localize`Which documents are sent to the remote engine. Use 'workflow_only' to keep remote OCR off unless a workflow enables it for a document.`,
},
{ {
key: 'app_logo', key: 'app_logo',
title: $localize`Application Logo`, title: $localize`Application Logo`,
@@ -449,10 +398,6 @@ export interface PaperlessConfig extends ObjectWithId {
barcode_enable_tag: boolean barcode_enable_tag: boolean
barcode_tag_mapping: object barcode_tag_mapping: object
barcode_tag_split: boolean barcode_tag_split: boolean
remote_ocr_engine: string
remote_ocr_api_key: string
remote_ocr_endpoint: string
remote_ocr_mode: string
ai_enabled: boolean ai_enabled: boolean
llm_embedding_backend: string llm_embedding_backend: string
llm_embedding_model: string llm_embedding_model: string
-13
View File
@@ -1,6 +1,5 @@
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode' import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types' import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types'
import { RemoteOCRModeConfig } from './paperless-config'
import { User } from './user' import { User } from './user'
export interface UiSettings { export interface UiSettings {
@@ -95,8 +94,6 @@ export const SETTINGS_KEYS = {
OUTLOOK_OAUTH_URL: 'outlook_oauth_url', OUTLOOK_OAUTH_URL: 'outlook_oauth_url',
EMAIL_ENABLED: 'email_enabled', EMAIL_ENABLED: 'email_enabled',
AI_ENABLED: 'ai_enabled', AI_ENABLED: 'ai_enabled',
REMOTE_OCR_CONFIGURED: 'remote_ocr:configured',
REMOTE_OCR_MODE: 'remote_ocr:mode',
} }
export const SETTINGS: UiSetting[] = [ export const SETTINGS: UiSetting[] = [
@@ -350,14 +347,4 @@ export const SETTINGS: UiSetting[] = [
type: 'string', type: 'string',
default: PdfEditorEditMode.Create, default: PdfEditorEditMode.Create,
}, },
{
key: SETTINGS_KEYS.REMOTE_OCR_CONFIGURED,
type: 'boolean',
default: false,
},
{
key: SETTINGS_KEYS.REMOTE_OCR_MODE,
type: 'string',
default: RemoteOCRModeConfig.ALWAYS,
},
] ]
-1
View File
@@ -7,7 +7,6 @@ export enum WorkflowActionType {
Webhook = 4, Webhook = 4,
PasswordRemoval = 5, PasswordRemoval = 5,
MoveToTrash = 6, MoveToTrash = 6,
RemoteOcr = 7,
} }
export interface WorkflowActionEmail extends ObjectWithId { export interface WorkflowActionEmail extends ObjectWithId {
+1 -1
View File
@@ -18,7 +18,7 @@ export class DirtyFormGuard extends DirtyCheckGuard {
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Leave page` modal.componentInstance.btnCaption = $localize`Leave page`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
modal.close() modal.close()
}) })
const subject = new Subject<boolean>() const subject = new Subject<boolean>()
@@ -36,12 +36,12 @@ export class DirtySavedViewGuard {
modal.componentInstance.alternativeBtnClass = 'btn-primary' modal.componentInstance.alternativeBtnClass = 'btn-primary'
modal.componentInstance.alternativeBtnCaption = $localize`Save and close` modal.componentInstance.alternativeBtnCaption = $localize`Save and close`
modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => { modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
component.saveViewConfig() component.saveViewConfig()
modal.close() modal.close()
}) })
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
modal.close() modal.close()
}) })
@@ -142,7 +142,7 @@ export class OpenDocumentsService {
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Close document` modal.componentInstance.btnCaption = $localize`Close document`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
modal.close() modal.close()
this.openDocuments.splice(index, 1) this.openDocuments.splice(index, 1)
this.dirtyDocuments.delete(doc.id) this.dirtyDocuments.delete(doc.id)
@@ -165,7 +165,7 @@ export class OpenDocumentsService {
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Close documents` modal.componentInstance.btnCaption = $localize`Close documents`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled.set(false)
modal.close() modal.close()
this.openDocuments.splice(0, this.openDocuments.length) this.openDocuments.splice(0, this.openDocuments.length)
this.dirtyDocuments.clear() this.dirtyDocuments.clear()
@@ -284,21 +284,6 @@ describe(`DocumentService`, () => {
expect(req.request.method).toEqual('POST') expect(req.request.method).toEqual('POST')
expect(req.request.body).toEqual({ expect(req.request.body).toEqual({
documents: ids, documents: ids,
remote_ocr: false,
})
})
it('should request remote OCR when reprocessing with it enabled', () => {
const ids = [1, 2, 3]
subscription = service
.reprocessDocuments({ documents: ids }, true)
.subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/reprocess/`
)
expect(req.request.body).toEqual({
documents: ids,
remote_ocr: true,
}) })
}) })
@@ -349,13 +349,9 @@ export class DocumentService extends AbstractPaperlessService<Document> {
}) })
} }
reprocessDocuments( reprocessDocuments(selection: DocumentSelectionQuery) {
selection: DocumentSelectionQuery,
remoteOcr: boolean = false
) {
return this.http.post(this.getResourceUrl(null, 'reprocess'), { return this.http.post(this.getResourceUrl(null, 'reprocess'), {
...selection, ...selection,
remote_ocr: remoteOcr,
}) })
} }
@@ -13,7 +13,6 @@ import { environment } from 'src/environments/environment'
import { CustomFieldDataType } from '../data/custom-field' import { CustomFieldDataType } from '../data/custom-field'
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document' import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
import { SavedView } from '../data/saved-view' import { SavedView } from '../data/saved-view'
import { RemoteOCRModeConfig } from '../data/paperless-config'
import { SETTINGS_KEYS, UiSettings } from '../data/ui-settings' import { SETTINGS_KEYS, UiSettings } from '../data/ui-settings'
import { PermissionsService } from './permissions.service' import { PermissionsService } from './permissions.service'
import { CustomFieldsService } from './rest/custom-fields.service' import { CustomFieldsService } from './rest/custom-fields.service'
@@ -435,26 +434,4 @@ describe('SettingsService', () => {
).name ).name
).toEqual(customFields[0].name) ).toEqual(customFields[0].name)
}) })
it('should offer remote OCR only when configured and selective', () => {
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED, false)
settingsService.set(
SETTINGS_KEYS.REMOTE_OCR_MODE,
RemoteOCRModeConfig.WORKFLOW_ONLY
)
expect(settingsService.remoteOCRIsSelectable).toBeFalsy()
// configured, but already handling every document
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED, true)
settingsService.set(
SETTINGS_KEYS.REMOTE_OCR_MODE,
RemoteOCRModeConfig.ALWAYS
)
expect(settingsService.remoteOCRIsSelectable).toBeFalsy()
settingsService.set(
SETTINGS_KEYS.REMOTE_OCR_MODE,
RemoteOCRModeConfig.WORKFLOW_ONLY
)
expect(settingsService.remoteOCRIsSelectable).toBeTruthy()
})
}) })
@@ -19,7 +19,6 @@ import {
} from 'src/app/utils/color' } from 'src/app/utils/color'
import { DEFAULT_APP_TITLE, environment } from 'src/environments/environment' import { DEFAULT_APP_TITLE, environment } from 'src/environments/environment'
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document' import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
import { RemoteOCRModeConfig } from '../data/paperless-config'
import { SavedView } from '../data/saved-view' import { SavedView } from '../data/saved-view'
import { import {
PAPERLESS_GREEN_HEX, PAPERLESS_GREEN_HEX,
@@ -688,17 +687,6 @@ export class SettingsService {
return this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) return this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)
} }
/**
* Offering remote OCR as a choice only makes sense when an engine
* is configured but is not already handling every document.
*/
get remoteOCRIsSelectable(): boolean {
return (
this.get(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED) &&
this.get(SETTINGS_KEYS.REMOTE_OCR_MODE) !== RemoteOCRModeConfig.ALWAYS
)
}
offerTour(): boolean { offerTour(): boolean {
return this.dashboardIsEmpty() && !this.get(SETTINGS_KEYS.TOUR_COMPLETE) return this.dashboardIsEmpty() && !this.get(SETTINGS_KEYS.TOUR_COMPLETE)
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -8
View File
@@ -394,16 +394,10 @@ def delete(doc_ids: list[int]) -> Literal["OK"]:
return "OK" return "OK"
def reprocess(doc_ids: list[int], *, remote_ocr: bool = False) -> Literal["OK"]: def reprocess(doc_ids: list[int]) -> Literal["OK"]:
"""
Re-run parsing for the given documents.
Consumption workflows do not run here, so ``remote_ocr`` is how the user
asks for the remote engine when it is not configured to handle everything.
"""
for document_id in doc_ids: for document_id in doc_ids:
update_document_content_maybe_archive_file.apply_async( update_document_content_maybe_archive_file.apply_async(
kwargs={"document_id": document_id, "remote_ocr": remote_ocr}, kwargs={"document_id": document_id},
headers={"trigger_source": PaperlessTask.TriggerSource.MANUAL}, headers={"trigger_source": PaperlessTask.TriggerSource.MANUAL},
) )
-18
View File
@@ -53,7 +53,6 @@ from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats from documents.utils import copy_file_with_basic_stats
from documents.utils import run_subprocess from documents.utils import run_subprocess
from paperless.config import OcrConfig from paperless.config import OcrConfig
from paperless.config import RemoteOCRConfig
from paperless.models import ArchiveFileGenerationChoices from paperless.models import ArchiveFileGenerationChoices
from paperless.parsers import ParserContext from paperless.parsers import ParserContext
from paperless.parsers import ParserProtocol from paperless.parsers import ParserProtocol
@@ -452,19 +451,12 @@ class ConsumerPlugin(
except Exception as e: except Exception as e:
self.log.error(f"Error attempting to clean PDF: {e}") self.log.error(f"Error attempting to clean PDF: {e}")
# Workflows have already run at this point, so the metadata knows
# whether this document was singled out for remote OCR
allow_remote = (
self.metadata.remote_ocr or RemoteOCRConfig().remote_ocr_by_default
)
# Based on the mime type, get the parser for that type # Based on the mime type, get the parser for that type
parser_class: type[ParserProtocol] | None = ( parser_class: type[ParserProtocol] | None = (
get_parser_registry().get_parser_for_file( get_parser_registry().get_parser_for_file(
mime_type, mime_type,
self.filename, self.filename,
self.working_copy, self.working_copy,
allow_remote=allow_remote,
) )
) )
if not parser_class: if not parser_class:
@@ -473,16 +465,6 @@ class ConsumerPlugin(
f"Unsupported mime type {mime_type}", f"Unsupported mime type {mime_type}",
) )
if self.metadata.remote_ocr and not getattr(
parser_class,
"uses_remote_service",
False,
):
self.log.warning(
"Remote OCR was requested for this document but no remote "
"parser is available for it, processing locally instead.",
)
# Notify all listeners that we're going to do some work. # Notify all listeners that we're going to do some work.
document_consumption_started.send( document_consumption_started.send(
-3
View File
@@ -34,7 +34,6 @@ class DocumentMetadataOverrides:
skip_asn_if_exists: bool = False skip_asn_if_exists: bool = False
version_label: str | None = None version_label: str | None = None
actor_id: int | None = None actor_id: int | None = None
remote_ocr: bool = False
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides": def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
""" """
@@ -58,8 +57,6 @@ class DocumentMetadataOverrides:
self.actor_id = other.actor_id self.actor_id = other.actor_id
if other.skip_asn_if_exists: if other.skip_asn_if_exists:
self.skip_asn_if_exists = True self.skip_asn_if_exists = True
if other.remote_ocr:
self.remote_ocr = True
if other.version_label is not None: if other.version_label is not None:
self.version_label = other.version_label self.version_label = other.version_label
+106
View File
@@ -0,0 +1,106 @@
from __future__ import annotations
import importlib
import zipfile
# ZIP_ZSTANDARD exists only on Python 3.14+ (PEP 784). None elsewhere.
ZSTD: int | None = getattr(zipfile, "ZIP_ZSTANDARD", None)
# CLI choices are fixed across runtimes so argparse never hides zstd; runtime
# availability is enforced separately in compression_available().
COMPRESSION_CHOICES: tuple[str, ...] = (
"stored",
"deflated",
"bzip2",
"lzma",
"zstd",
)
# Method name -> zipfile compression constant (zstd only when supported).
COMPRESSION_METHODS: dict[str, int] = {
"stored": zipfile.ZIP_STORED,
"deflated": zipfile.ZIP_DEFLATED,
"bzip2": zipfile.ZIP_BZIP2,
"lzma": zipfile.ZIP_LZMA,
}
if ZSTD is not None:
COMPRESSION_METHODS["zstd"] = ZSTD
# Inclusive (min, max) level bounds per method; None => level not applicable.
# Verified on CPython 3.14.3.
#
# zstd's raw library bounds are (-131072, 22)
# (compression.zstd.CompressionParameter.compression_level.bounds()) — the
# minimum is an internal implementation constant (-ZSTD_TARGETLENGTH_MAX),
# not a meaningful distinct "level"; deeper negative values than -22 buy
# nothing over -22 in practice. We expose the conventional zstd CLI range
# instead of the raw library bounds.
LEVEL_BOUNDS: dict[str, tuple[int, int] | None] = {
"stored": None,
"deflated": (0, 9),
"bzip2": (1, 9),
"lzma": None,
"zstd": (-22, 22),
}
# zipfile compress_type id -> method name.
_COMPRESS_TYPE_TO_METHOD: dict[int, str] = {
zipfile.ZIP_STORED: "stored",
zipfile.ZIP_DEFLATED: "deflated",
zipfile.ZIP_BZIP2: "bzip2",
zipfile.ZIP_LZMA: "lzma",
93: "zstd",
}
def compression_available(method: str) -> bool:
"""Whether the running interpreter can actually use the given method."""
if method in ("stored", "deflated"):
# zlib is a hard CPython dependency; stored needs nothing.
return True
if method == "bzip2":
return _module_importable("bz2")
if method == "lzma":
return _module_importable("lzma")
if method == "zstd":
return ZSTD is not None and _module_importable("compression.zstd")
return False # pragma: no cover -- method is always one of COMPRESSION_CHOICES
def _module_importable(name: str) -> bool:
try:
importlib.import_module(name)
except ImportError:
return False
return True
def level_error(method: str, level: int | None) -> str | None:
"""Return a human message if (method, level) is invalid, else None."""
if level is None:
return None
bounds = LEVEL_BOUNDS[method]
if bounds is None:
return f"--zip-compression-level has no effect for '{method}'"
low, high = bounds
if not (low <= level <= high):
return (
f"--zip-compression-level for '{method}' must be between {low} and {high}"
)
return None
def compress_type_readable(compress_type: int) -> bool:
"""Whether this interpreter can decompress an entry of the given type."""
method = _COMPRESS_TYPE_TO_METHOD.get(compress_type)
if method is None:
return False
return compression_available(method)
def unreadable_method_names(compress_types: set[int]) -> set[str]:
"""Map a set of compress_type ids to human method names for error messages."""
names: set[str] = set()
for ct in compress_types:
names.add(_COMPRESS_TYPE_TO_METHOD.get(ct, f"method {ct}"))
return names
+13 -2
View File
@@ -243,11 +243,21 @@ class ZipExportSink(ExportSink):
added as an entry at finalize (a zip entry cannot be interleaved with others). added as an entry at finalize (a zip entry cannot be interleaved with others).
""" """
def __init__(self, target: Path, zip_name: str, *, delete: bool = False) -> None: def __init__(
self,
target: Path,
zip_name: str,
*,
delete: bool = False,
compression: int = zipfile.ZIP_DEFLATED,
compresslevel: int | None = None,
) -> None:
self._target = target.resolve() self._target = target.resolve()
self._zip_path = (self._target / zip_name).with_suffix(".zip") self._zip_path = (self._target / zip_name).with_suffix(".zip")
self._tmp_path = self._zip_path.with_name(self._zip_path.name + ".tmp") self._tmp_path = self._zip_path.with_name(self._zip_path.name + ".tmp")
self._delete = delete self._delete = delete
self._compression = compression
self._compresslevel = compresslevel
self._zip: zipfile.ZipFile | None = None self._zip: zipfile.ZipFile | None = None
self._dirs: set[str] = set() self._dirs: set[str] = set()
self._pending_manifest: tuple[Path, str] | None = None self._pending_manifest: tuple[Path, str] | None = None
@@ -258,7 +268,8 @@ class ZipExportSink(ExportSink):
self._zip = zipfile.ZipFile( self._zip = zipfile.ZipFile(
self._tmp_path, self._tmp_path,
"w", "w",
compression=zipfile.ZIP_DEFLATED, compression=self._compression,
compresslevel=self._compresslevel,
allowZip64=True, allowZip64=True,
) )

Some files were not shown because too many files have changed in this diff Show More