mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-12 00:19:48 +00:00
Merge branch 'dev' into feature-remote-ocr-2
This commit is contained in:
12
docs/api.md
12
docs/api.md
@@ -282,6 +282,18 @@ The following methods are supported:
|
|||||||
- `"merge": true or false` (defaults to false)
|
- `"merge": true or false` (defaults to false)
|
||||||
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||||
removing them) or be merged with existing permissions.
|
removing them) or be merged with existing permissions.
|
||||||
|
- `edit_pdf`
|
||||||
|
- Requires `parameters`:
|
||||||
|
- `"doc_ids": [DOCUMENT_ID]` A list of a single document ID to edit.
|
||||||
|
- `"operations": [OPERATION, ...]` A list of operations to perform on the documents. Each operation is a dictionary
|
||||||
|
with the following keys:
|
||||||
|
- `"page": PAGE_NUMBER` The page number to edit (1-based).
|
||||||
|
- `"rotate": DEGREES` Optional rotation in degrees (90, 180, 270).
|
||||||
|
- `"doc": OUTPUT_DOCUMENT_INDEX` Optional index of the output document for split operations.
|
||||||
|
- Optional `parameters`:
|
||||||
|
- `"delete_original": true` to delete the original documents after editing.
|
||||||
|
- `"update_document": true` to update the existing document with the edited PDF.
|
||||||
|
- `"include_metadata": true` to copy metadata from the original document to the edited document.
|
||||||
- `merge`
|
- `merge`
|
||||||
- No additional `parameters` required.
|
- No additional `parameters` required.
|
||||||
- The ordering of the merged document is determined by the list of IDs.
|
- The ordering of the merged document is determined by the list of IDs.
|
||||||
|
@@ -1282,6 +1282,30 @@ within your documents.
|
|||||||
|
|
||||||
Defaults to false.
|
Defaults to false.
|
||||||
|
|
||||||
|
## Workflow webhooks
|
||||||
|
|
||||||
|
#### [`PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES=<str>`](#PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES) {#PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES}
|
||||||
|
|
||||||
|
: A comma-separated list of allowed schemes for webhooks. This setting
|
||||||
|
controls which URL schemes are permitted for webhook URLs.
|
||||||
|
|
||||||
|
Defaults to `http,https`.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_WEBHOOKS_ALLOWED_PORTS=<str>`](#PAPERLESS_WEBHOOKS_ALLOWED_PORTS) {#PAPERLESS_WEBHOOKS_ALLOWED_PORTS}
|
||||||
|
|
||||||
|
: A comma-separated list of allowed ports for webhooks. This setting
|
||||||
|
controls which ports are permitted for webhook URLs. For example, if you
|
||||||
|
set this to `80,443`, webhooks will only be sent to URLs that use these
|
||||||
|
ports.
|
||||||
|
|
||||||
|
Defaults to empty list, which allows all ports.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS=<bool>`](#PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS) {#PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS}
|
||||||
|
|
||||||
|
: If set to false, webhooks cannot be sent to internal URLs (e.g., localhost).
|
||||||
|
|
||||||
|
Defaults to true, which allows internal requests.
|
||||||
|
|
||||||
### Polling {#polling}
|
### Polling {#polling}
|
||||||
|
|
||||||
#### [`PAPERLESS_CONSUMER_POLLING=<num>`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING}
|
#### [`PAPERLESS_CONSUMER_POLLING=<num>`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING}
|
||||||
|
@@ -499,6 +499,10 @@ The following workflow action types are available:
|
|||||||
- Encoding for the request body, either JSON or form data
|
- Encoding for the request body, either JSON or form data
|
||||||
- The request headers as key-value pairs
|
- The request headers as key-value pairs
|
||||||
|
|
||||||
|
For security reasons, webhooks can be limited to specific ports and disallowed from connecting to local URLs. See the relevant
|
||||||
|
[configuration settings](configuration.md#workflow-webhooks) to change this behavior. If you are allowing non-admins to create workflows,
|
||||||
|
you may want to adjust these settings to prevent abuse.
|
||||||
|
|
||||||
#### Workflow placeholders
|
#### Workflow placeholders
|
||||||
|
|
||||||
Some workflow text can include placeholders but the available options differ depending on the type of
|
Some workflow text can include placeholders but the available options differ depending on the type of
|
||||||
@@ -576,12 +580,14 @@ The following custom field types are supported:
|
|||||||
|
|
||||||
## PDF Actions
|
## PDF Actions
|
||||||
|
|
||||||
Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):
|
Paperless-ngx supports basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files). When viewing an individual document you can
|
||||||
|
open the 'PDF Editor' to use a simple UI for re-arranging, rotating, deleting pages and splitting documents.
|
||||||
|
|
||||||
- Merging documents: available when selecting multiple documents for 'bulk editing'.
|
- Merging documents: available when selecting multiple documents for 'bulk editing'.
|
||||||
- Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
|
- Rotating documents: available when selecting multiple documents for 'bulk editing' and via the pdf editor on an individual document's details page.
|
||||||
- Splitting documents: available from an individual document's details page.
|
- Splitting documents: via the pdf editor on an individual document's details page.
|
||||||
- Deleting pages: available from an individual document's details page.
|
- Deleting pages: via the pdf editor on an individual document's details page.
|
||||||
|
- Re-arranging pages: via the pdf editor on an individual document's details page.
|
||||||
|
|
||||||
!!! important
|
!!! important
|
||||||
|
|
||||||
|
@@ -385,7 +385,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">117</context>
|
<context context-type="linenumber">109</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1241348629231510663" datatype="html">
|
<trans-unit id="1241348629231510663" datatype="html">
|
||||||
@@ -534,7 +534,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">370</context>
|
<context context-type="linenumber">362</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3768927257183755959" datatype="html">
|
<trans-unit id="3768927257183755959" datatype="html">
|
||||||
@@ -593,7 +593,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">363</context>
|
<context context-type="linenumber">355</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context>
|
||||||
@@ -739,7 +739,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">383</context>
|
<context context-type="linenumber">375</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
@@ -1197,7 +1197,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">339</context>
|
<context context-type="linenumber">331</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.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -2544,19 +2544,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">997</context>
|
<context context-type="linenumber">998</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1359</context>
|
<context context-type="linenumber">1360</context>
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1398</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1439</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>
|
||||||
@@ -3164,7 +3156,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">950</context>
|
<context context-type="linenumber">951</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>
|
||||||
@@ -3191,47 +3183,6 @@
|
|||||||
<context context-type="linenumber">747</context>
|
<context context-type="linenumber">747</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1407560924967345762" datatype="html">
|
|
||||||
<source>Page</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">11</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">11</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
|
||||||
<context context-type="linenumber">5</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
|
||||||
<context context-type="linenumber">11</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2266163016683537825" datatype="html">
|
|
||||||
<source>of <x id="INTERPOLATION" equiv-text="{{totalPages}}"/></source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">13</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">13</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
|
||||||
<context context-type="linenumber">7,8</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6903610408081711391" datatype="html">
|
|
||||||
<source>Pages to remove</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">16</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="994016933065248559" datatype="html">
|
<trans-unit id="994016933065248559" datatype="html">
|
||||||
<source>Documents:</source>
|
<source>Documents:</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -3281,20 +3232,6 @@
|
|||||||
<context context-type="linenumber">25</context>
|
<context context-type="linenumber">25</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6567555383934959967" datatype="html">
|
|
||||||
<source>Add Split</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">28</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="492847770415850840" datatype="html">
|
|
||||||
<source>Delete original document after successful split</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">51</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2509141182388535183" datatype="html">
|
<trans-unit id="2509141182388535183" datatype="html">
|
||||||
<source>View</source>
|
<source>View</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -3409,11 +3346,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">111</context>
|
<context context-type="linenumber">103</context>
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1416</context>
|
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context>
|
<context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context>
|
||||||
@@ -4354,7 +4287,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">305</context>
|
<context context-type="linenumber">297</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8057014866157903311" datatype="html">
|
<trans-unit id="8057014866157903311" datatype="html">
|
||||||
@@ -4458,7 +4391,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">96</context>
|
<context context-type="linenumber">88</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5342432350421167093" datatype="html">
|
<trans-unit id="5342432350421167093" datatype="html">
|
||||||
@@ -5528,6 +5461,104 @@
|
|||||||
<context context-type="linenumber">9</context>
|
<context context-type="linenumber">9</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="5034217198277582100" datatype="html">
|
||||||
|
<source>Select all pages</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">9</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="234610397929376642" datatype="html">
|
||||||
|
<source>Deselect all pages</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">12</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2530246103796817298" datatype="html">
|
||||||
|
<source>Rotate selected pages counter-clockwise</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">17</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="4787219034890830544" datatype="html">
|
||||||
|
<source>Rotate selected pages clockwise</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">20</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3441043765105475130" datatype="html">
|
||||||
|
<source>Delete selected pages</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">23</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3873740163706409154" datatype="html">
|
||||||
|
<source>Rotate page counter-clockwise</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">33</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3450236521040548507" datatype="html">
|
||||||
|
<source>Rotate page clockwise</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">36</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7647925464077975347" datatype="html">
|
||||||
|
<source>Delete page</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">41</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2480952115552020422" datatype="html">
|
||||||
|
<source>Add / remove document split here</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">44</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="35277754987868961" datatype="html">
|
||||||
|
<source>Split here</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">70</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7273640930165035289" datatype="html">
|
||||||
|
<source>Create new document(s)</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">82</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8035757452478567832" datatype="html">
|
||||||
|
<source>Update existing document</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">87</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7248454234750442816" datatype="html">
|
||||||
|
<source>Copy metadata</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">93</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6684403463658676119" datatype="html">
|
||||||
|
<source>Delete original</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/pdf-editor/pdf-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">97</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="7940755769131903278" datatype="html">
|
<trans-unit id="7940755769131903278" datatype="html">
|
||||||
<source>Merge with existing permissions</source>
|
<source>Merge with existing permissions</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -5977,7 +6008,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">92</context>
|
<context context-type="linenumber">84</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3429210839568770054" datatype="html">
|
<trans-unit id="3429210839568770054" datatype="html">
|
||||||
@@ -6479,6 +6510,24 @@
|
|||||||
<context context-type="linenumber">1</context>
|
<context context-type="linenumber">1</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="1407560924967345762" datatype="html">
|
||||||
|
<source>Page</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
|
<context context-type="linenumber">5</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">11</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2266163016683537825" datatype="html">
|
||||||
|
<source>of <x id="INTERPOLATION" equiv-text="{{previewNumPages}}"/></source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
|
<context context-type="linenumber">7,8</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="8590109102084543521" datatype="html">
|
<trans-unit id="8590109102084543521" datatype="html">
|
||||||
<source>-</source>
|
<source>-</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -6522,57 +6571,43 @@
|
|||||||
<context context-type="linenumber">69</context>
|
<context context-type="linenumber">69</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2434944824726929798" datatype="html">
|
<trans-unit id="5084275925647254161" datatype="html">
|
||||||
<source>Split</source>
|
<source>PDF Editor</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">62</context>
|
<context context-type="linenumber">62</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1050269006235116171" datatype="html">
|
|
||||||
<source>Rotate</source>
|
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">66</context>
|
<context context-type="linenumber">1359</context>
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
|
||||||
<context context-type="linenumber">110</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="4399672576012609374" datatype="html">
|
|
||||||
<source>Delete page(s)</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
|
||||||
<context context-type="linenumber">70</context>
|
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6490688569532630280" datatype="html">
|
<trans-unit id="6490688569532630280" datatype="html">
|
||||||
<source>Send</source>
|
<source>Send</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">88</context>
|
<context context-type="linenumber">80</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4452427314943113135" datatype="html">
|
<trans-unit id="4452427314943113135" datatype="html">
|
||||||
<source>Previous</source>
|
<source>Previous</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">114</context>
|
<context context-type="linenumber">106</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5028777105388019087" datatype="html">
|
<trans-unit id="5028777105388019087" datatype="html">
|
||||||
<source>Details</source>
|
<source>Details</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">127</context>
|
<context context-type="linenumber">119</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5701618810648052610" datatype="html">
|
<trans-unit id="5701618810648052610" datatype="html">
|
||||||
<source>Title</source>
|
<source>Title</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">130</context>
|
<context context-type="linenumber">122</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
@@ -6595,21 +6630,21 @@
|
|||||||
<source>Archive serial number</source>
|
<source>Archive serial number</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">131</context>
|
<context context-type="linenumber">123</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5114742157723900905" datatype="html">
|
<trans-unit id="5114742157723900905" datatype="html">
|
||||||
<source>Date created</source>
|
<source>Date created</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">132</context>
|
<context context-type="linenumber">124</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2691296884221415710" datatype="html">
|
<trans-unit id="2691296884221415710" datatype="html">
|
||||||
<source>Correspondent</source>
|
<source>Correspondent</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">134</context>
|
<context context-type="linenumber">126</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.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -6636,7 +6671,7 @@
|
|||||||
<source>Document type</source>
|
<source>Document type</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">136</context>
|
<context context-type="linenumber">128</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.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -6663,7 +6698,7 @@
|
|||||||
<source>Storage path</source>
|
<source>Storage path</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">138</context>
|
<context context-type="linenumber">130</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.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -6686,7 +6721,7 @@
|
|||||||
<source>Default</source>
|
<source>Default</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">139</context>
|
<context context-type="linenumber">131</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
@@ -6697,14 +6732,14 @@
|
|||||||
<source>Content</source>
|
<source>Content</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">235</context>
|
<context context-type="linenumber">227</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="218403386307979629" datatype="html">
|
<trans-unit id="218403386307979629" datatype="html">
|
||||||
<source>Metadata</source>
|
<source>Metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">244</context>
|
<context context-type="linenumber">236</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
|
||||||
@@ -6715,175 +6750,175 @@
|
|||||||
<source>Date modified</source>
|
<source>Date modified</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">251</context>
|
<context context-type="linenumber">243</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6392918669949841614" datatype="html">
|
<trans-unit id="6392918669949841614" datatype="html">
|
||||||
<source>Date added</source>
|
<source>Date added</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">255</context>
|
<context context-type="linenumber">247</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="146828917013192897" datatype="html">
|
<trans-unit id="146828917013192897" datatype="html">
|
||||||
<source>Media filename</source>
|
<source>Media filename</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">259</context>
|
<context context-type="linenumber">251</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4500855521601039868" datatype="html">
|
<trans-unit id="4500855521601039868" datatype="html">
|
||||||
<source>Original filename</source>
|
<source>Original filename</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">263</context>
|
<context context-type="linenumber">255</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7985558498848210210" datatype="html">
|
<trans-unit id="7985558498848210210" datatype="html">
|
||||||
<source>Original MD5 checksum</source>
|
<source>Original MD5 checksum</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">267</context>
|
<context context-type="linenumber">259</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5888243105821763422" datatype="html">
|
<trans-unit id="5888243105821763422" datatype="html">
|
||||||
<source>Original file size</source>
|
<source>Original file size</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">271</context>
|
<context context-type="linenumber">263</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2696647325713149563" datatype="html">
|
<trans-unit id="2696647325713149563" datatype="html">
|
||||||
<source>Original mime type</source>
|
<source>Original mime type</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">275</context>
|
<context context-type="linenumber">267</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="342875990758166588" datatype="html">
|
<trans-unit id="342875990758166588" datatype="html">
|
||||||
<source>Archive MD5 checksum</source>
|
<source>Archive MD5 checksum</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">280</context>
|
<context context-type="linenumber">272</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6033581412811562084" datatype="html">
|
<trans-unit id="6033581412811562084" datatype="html">
|
||||||
<source>Archive file size</source>
|
<source>Archive file size</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">286</context>
|
<context context-type="linenumber">278</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6992781481378431874" datatype="html">
|
<trans-unit id="6992781481378431874" datatype="html">
|
||||||
<source>Original document metadata</source>
|
<source>Original document metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">295</context>
|
<context context-type="linenumber">287</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2846565152091361585" datatype="html">
|
<trans-unit id="2846565152091361585" datatype="html">
|
||||||
<source>Archived document metadata</source>
|
<source>Archived document metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">298</context>
|
<context context-type="linenumber">290</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7206723502037428235" datatype="html">
|
<trans-unit id="7206723502037428235" datatype="html">
|
||||||
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
|
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">317,320</context>
|
<context context-type="linenumber">309,312</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="186236568870281953" datatype="html">
|
<trans-unit id="186236568870281953" datatype="html">
|
||||||
<source>History</source>
|
<source>History</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">328</context>
|
<context context-type="linenumber">320</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5129524307369213584" datatype="html">
|
<trans-unit id="5129524307369213584" datatype="html">
|
||||||
<source>Save & next</source>
|
<source>Save & next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">365</context>
|
<context context-type="linenumber">357</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4910102545766233758" datatype="html">
|
<trans-unit id="4910102545766233758" datatype="html">
|
||||||
<source>Save & close</source>
|
<source>Save & close</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">368</context>
|
<context context-type="linenumber">360</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1309556917227148591" datatype="html">
|
<trans-unit id="1309556917227148591" datatype="html">
|
||||||
<source>Document loading...</source>
|
<source>Document loading...</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">378</context>
|
<context context-type="linenumber">370</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8191371354890763172" datatype="html">
|
<trans-unit id="8191371354890763172" datatype="html">
|
||||||
<source>Enter Password</source>
|
<source>Enter Password</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">432</context>
|
<context context-type="linenumber">424</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2218903673684131427" datatype="html">
|
<trans-unit id="2218903673684131427" datatype="html">
|
||||||
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">411,413</context>
|
<context context-type="linenumber">412,414</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3200733026060976258" datatype="html">
|
<trans-unit id="3200733026060976258" datatype="html">
|
||||||
<source>Document changes detected</source>
|
<source>Document changes detected</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">434</context>
|
<context context-type="linenumber">435</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2887155916749964" datatype="html">
|
<trans-unit id="2887155916749964" datatype="html">
|
||||||
<source>The version of this document in your browser session appears older than the existing version.</source>
|
<source>The version of this document in your browser session appears older than the existing version.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">435</context>
|
<context context-type="linenumber">436</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="237142428785956348" datatype="html">
|
<trans-unit id="237142428785956348" datatype="html">
|
||||||
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
|
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">436</context>
|
<context context-type="linenumber">437</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8720977247725652816" datatype="html">
|
<trans-unit id="8720977247725652816" datatype="html">
|
||||||
<source>Ok</source>
|
<source>Ok</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">438</context>
|
<context context-type="linenumber">439</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6142395741265832184" datatype="html">
|
<trans-unit id="6142395741265832184" datatype="html">
|
||||||
<source>Next document</source>
|
<source>Next document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">554</context>
|
<context context-type="linenumber">555</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="651985345816518480" datatype="html">
|
<trans-unit id="651985345816518480" datatype="html">
|
||||||
<source>Previous document</source>
|
<source>Previous document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">564</context>
|
<context context-type="linenumber">565</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2885986061416655600" datatype="html">
|
<trans-unit id="2885986061416655600" datatype="html">
|
||||||
<source>Close document</source>
|
<source>Close document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">572</context>
|
<context context-type="linenumber">573</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
|
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
|
||||||
@@ -6894,67 +6929,67 @@
|
|||||||
<source>Save document</source>
|
<source>Save document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">579</context>
|
<context context-type="linenumber">580</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1784543155727940353" datatype="html">
|
<trans-unit id="1784543155727940353" datatype="html">
|
||||||
<source>Save and close / next</source>
|
<source>Save and close / next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">588</context>
|
<context context-type="linenumber">589</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5758784066858623886" datatype="html">
|
<trans-unit id="5758784066858623886" datatype="html">
|
||||||
<source>Error retrieving metadata</source>
|
<source>Error retrieving metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">640</context>
|
<context context-type="linenumber">641</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3456881259945295697" datatype="html">
|
<trans-unit id="3456881259945295697" datatype="html">
|
||||||
<source>Error retrieving suggestions.</source>
|
<source>Error retrieving suggestions.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">669</context>
|
<context context-type="linenumber">670</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2194092841814123758" datatype="html">
|
<trans-unit id="2194092841814123758" datatype="html">
|
||||||
<source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source>
|
<source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">844</context>
|
<context context-type="linenumber">845</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">868</context>
|
<context context-type="linenumber">869</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6626387786259219838" datatype="html">
|
<trans-unit id="6626387786259219838" datatype="html">
|
||||||
<source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source>
|
<source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">874</context>
|
<context context-type="linenumber">875</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="448882439049417053" datatype="html">
|
<trans-unit id="448882439049417053" datatype="html">
|
||||||
<source>Error saving document</source>
|
<source>Error saving document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">919</context>
|
<context context-type="linenumber">920</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8410796510716511826" datatype="html">
|
<trans-unit id="8410796510716511826" datatype="html">
|
||||||
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source>
|
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">951</context>
|
<context context-type="linenumber">952</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="282586936710748252" datatype="html">
|
<trans-unit id="282586936710748252" datatype="html">
|
||||||
<source>Documents can be restored prior to permanent deletion.</source>
|
<source>Documents can be restored prior to permanent deletion.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">952</context>
|
<context context-type="linenumber">953</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>
|
||||||
@@ -6965,7 +7000,7 @@
|
|||||||
<source>Move to trash</source>
|
<source>Move to trash</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">954</context>
|
<context context-type="linenumber">955</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>
|
||||||
@@ -6976,14 +7011,14 @@
|
|||||||
<source>Error deleting document</source>
|
<source>Error deleting document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">973</context>
|
<context context-type="linenumber">974</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="619486176823357521" datatype="html">
|
<trans-unit id="619486176823357521" datatype="html">
|
||||||
<source>Reprocess confirm</source>
|
<source>Reprocess confirm</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">993</context>
|
<context context-type="linenumber">994</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>
|
||||||
@@ -6994,141 +7029,67 @@
|
|||||||
<source>This operation will permanently recreate the archive file for this document.</source>
|
<source>This operation will permanently recreate the archive file for this document.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">994</context>
|
<context context-type="linenumber">995</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="302054111564709516" datatype="html">
|
<trans-unit id="302054111564709516" datatype="html">
|
||||||
<source>The archive file will be re-generated with the current settings.</source>
|
<source>The archive file 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-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">995</context>
|
<context context-type="linenumber">996</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8251197608401006898" datatype="html">
|
<trans-unit id="8251197608401006898" datatype="html">
|
||||||
<source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
|
<source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1005</context>
|
<context context-type="linenumber">1006</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4409560272830824468" datatype="html">
|
<trans-unit id="4409560272830824468" datatype="html">
|
||||||
<source>Error executing operation</source>
|
<source>Error executing operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1016</context>
|
<context context-type="linenumber">1017</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6030453331794586802" datatype="html">
|
<trans-unit id="6030453331794586802" datatype="html">
|
||||||
<source>Error downloading document</source>
|
<source>Error downloading document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1065</context>
|
<context context-type="linenumber">1066</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4458954481601077369" datatype="html">
|
<trans-unit id="4458954481601077369" datatype="html">
|
||||||
<source>Page Fit</source>
|
<source>Page Fit</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1142</context>
|
<context context-type="linenumber">1143</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1217563727923422413" datatype="html">
|
<trans-unit id="4663705961777238777" datatype="html">
|
||||||
<source>Split confirm</source>
|
<source>PDF edit operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1357</context>
|
<context context-type="linenumber">1378</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2805304563009985503" datatype="html">
|
<trans-unit id="9043972994040261999" datatype="html">
|
||||||
<source>This operation will split the selected document(s) into new documents.</source>
|
<source>Error executing PDF edit operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1358</context>
|
<context context-type="linenumber">1390</context>
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7638681545012641321" datatype="html">
|
|
||||||
<source>Split operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1374</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="3235014591864339926" datatype="html">
|
|
||||||
<source>Error executing split operation</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1383</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6555329262222566158" datatype="html">
|
|
||||||
<source>Rotate confirm</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1396</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
|
||||||
<context context-type="linenumber">823</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="857641176955257111" datatype="html">
|
|
||||||
<source>This operation will permanently rotate the original version of the current document.</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1397</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="3802852336439815451" datatype="html">
|
|
||||||
<source>Rotation of "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open the document after the operation has completed to see the changes.</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1413</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2962674215361798818" datatype="html">
|
|
||||||
<source>Error executing rotate operation</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1425</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="3539261415918606512" datatype="html">
|
|
||||||
<source>Delete pages confirm</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1437</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5854352498125813866" datatype="html">
|
|
||||||
<source>This operation will permanently delete the selected pages from the original document.</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1438</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1138505464360427037" datatype="html">
|
|
||||||
<source>Delete pages operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1453</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1249139200486584973" datatype="html">
|
|
||||||
<source>Error executing delete pages operation</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
|
||||||
<context context-type="linenumber">1462</context>
|
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6085793215710522488" datatype="html">
|
<trans-unit id="6085793215710522488" datatype="html">
|
||||||
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1522</context>
|
<context context-type="linenumber">1450</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1526</context>
|
<context context-type="linenumber">1454</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4958946940233632319" datatype="html">
|
<trans-unit id="4958946940233632319" datatype="html">
|
||||||
@@ -7225,6 +7186,13 @@
|
|||||||
<context context-type="linenumber">86</context>
|
<context context-type="linenumber">86</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="1050269006235116171" datatype="html">
|
||||||
|
<source>Rotate</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">110</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="3206542606001340679" datatype="html">
|
<trans-unit id="3206542606001340679" datatype="html">
|
||||||
<source>Merge</source>
|
<source>Merge</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -7478,6 +7446,13 @@
|
|||||||
<context context-type="linenumber">791</context>
|
<context context-type="linenumber">791</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="6555329262222566158" datatype="html">
|
||||||
|
<source>Rotate confirm</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
|
<context context-type="linenumber">823</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="6390006284731990222" datatype="html">
|
<trans-unit id="6390006284731990222" datatype="html">
|
||||||
<source>This operation will permanently rotate the original version of <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source>
|
<source>This operation will permanently rotate the original version of <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
|
@@ -121,6 +121,26 @@ if (!URL.revokeObjectURL) {
|
|||||||
}
|
}
|
||||||
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
||||||
|
|
||||||
|
if (typeof IntersectionObserver === 'undefined') {
|
||||||
|
class MockIntersectionObserver {
|
||||||
|
constructor(
|
||||||
|
public callback: IntersectionObserverCallback,
|
||||||
|
public options?: IntersectionObserverInit
|
||||||
|
) {}
|
||||||
|
|
||||||
|
observe = jest.fn()
|
||||||
|
unobserve = jest.fn()
|
||||||
|
disconnect = jest.fn()
|
||||||
|
takeRecords = jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'IntersectionObserver', {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: MockIntersectionObserver,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
HTMLCanvasElement.prototype.getContext = <
|
HTMLCanvasElement.prototype.getContext = <
|
||||||
typeof HTMLCanvasElement.prototype.getContext
|
typeof HTMLCanvasElement.prototype.getContext
|
||||||
>jest.fn()
|
>jest.fn()
|
||||||
|
@@ -1,54 +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">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="btn-toolbar flex-nowrap">
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<div class="input-group-text" i18n>Page</div>
|
|
||||||
<input class="form-control mw-60" type="number" min="1" [(ngModel)]="currentPage" />
|
|
||||||
<div class="input-group-text" i18n>of {{totalPages}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="input-group input-group-sm ms-auto">
|
|
||||||
<span class="input-group-text" i18n>Pages to remove</span>
|
|
||||||
<input [ngModel]="pagesString" class="form-control" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pdf-viewer-container w-100 mt-3">
|
|
||||||
<pdf-viewer #pdfViewer [src]="pdfSrc" [(page)]="currentPage"
|
|
||||||
[original-size]="false"
|
|
||||||
[zoom]="1"
|
|
||||||
zoom-scale="page-fit"
|
|
||||||
[render-text]="false"
|
|
||||||
(pagerendered)="pageRendered($event)"
|
|
||||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
|
||||||
</pdf-viewer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer flex-nowrap">
|
|
||||||
<div>
|
|
||||||
@if (message) {
|
|
||||||
<p [innerHTML]="message | safeHtml"></p>
|
|
||||||
}
|
|
||||||
@if (messageBold) {
|
|
||||||
<p class="mb-0 small"><b [innerHTML]="messageBold | safeHtml"></b></p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<ng-template #pageCheckOverlay let-page="page" let-pages="pages">
|
|
||||||
<div class="position-absolute top-0 start-0 w-100 h-100 p-2" (click)="pageCheckChanged(page)">
|
|
||||||
<input type="checkbox" class="form-check-input" />
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
@@ -1,28 +0,0 @@
|
|||||||
.pdf-viewer-container {
|
|
||||||
background-color: gray;
|
|
||||||
height: 550px;
|
|
||||||
|
|
||||||
pdf-viewer {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mw-60 {
|
|
||||||
max-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.position-absolute:has(.form-check-input:checked) {
|
|
||||||
background-color: rgba(var(--bs-dark-rgb), 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-input {
|
|
||||||
&:checked {
|
|
||||||
background-color: var(--bs-danger);
|
|
||||||
border-color: var(--bs-danger);
|
|
||||||
}
|
|
||||||
&:focus {
|
|
||||||
box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), var(--pngx-focus-alpha));
|
|
||||||
border-color: var(--bs-danger);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,60 +0,0 @@
|
|||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
|
|
||||||
|
|
||||||
describe('DeletePagesConfirmDialogComponent', () => {
|
|
||||||
let component: DeletePagesConfirmDialogComponent
|
|
||||||
let fixture: ComponentFixture<DeletePagesConfirmDialogComponent>
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [],
|
|
||||||
imports: [
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
DeletePagesConfirmDialogComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
NgbActiveModal,
|
|
||||||
SafeHtmlPipe,
|
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
|
||||||
provideHttpClientTesting(),
|
|
||||||
],
|
|
||||||
}).compileComponents()
|
|
||||||
fixture = TestBed.createComponent(DeletePagesConfirmDialogComponent)
|
|
||||||
component = fixture.componentInstance
|
|
||||||
fixture.detectChanges()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return a string with comma-separated pages', () => {
|
|
||||||
component.pages = [1, 2, 3, 4]
|
|
||||||
expect(component.pagesString).toEqual('1, 2, 3, 4')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update totalPages when pdf is loaded', () => {
|
|
||||||
component.pdfPreviewLoaded({ numPages: 5 } as any)
|
|
||||||
expect(component.totalPages).toEqual(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update checks when page is rendered', () => {
|
|
||||||
const event = {
|
|
||||||
target: document.createElement('div'),
|
|
||||||
detail: { pageNumber: 1 },
|
|
||||||
} as any
|
|
||||||
component.pageRendered(event)
|
|
||||||
expect(component['checks'].length).toEqual(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update pages when page check is changed', () => {
|
|
||||||
component.pageCheckChanged(1)
|
|
||||||
expect(component.pages).toEqual([1])
|
|
||||||
component.pageCheckChanged(1)
|
|
||||||
expect(component.pages).toEqual([])
|
|
||||||
})
|
|
||||||
})
|
|
@@ -1,69 +0,0 @@
|
|||||||
import { Component, TemplateRef, ViewChild, inject } from '@angular/core'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import {
|
|
||||||
PDFDocumentProxy,
|
|
||||||
PdfViewerComponent,
|
|
||||||
PdfViewerModule,
|
|
||||||
} from 'ng2-pdf-viewer'
|
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'pngx-delete-pages-confirm-dialog',
|
|
||||||
templateUrl: './delete-pages-confirm-dialog.component.html',
|
|
||||||
styleUrl: './delete-pages-confirm-dialog.component.scss',
|
|
||||||
imports: [PdfViewerModule, FormsModule, ReactiveFormsModule, SafeHtmlPipe],
|
|
||||||
})
|
|
||||||
export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent {
|
|
||||||
private documentService = inject(DocumentService)
|
|
||||||
|
|
||||||
public documentID: number
|
|
||||||
public pages: number[] = []
|
|
||||||
public currentPage: number = 1
|
|
||||||
public totalPages: number
|
|
||||||
|
|
||||||
@ViewChild('pdfViewer') pdfViewer: PdfViewerComponent
|
|
||||||
@ViewChild('pageCheckOverlay') pageCheckOverlay!: TemplateRef<any>
|
|
||||||
private checks: HTMLElement[] = []
|
|
||||||
|
|
||||||
public get pagesString(): string {
|
|
||||||
return this.pages.join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
public get pdfSrc(): string {
|
|
||||||
return this.documentService.getPreviewUrl(this.documentID)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
public pdfPreviewLoaded(pdf: PDFDocumentProxy) {
|
|
||||||
this.totalPages = pdf.numPages
|
|
||||||
}
|
|
||||||
|
|
||||||
pageRendered(event: CustomEvent) {
|
|
||||||
const pageDiv = event.target as HTMLDivElement
|
|
||||||
const check = this.pageCheckOverlay.createEmbeddedView({
|
|
||||||
page: event.detail.pageNumber,
|
|
||||||
})
|
|
||||||
this.checks[event.detail.pageNumber - 1] = check.rootNodes[0]
|
|
||||||
pageDiv?.insertBefore(check.rootNodes[0], pageDiv.firstChild)
|
|
||||||
this.updateChecks()
|
|
||||||
}
|
|
||||||
|
|
||||||
pageCheckChanged(pageNumber: number) {
|
|
||||||
if (!this.pages.includes(pageNumber)) this.pages.push(pageNumber)
|
|
||||||
else if (this.pages.includes(pageNumber))
|
|
||||||
this.pages.splice(this.pages.indexOf(pageNumber), 1)
|
|
||||||
this.updateChecks()
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateChecks() {
|
|
||||||
this.checks.forEach((check, i) => {
|
|
||||||
const input = check.getElementsByTagName('input')[0]
|
|
||||||
input.checked = this.pages.includes(i + 1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,59 +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">
|
|
||||||
<p>{{message}}</p>
|
|
||||||
<div class="row mb-2">
|
|
||||||
<div class="col-7">
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<div class="input-group-text" i18n>Page</div>
|
|
||||||
<input class="form-control" type="number" min="1" [(ngModel)]="page" />
|
|
||||||
<div class="input-group-text" i18n>of {{totalPages}}</div>
|
|
||||||
</div>
|
|
||||||
<div class="pdf-viewer-container w-100 mt-3">
|
|
||||||
<pdf-viewer [src]="pdfSrc" [(page)]="page"
|
|
||||||
[original-size]="false"
|
|
||||||
[zoom]="1"
|
|
||||||
zoom-scale="page-fit"
|
|
||||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
|
||||||
</pdf-viewer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-5">
|
|
||||||
<div class="d-grid">
|
|
||||||
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit">
|
|
||||||
<i-bs name="plus-circle"></i-bs>
|
|
||||||
<span i18n>Add Split</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="list-group mt-3">
|
|
||||||
@for (pageStr of pagesString.split(','); track pageStr; let i = $index) {
|
|
||||||
<li class="list-group-item d-flex align-items-center">
|
|
||||||
{{pageStr}}
|
|
||||||
@if (pagesString.split(',').length > 1) {
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-danger ms-auto" (click)="removeSplit(i)">
|
|
||||||
<i-bs name="trash"></i-bs>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="form-check form-switch me-auto">
|
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalSwitch" [(ngModel)]="deleteOriginal" [disabled]="!userOwnsDocument">
|
|
||||||
<label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label>
|
|
||||||
</div>
|
|
||||||
<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,9 +0,0 @@
|
|||||||
.pdf-viewer-container {
|
|
||||||
background-color: gray;
|
|
||||||
height: 500px;
|
|
||||||
|
|
||||||
pdf-viewer {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,107 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
|
||||||
|
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|
||||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|
||||||
import { of } from 'rxjs'
|
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
|
||||||
import { SplitConfirmDialogComponent } from './split-confirm-dialog.component'
|
|
||||||
|
|
||||||
describe('SplitConfirmDialogComponent', () => {
|
|
||||||
let component: SplitConfirmDialogComponent
|
|
||||||
let fixture: ComponentFixture<SplitConfirmDialogComponent>
|
|
||||||
let documentService: DocumentService
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
|
||||||
ReactiveFormsModule,
|
|
||||||
FormsModule,
|
|
||||||
PdfViewerModule,
|
|
||||||
SplitConfirmDialogComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
NgbActiveModal,
|
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
|
||||||
provideHttpClientTesting(),
|
|
||||||
],
|
|
||||||
}).compileComponents()
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(SplitConfirmDialogComponent)
|
|
||||||
documentService = TestBed.inject(DocumentService)
|
|
||||||
component = fixture.componentInstance
|
|
||||||
fixture.detectChanges()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should load document on init', () => {
|
|
||||||
const getSpy = jest.spyOn(documentService, 'get')
|
|
||||||
component.documentID = 1
|
|
||||||
getSpy.mockReturnValue(of({ id: 1 } as any))
|
|
||||||
component.ngOnInit()
|
|
||||||
expect(documentService.get).toHaveBeenCalledWith(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update pagesString when pages are added', () => {
|
|
||||||
component.totalPages = 5
|
|
||||||
component.page = 2
|
|
||||||
component.addSplit()
|
|
||||||
expect(component.pagesString).toEqual('1-2,3-5')
|
|
||||||
component.page = 4
|
|
||||||
component.addSplit()
|
|
||||||
expect(component.pagesString).toEqual('1-2,3-4,5')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update pagesString when pages are removed', () => {
|
|
||||||
component.totalPages = 5
|
|
||||||
component.page = 2
|
|
||||||
component.addSplit()
|
|
||||||
component.page = 4
|
|
||||||
component.addSplit()
|
|
||||||
expect(component.pagesString).toEqual('1-2,3-4,5')
|
|
||||||
component.removeSplit(0)
|
|
||||||
expect(component.pagesString).toEqual('1-4,5')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should enable confirm button when pages are added', () => {
|
|
||||||
component.totalPages = 5
|
|
||||||
component.page = 2
|
|
||||||
component.addSplit()
|
|
||||||
expect(component.confirmButtonEnabled).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should disable confirm button when all pages are removed', () => {
|
|
||||||
component.totalPages = 5
|
|
||||||
component.page = 2
|
|
||||||
component.addSplit()
|
|
||||||
component.removeSplit(0)
|
|
||||||
expect(component.confirmButtonEnabled).toBeFalsy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not add split if page is the last page', () => {
|
|
||||||
component.totalPages = 5
|
|
||||||
component.page = 5
|
|
||||||
component.addSplit()
|
|
||||||
expect(component.pagesString).toEqual('1-5')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update totalPages when pdf is loaded', () => {
|
|
||||||
component.pdfPreviewLoaded({ numPages: 5 } as any)
|
|
||||||
expect(component.totalPages).toEqual(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should correctly disable split button', () => {
|
|
||||||
component.totalPages = 5
|
|
||||||
component.page = 1
|
|
||||||
expect(component.canSplit).toBeTruthy()
|
|
||||||
component.page = 5
|
|
||||||
expect(component.canSplit).toBeFalsy()
|
|
||||||
component.page = 4
|
|
||||||
expect(component.canSplit).toBeTruthy()
|
|
||||||
component['pages'] = new Set([1, 2, 3, 4])
|
|
||||||
expect(component.canSplit).toBeFalsy()
|
|
||||||
})
|
|
||||||
})
|
|
@@ -1,98 +0,0 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core'
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
|
||||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
|
||||||
import { Document } from 'src/app/data/document'
|
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'pngx-split-confirm-dialog',
|
|
||||||
templateUrl: './split-confirm-dialog.component.html',
|
|
||||||
styleUrl: './split-confirm-dialog.component.scss',
|
|
||||||
imports: [
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
NgxBootstrapIconsModule,
|
|
||||||
PdfViewerModule,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class SplitConfirmDialogComponent
|
|
||||||
extends ConfirmDialogComponent
|
|
||||||
implements OnInit
|
|
||||||
{
|
|
||||||
private documentService = inject(DocumentService)
|
|
||||||
private permissionService = inject(PermissionsService)
|
|
||||||
|
|
||||||
public get pagesString(): string {
|
|
||||||
let pagesStr = ''
|
|
||||||
|
|
||||||
let lastPage = 1
|
|
||||||
for (let i = 1; i <= this.totalPages; i++) {
|
|
||||||
if (this.pages.has(i) || i === this.totalPages) {
|
|
||||||
if (lastPage === i) {
|
|
||||||
pagesStr += `${i},`
|
|
||||||
lastPage = Math.min(i + 1, this.totalPages)
|
|
||||||
} else {
|
|
||||||
pagesStr += `${lastPage}-${i},`
|
|
||||||
lastPage = Math.min(i + 1, this.totalPages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pagesStr.replace(/,$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
private pages: Set<number> = new Set()
|
|
||||||
|
|
||||||
public documentID: number
|
|
||||||
private document: Document
|
|
||||||
public page: number = 1
|
|
||||||
public totalPages: number
|
|
||||||
public deleteOriginal: boolean = false
|
|
||||||
|
|
||||||
public get canSplit(): boolean {
|
|
||||||
return (
|
|
||||||
this.page < this.totalPages &&
|
|
||||||
this.pages.size < this.totalPages - 1 &&
|
|
||||||
!this.pages.has(this.page)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public get pdfSrc(): string {
|
|
||||||
return this.documentService.getPreviewUrl(this.documentID)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.confirmButtonEnabled = this.pages.size > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.documentService.get(this.documentID).subscribe((r) => {
|
|
||||||
this.document = r
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
|
|
||||||
this.totalPages = pdf.numPages
|
|
||||||
}
|
|
||||||
|
|
||||||
addSplit() {
|
|
||||||
if (this.page === this.totalPages) return
|
|
||||||
this.pages.add(this.page)
|
|
||||||
this.pages = new Set(Array.from(this.pages).sort((a, b) => a - b))
|
|
||||||
this.confirmButtonEnabled = this.pages.size > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSplit(i: number) {
|
|
||||||
let page = Array.from(this.pages)[Math.min(i, this.pages.size - 1)]
|
|
||||||
this.pages.delete(page)
|
|
||||||
this.confirmButtonEnabled = this.pages.size > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
get userOwnsDocument(): boolean {
|
|
||||||
return this.permissionService.currentUserOwnsObject(this.document)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,103 @@
|
|||||||
|
<pdf-viewer [src]="pdfSrc" [render-text]="false" zoom="0.4" (after-load-complete)="pdfLoaded($event)"></pdf-viewer>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">{{ title }}</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="btn-toolbar mb-2">
|
||||||
|
<div class="btn-group me-3">
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="selectAll()" title="Select all pages" i18n-title>
|
||||||
|
<i-bs name="check-all"></i-bs>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="deselectAll()" [disabled]="!hasSelection()" title="Deselect all pages" i18n-title>
|
||||||
|
<i-bs name="x"></i-bs>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="rotateSelected(-90)" [disabled]="!hasSelection()" title="Rotate selected pages counter-clockwise" i18n-title>
|
||||||
|
<i-bs name="arrow-counterclockwise"></i-bs>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="rotateSelected(90)" [disabled]="!hasSelection()" title="Rotate selected pages clockwise" i18n-title>
|
||||||
|
<i-bs name="arrow-clockwise"></i-bs>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger" (click)="deleteSelected()" [disabled]="!hasSelection()" title="Delete selected pages" i18n-title>
|
||||||
|
<i-bs name="trash"></i-bs>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div cdkDropList (cdkDropListDropped)="drop($event)" cdkDropListOrientation="mixed" class="d-flex flex-wrap row-cols-5">
|
||||||
|
@for (p of pages; track p.page; let i = $index) {
|
||||||
|
<div class="page-item rounded p-2" cdkDrag (click)="toggleSelection(i)" [class.selected]="p.selected">
|
||||||
|
<div class="btn-toolbar hover-actions z-10">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
<button class="btn btn-sm btn-dark" (click)="rotate(i); $event.stopPropagation()" title="Rotate page counter-clockwise" i18n-title>
|
||||||
|
<i-bs name="arrow-counterclockwise"></i-bs>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-dark" (click)="rotate(i); $event.stopPropagation()" title="Rotate page clockwise" i18n-title>
|
||||||
|
<i-bs name="arrow-clockwise"></i-bs>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-dark text-danger" (click)="remove(i); $event.stopPropagation()" title="Delete page" i18n-title>
|
||||||
|
<i-bs name="trash"></i-bs>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-dark" (click)="toggleSplit(i); $event.stopPropagation()" title="Add / remove document split here" i18n-title>
|
||||||
|
<i-bs name="scissors"></i-bs>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border-end border-bottom bg-light py-1 px-2 document-check z-10">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="page{{i}}" [checked]="p.selected" (click)="toggleSelection(i); $event.stopPropagation()">
|
||||||
|
<label class="form-check-label" for="page{{i}}"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pdf-viewer-container w-100" [class.selected]="p.selected">
|
||||||
|
@defer (on viewport) {
|
||||||
|
@if (!p.loaded) {
|
||||||
|
<div class="placeholder-glow w-100 h-100 z-10">
|
||||||
|
<span class="placeholder w-100 h-100"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<pdf-viewer class="fade" [class.show]="p.loaded" [src]="pdfSrc" [page]="p.page" [rotation]="p.rotate" [original-size]="false" [show-all]="false" [render-text]="false" (page-rendered)="p.loaded = true"></pdf-viewer>
|
||||||
|
} @placeholder {
|
||||||
|
<div class="placeholder-glow w-100 h-100 z-10">
|
||||||
|
<span class="placeholder w-100 h-100"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (p.splitAfter) {
|
||||||
|
<div class="split-after rounded position-absolute top-0 end-0 bg-dark text-uppercase text-center h-100 px-1 small fw-bold">— <span i18n>Split here</span> —</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer flex-column">
|
||||||
|
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="PdfEditorEditMode.Create" id="editModeCreate" name="editmode">
|
||||||
|
<label for="editModeCreate" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i-bs name="plus"></i-bs>
|
||||||
|
<span class="form-check-label ms-1" i18n>Create new document(s)</span>
|
||||||
|
</label>
|
||||||
|
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="PdfEditorEditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()">
|
||||||
|
<label for="editModeUpdate" class="btn btn-outline-primary btn-sm">
|
||||||
|
<i-bs name="pencil"></i-bs>
|
||||||
|
<span class="form-check-label ms-2" i18n>Update existing document</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@if (editMode === PdfEditorEditMode.Create) {
|
||||||
|
<div class="form-check ms-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="copyMeta" [(ngModel)]="includeMetadata">
|
||||||
|
<label class="form-check-label" for="copyMeta" i18n>Copy metadata</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check ms-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="deleteOriginal" [(ngModel)]="deleteOriginal">
|
||||||
|
<label class="form-check-label" for="deleteOriginal" i18n>Delete original</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button type="button" class="btn ms-auto 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
|
||||||
|
.page-item {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background-origin: border-box;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--pngx-primary-darken-5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer-container {
|
||||||
|
background-color: gray;
|
||||||
|
height: 240px;
|
||||||
|
|
||||||
|
pdf-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .ng2-pdf-viewer-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:hover .hover-actions {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-check {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-bottom-right-radius: 0.25rem;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
padding: 0;
|
||||||
|
min-height: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item:hover .document-check, .selected .document-check {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-10 {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-after {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
}
|
@@ -0,0 +1,142 @@
|
|||||||
|
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 { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { PDFEditorComponent } from './pdf-editor.component'
|
||||||
|
|
||||||
|
describe('PDFEditorComponent', () => {
|
||||||
|
let component: PDFEditorComponent
|
||||||
|
let fixture: ComponentFixture<PDFEditorComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PDFEditorComponent, NgxBootstrapIconsModule.pick(allIcons)],
|
||||||
|
providers: [
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
{ provide: NgbActiveModal, useValue: {} },
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
fixture = TestBed.createComponent(PDFEditorComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return correct operations with no changes', () => {
|
||||||
|
component.pages = [
|
||||||
|
{ page: 1, rotate: 0, splitAfter: false },
|
||||||
|
{ page: 2, rotate: 0, splitAfter: false },
|
||||||
|
{ page: 3, rotate: 0, splitAfter: false },
|
||||||
|
]
|
||||||
|
const ops = component.getOperations()
|
||||||
|
expect(ops).toEqual([
|
||||||
|
{ page: 1, rotate: 0, doc: 0 },
|
||||||
|
{ page: 2, rotate: 0, doc: 0 },
|
||||||
|
{ page: 3, rotate: 0, doc: 0 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should rotate, delete and reorder pages', () => {
|
||||||
|
component.pages = [
|
||||||
|
{ page: 1, rotate: 0, splitAfter: false, selected: false },
|
||||||
|
{ page: 2, rotate: 0, splitAfter: false, selected: false },
|
||||||
|
]
|
||||||
|
component.toggleSelection(0)
|
||||||
|
component.rotateSelected(90)
|
||||||
|
expect(component.pages[0].rotate).toBe(90)
|
||||||
|
component.toggleSelection(0) // deselect
|
||||||
|
component.toggleSelection(1)
|
||||||
|
component.deleteSelected()
|
||||||
|
expect(component.pages.length).toBe(1)
|
||||||
|
component.pages.push({ page: 2, rotate: 0, splitAfter: false })
|
||||||
|
component.drop({ previousIndex: 0, currentIndex: 1 } as any)
|
||||||
|
expect(component.pages[0].page).toBe(2)
|
||||||
|
component.rotate(0)
|
||||||
|
expect(component.pages[0].rotate).toBe(90)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty pages array', () => {
|
||||||
|
component.pages = []
|
||||||
|
expect(component.getOperations()).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should increment doc index after splitAfter', () => {
|
||||||
|
component.pages = [
|
||||||
|
{ page: 1, rotate: 0, splitAfter: true },
|
||||||
|
{ page: 2, rotate: 0, splitAfter: false },
|
||||||
|
{ page: 3, rotate: 0, splitAfter: true },
|
||||||
|
{ page: 4, rotate: 0, splitAfter: false },
|
||||||
|
]
|
||||||
|
const ops = component.getOperations()
|
||||||
|
expect(ops).toEqual([
|
||||||
|
{ page: 1, rotate: 0, doc: 0 },
|
||||||
|
{ page: 2, rotate: 0, doc: 1 },
|
||||||
|
{ page: 3, rotate: 0, doc: 1 },
|
||||||
|
{ page: 4, rotate: 0, doc: 2 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include rotations in operations', () => {
|
||||||
|
component.pages = [
|
||||||
|
{ page: 1, rotate: 90, splitAfter: false },
|
||||||
|
{ page: 2, rotate: 180, splitAfter: true },
|
||||||
|
{ page: 3, rotate: 270, splitAfter: false },
|
||||||
|
]
|
||||||
|
const ops = component.getOperations()
|
||||||
|
expect(ops).toEqual([
|
||||||
|
{ page: 1, rotate: 90, doc: 0 },
|
||||||
|
{ page: 2, rotate: 180, doc: 0 },
|
||||||
|
{ page: 3, rotate: 270, doc: 1 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle remove operation', () => {
|
||||||
|
component.pages = [
|
||||||
|
{ page: 1, rotate: 0, splitAfter: false, selected: false },
|
||||||
|
{ page: 2, rotate: 0, splitAfter: false, selected: true },
|
||||||
|
{ page: 3, rotate: 0, splitAfter: false, selected: false },
|
||||||
|
]
|
||||||
|
component.remove(1) // remove page 2
|
||||||
|
expect(component.pages.length).toBe(2)
|
||||||
|
expect(component.pages[0].page).toBe(1)
|
||||||
|
expect(component.pages[1].page).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should toggle splitAfter correctly', () => {
|
||||||
|
component.pages = [
|
||||||
|
{ page: 1, rotate: 0, splitAfter: false },
|
||||||
|
{ page: 2, rotate: 0, splitAfter: false },
|
||||||
|
]
|
||||||
|
component.toggleSplit(0)
|
||||||
|
expect(component.pages[0].splitAfter).toBeTruthy()
|
||||||
|
component.toggleSplit(1)
|
||||||
|
expect(component.pages[1].splitAfter).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should select and deselect all pages', () => {
|
||||||
|
component.pages = [
|
||||||
|
{ page: 1, rotate: 0, splitAfter: false, selected: false },
|
||||||
|
{ page: 2, rotate: 0, splitAfter: false, selected: false },
|
||||||
|
]
|
||||||
|
component.selectAll()
|
||||||
|
expect(component.pages.every((p) => p.selected)).toBeTruthy()
|
||||||
|
expect(component.hasSelection()).toBeTruthy()
|
||||||
|
component.deselectAll()
|
||||||
|
expect(component.pages.every((p) => !p.selected)).toBeTruthy()
|
||||||
|
expect(component.hasSelection()).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle pdf loading and page generation', () => {
|
||||||
|
const mockPdf = {
|
||||||
|
numPages: 3,
|
||||||
|
getPage: (pageNum: number) => Promise.resolve({ pageNumber: pageNum }),
|
||||||
|
}
|
||||||
|
component.pdfLoaded(mockPdf as any)
|
||||||
|
expect(component.totalPages).toBe(3)
|
||||||
|
expect(component.pages.length).toBe(3)
|
||||||
|
expect(component.pages[0].page).toBe(1)
|
||||||
|
expect(component.pages[1].page).toBe(2)
|
||||||
|
expect(component.pages[2].page).toBe(3)
|
||||||
|
})
|
||||||
|
})
|
@@ -0,0 +1,133 @@
|
|||||||
|
import {
|
||||||
|
CdkDragDrop,
|
||||||
|
DragDropModule,
|
||||||
|
moveItemInArray,
|
||||||
|
} from '@angular/cdk/drag-drop'
|
||||||
|
import { Component, inject } from '@angular/core'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
|
||||||
|
|
||||||
|
interface PageOperation {
|
||||||
|
page: number
|
||||||
|
rotate: number
|
||||||
|
splitAfter: boolean
|
||||||
|
selected?: boolean
|
||||||
|
loaded?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PdfEditorEditMode {
|
||||||
|
Update = 'update',
|
||||||
|
Create = 'create',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-pdf-editor',
|
||||||
|
templateUrl: './pdf-editor.component.html',
|
||||||
|
styleUrl: './pdf-editor.component.scss',
|
||||||
|
imports: [
|
||||||
|
DragDropModule,
|
||||||
|
FormsModule,
|
||||||
|
PdfViewerModule,
|
||||||
|
NgxBootstrapIconsModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PDFEditorComponent extends ConfirmDialogComponent {
|
||||||
|
public PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
|
private documentService = inject(DocumentService)
|
||||||
|
activeModal: NgbActiveModal = inject(NgbActiveModal)
|
||||||
|
|
||||||
|
documentID: number
|
||||||
|
pages: PageOperation[] = []
|
||||||
|
totalPages = 0
|
||||||
|
editMode: PdfEditorEditMode = PdfEditorEditMode.Create
|
||||||
|
deleteOriginal: boolean = false
|
||||||
|
includeMetadata: boolean = true
|
||||||
|
|
||||||
|
get pdfSrc(): string {
|
||||||
|
return this.documentService.getPreviewUrl(this.documentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfLoaded(pdf: PDFDocumentProxy) {
|
||||||
|
this.totalPages = pdf.numPages
|
||||||
|
this.pages = Array.from({ length: this.totalPages }, (_, i) => ({
|
||||||
|
page: i + 1,
|
||||||
|
rotate: 0,
|
||||||
|
splitAfter: false,
|
||||||
|
selected: false,
|
||||||
|
loaded: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSelection(i: number) {
|
||||||
|
this.pages[i].selected = !this.pages[i].selected
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(i: number) {
|
||||||
|
this.pages[i].rotate = (this.pages[i].rotate + 90) % 360
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateSelected(dir: number) {
|
||||||
|
for (let p of this.pages) {
|
||||||
|
if (p.selected) {
|
||||||
|
p.rotate = (p.rotate + dir + 360) % 360
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(i: number) {
|
||||||
|
this.pages.splice(i, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSplit(i: number) {
|
||||||
|
this.pages[i].splitAfter = !this.pages[i].splitAfter
|
||||||
|
if (this.pages[i].splitAfter) {
|
||||||
|
// force create mode
|
||||||
|
this.editMode = PdfEditorEditMode.Create
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll() {
|
||||||
|
this.pages.forEach((p) => (p.selected = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
deselectAll() {
|
||||||
|
this.pages.forEach((p) => (p.selected = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSelected() {
|
||||||
|
this.pages = this.pages.filter((p) => !p.selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelection(): boolean {
|
||||||
|
return this.pages.some((p) => p.selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSplit(): boolean {
|
||||||
|
return this.pages.some((p) => p.splitAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(event: CdkDragDrop<PageOperation[]>) {
|
||||||
|
moveItemInArray(this.pages, event.previousIndex, event.currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
getOperations() {
|
||||||
|
return this.pages.map((p, idx) => ({
|
||||||
|
page: p.page,
|
||||||
|
rotate: p.rotate,
|
||||||
|
doc: this.computeDocIndex(idx),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeDocIndex(index: number): number {
|
||||||
|
let docIndex = 0
|
||||||
|
for (let i = 0; i <= index; i++) {
|
||||||
|
if (this.pages[i].splitAfter && i < index) docIndex++
|
||||||
|
}
|
||||||
|
return docIndex
|
||||||
|
}
|
||||||
|
}
|
@@ -58,16 +58,8 @@
|
|||||||
<i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span>
|
<i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="splitDocument()" [disabled]="!userCanAdd || originalContentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
|
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
||||||
<i-bs width="1em" height="1em" name="scissors"></i-bs> <span i18n>Split</span>
|
<i-bs name="pencil"></i-bs> <ng-container i18n>PDF Editor</ng-container>
|
||||||
</button>
|
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="rotateDocument()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
|
||||||
<i-bs name="arrow-clockwise"></i-bs> <ng-container i18n>Rotate</ng-container>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="deletePages()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
|
|
||||||
<i-bs name="file-earmark-minus"></i-bs> <ng-container i18n>Delete page(s)</ng-container>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1158,81 +1158,43 @@ describe('DocumentDetailComponent', () => {
|
|||||||
).not.toBeUndefined()
|
).not.toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support split', () => {
|
it('should support pdf editor, handle error', () => {
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||||
|
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
|
||||||
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
initNormally()
|
initNormally()
|
||||||
component.splitDocument()
|
component.editPdf()
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
modal.componentInstance.documentID = doc.id
|
modal.componentInstance.documentID = doc.id
|
||||||
modal.componentInstance.totalPages = 5
|
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }]
|
||||||
modal.componentInstance.page = 2
|
|
||||||
modal.componentInstance.addSplit()
|
|
||||||
modal.componentInstance.confirm()
|
modal.componentInstance.confirm()
|
||||||
let req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
)
|
)
|
||||||
expect(req.request.body).toEqual({
|
expect(req.request.body).toEqual({
|
||||||
documents: [doc.id],
|
documents: [doc.id],
|
||||||
method: 'split',
|
method: 'edit_pdf',
|
||||||
parameters: { pages: '1-2,3-5', delete_originals: false },
|
parameters: {
|
||||||
})
|
operations: [{ page: 1, rotate: 0, doc: 0 }],
|
||||||
req.error(new ProgressEvent('failed'))
|
delete_original: false,
|
||||||
modal.componentInstance.confirm()
|
update_document: false,
|
||||||
req = httpTestingController.expectOne(
|
include_metadata: true,
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
},
|
||||||
)
|
|
||||||
req.flush(true)
|
|
||||||
})
|
})
|
||||||
|
req.error(new ErrorEvent('failed'))
|
||||||
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
it('should support rotate', () => {
|
component.editPdf()
|
||||||
let modal: NgbModalRef
|
|
||||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
|
||||||
initNormally()
|
|
||||||
component.rotateDocument()
|
|
||||||
expect(modal).not.toBeUndefined()
|
|
||||||
modal.componentInstance.documentID = doc.id
|
modal.componentInstance.documentID = doc.id
|
||||||
modal.componentInstance.rotate()
|
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: true }]
|
||||||
modal.componentInstance.confirm()
|
modal.componentInstance.deleteOriginal = true
|
||||||
let req = httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
|
||||||
)
|
|
||||||
expect(req.request.body).toEqual({
|
|
||||||
documents: [doc.id],
|
|
||||||
method: 'rotate',
|
|
||||||
parameters: { degrees: 90 },
|
|
||||||
})
|
|
||||||
req.error(new ProgressEvent('failed'))
|
|
||||||
modal.componentInstance.confirm()
|
|
||||||
req = httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
|
||||||
)
|
|
||||||
req.flush(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should support delete pages', () => {
|
|
||||||
let modal: NgbModalRef
|
|
||||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
|
||||||
initNormally()
|
|
||||||
component.deletePages()
|
|
||||||
expect(modal).not.toBeUndefined()
|
|
||||||
modal.componentInstance.documentID = doc.id
|
|
||||||
modal.componentInstance.pages = [1, 2]
|
|
||||||
modal.componentInstance.confirm()
|
|
||||||
let req = httpTestingController.expectOne(
|
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
|
||||||
)
|
|
||||||
expect(req.request.body).toEqual({
|
|
||||||
documents: [doc.id],
|
|
||||||
method: 'delete_pages',
|
|
||||||
parameters: { pages: [1, 2] },
|
|
||||||
})
|
|
||||||
req.error(new ProgressEvent('failed'))
|
|
||||||
modal.componentInstance.confirm()
|
modal.componentInstance.confirm()
|
||||||
req = httpTestingController.expectOne(
|
req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
)
|
)
|
||||||
req.flush(true)
|
req.flush(true)
|
||||||
|
expect(closeSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support keyboard shortcuts', () => {
|
it('should support keyboard shortcuts', () => {
|
||||||
|
@@ -82,9 +82,6 @@ import { getFilenameFromContentDisposition } from 'src/app/utils/http'
|
|||||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||||
import * as UTIF from 'utif'
|
import * as UTIF from 'utif'
|
||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
|
|
||||||
import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
|
||||||
import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-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'
|
||||||
import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
@@ -102,6 +99,10 @@ import { TagsComponent } from '../common/input/tags/tags.component'
|
|||||||
import { TextComponent } from '../common/input/text/text.component'
|
import { TextComponent } from '../common/input/text/text.component'
|
||||||
import { UrlComponent } from '../common/input/url/url.component'
|
import { UrlComponent } from '../common/input/url/url.component'
|
||||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||||
|
import {
|
||||||
|
PDFEditorComponent,
|
||||||
|
PdfEditorEditMode,
|
||||||
|
} from '../common/pdf-editor/pdf-editor.component'
|
||||||
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
||||||
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
||||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||||
@@ -1349,13 +1350,13 @@ export class DocumentDetailComponent
|
|||||||
this.documentForm.updateValueAndValidity()
|
this.documentForm.updateValueAndValidity()
|
||||||
}
|
}
|
||||||
|
|
||||||
splitDocument() {
|
editPdf() {
|
||||||
let modal = this.modalService.open(SplitConfirmDialogComponent, {
|
let modal = this.modalService.open(PDFEditorComponent, {
|
||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
size: 'lg',
|
size: 'xl',
|
||||||
|
scrollable: true,
|
||||||
})
|
})
|
||||||
modal.componentInstance.title = $localize`Split confirm`
|
modal.componentInstance.title = $localize`PDF Editor`
|
||||||
modal.componentInstance.messageBold = $localize`This operation will split the selected document(s) into new documents.`
|
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.documentID = this.document.id
|
modal.componentInstance.documentID = this.document.id
|
||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
@@ -1363,103 +1364,30 @@ export class DocumentDetailComponent
|
|||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled = false
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.bulkEdit([this.document.id], 'split', {
|
.bulkEdit([this.document.id], 'edit_pdf', {
|
||||||
pages: modal.componentInstance.pagesString,
|
operations: modal.componentInstance.getOperations(),
|
||||||
delete_originals: modal.componentInstance.deleteOriginal,
|
delete_original: modal.componentInstance.deleteOriginal,
|
||||||
|
update_document:
|
||||||
|
modal.componentInstance.editMode == PdfEditorEditMode.Update,
|
||||||
|
include_metadata: modal.componentInstance.includeMetadata,
|
||||||
})
|
})
|
||||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.showInfo(
|
this.toastService.showInfo(
|
||||||
$localize`Split operation for "${this.document.title}" will begin in the background.`
|
$localize`PDF edit operation for "${this.document.title}" will begin in the background.`
|
||||||
)
|
)
|
||||||
modal.close()
|
modal.close()
|
||||||
|
if (modal.componentInstance.deleteOriginal) {
|
||||||
|
this.openDocumentService.closeDocument(this.document)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled = true
|
||||||
}
|
}
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing split operation`,
|
$localize`Error executing PDF edit operation`,
|
||||||
error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
rotateDocument() {
|
|
||||||
let modal = this.modalService.open(RotateConfirmDialogComponent, {
|
|
||||||
backdrop: 'static',
|
|
||||||
size: 'lg',
|
|
||||||
})
|
|
||||||
modal.componentInstance.title = $localize`Rotate confirm`
|
|
||||||
modal.componentInstance.messageBold = $localize`This operation will permanently rotate the original version of the current document.`
|
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
|
||||||
modal.componentInstance.documentID = this.document.id
|
|
||||||
modal.componentInstance.showPDFNote = false
|
|
||||||
modal.componentInstance.confirmClicked
|
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
||||||
.subscribe(() => {
|
|
||||||
modal.componentInstance.buttonsEnabled = false
|
|
||||||
this.documentsService
|
|
||||||
.bulkEdit([this.document.id], 'rotate', {
|
|
||||||
degrees: modal.componentInstance.degrees,
|
|
||||||
})
|
|
||||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.toastService.show({
|
|
||||||
content: $localize`Rotation of "${this.document.title}" will begin in the background. Close and re-open the document after the operation has completed to see the changes.`,
|
|
||||||
delay: 8000,
|
|
||||||
action: this.close.bind(this),
|
|
||||||
actionName: $localize`Close`,
|
|
||||||
})
|
|
||||||
modal.close()
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
if (modal) {
|
|
||||||
modal.componentInstance.buttonsEnabled = true
|
|
||||||
}
|
|
||||||
this.toastService.showError(
|
|
||||||
$localize`Error executing rotate operation`,
|
|
||||||
error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
deletePages() {
|
|
||||||
let modal = this.modalService.open(DeletePagesConfirmDialogComponent, {
|
|
||||||
backdrop: 'static',
|
|
||||||
})
|
|
||||||
modal.componentInstance.title = $localize`Delete pages confirm`
|
|
||||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete the selected pages from the original document.`
|
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
|
||||||
modal.componentInstance.documentID = this.document.id
|
|
||||||
modal.componentInstance.confirmClicked
|
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
|
||||||
.subscribe(() => {
|
|
||||||
modal.componentInstance.buttonsEnabled = false
|
|
||||||
this.documentsService
|
|
||||||
.bulkEdit([this.document.id], 'delete_pages', {
|
|
||||||
pages: modal.componentInstance.pages,
|
|
||||||
})
|
|
||||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.toastService.showInfo(
|
|
||||||
$localize`Delete pages operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.`
|
|
||||||
)
|
|
||||||
modal.close()
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
if (modal) {
|
|
||||||
modal.componentInstance.buttonsEnabled = true
|
|
||||||
}
|
|
||||||
this.toastService.showError(
|
|
||||||
$localize`Error executing delete pages operation`,
|
|
||||||
error
|
error
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@@ -497,6 +497,103 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
|
|||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def edit_pdf(
|
||||||
|
doc_ids: list[int],
|
||||||
|
operations: list[dict],
|
||||||
|
*,
|
||||||
|
delete_original: bool = False,
|
||||||
|
update_document: bool = False,
|
||||||
|
include_metadata: bool = True,
|
||||||
|
user: User | None = None,
|
||||||
|
) -> Literal["OK"]:
|
||||||
|
"""
|
||||||
|
Operations is a list of dictionaries describing the final PDF pages.
|
||||||
|
Each entry must contain the original page number in `page` and may
|
||||||
|
specify `rotate` in degrees and `doc` indicating the output
|
||||||
|
document index (for splitting). Pages omitted from the list are
|
||||||
|
discarded.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Editing PDF of document {doc_ids[0]} with {len(operations)} operations",
|
||||||
|
)
|
||||||
|
doc = Document.objects.get(id=doc_ids[0])
|
||||||
|
import pikepdf
|
||||||
|
|
||||||
|
pdf_docs: list[pikepdf.Pdf] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
with pikepdf.open(doc.source_path) as src:
|
||||||
|
# prepare output documents
|
||||||
|
max_idx = max(op.get("doc", 0) for op in operations)
|
||||||
|
pdf_docs = [pikepdf.new() for _ in range(max_idx + 1)]
|
||||||
|
|
||||||
|
if update_document and len(pdf_docs) > 1:
|
||||||
|
logger.error(
|
||||||
|
"Update requested but multiple output documents specified",
|
||||||
|
)
|
||||||
|
raise ValueError("Multiple output documents specified")
|
||||||
|
|
||||||
|
for op in operations:
|
||||||
|
dst = pdf_docs[op.get("doc", 0)]
|
||||||
|
page = src.pages[op["page"] - 1]
|
||||||
|
dst.pages.append(page)
|
||||||
|
if op.get("rotate"):
|
||||||
|
dst.pages[-1].rotate(op["rotate"], relative=True)
|
||||||
|
|
||||||
|
if update_document:
|
||||||
|
temp_path = doc.source_path.with_suffix(".tmp.pdf")
|
||||||
|
pdf = pdf_docs[0]
|
||||||
|
pdf.remove_unreferenced_resources()
|
||||||
|
# save the edited PDF to a temporary file in case of errors
|
||||||
|
pdf.save(temp_path)
|
||||||
|
# replace the original document with the edited one
|
||||||
|
temp_path.replace(doc.source_path)
|
||||||
|
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
|
||||||
|
doc.page_count = len(pdf.pages)
|
||||||
|
doc.save()
|
||||||
|
update_document_content_maybe_archive_file.delay(document_id=doc.id)
|
||||||
|
else:
|
||||||
|
consume_tasks = []
|
||||||
|
overrides = (
|
||||||
|
DocumentMetadataOverrides().from_document(doc)
|
||||||
|
if include_metadata
|
||||||
|
else DocumentMetadataOverrides()
|
||||||
|
)
|
||||||
|
if user is not None:
|
||||||
|
overrides.owner_id = user.id
|
||||||
|
|
||||||
|
for idx, pdf in enumerate(pdf_docs, start=1):
|
||||||
|
filepath: Path = (
|
||||||
|
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
||||||
|
/ f"{doc.id}_edit_{idx}.pdf"
|
||||||
|
)
|
||||||
|
pdf.remove_unreferenced_resources()
|
||||||
|
pdf.save(filepath)
|
||||||
|
consume_tasks.append(
|
||||||
|
consume_file.s(
|
||||||
|
ConsumableDocument(
|
||||||
|
source=DocumentSource.ConsumeFolder,
|
||||||
|
original_file=filepath,
|
||||||
|
),
|
||||||
|
overrides,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if delete_original:
|
||||||
|
chord(header=consume_tasks, body=delete.si([doc.id])).delay()
|
||||||
|
else:
|
||||||
|
group(consume_tasks).delay()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error editing document {doc.id}: {e}")
|
||||||
|
raise ValueError(
|
||||||
|
f"An error occurred while editing the document: {e}",
|
||||||
|
) from e
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
def reflect_doclinks(
|
def reflect_doclinks(
|
||||||
document: Document,
|
document: Document,
|
||||||
field: CustomField,
|
field: CustomField,
|
||||||
|
@@ -1293,6 +1293,7 @@ class BulkEditSerializer(
|
|||||||
"merge",
|
"merge",
|
||||||
"split",
|
"split",
|
||||||
"delete_pages",
|
"delete_pages",
|
||||||
|
"edit_pdf",
|
||||||
],
|
],
|
||||||
label="Method",
|
label="Method",
|
||||||
write_only=True,
|
write_only=True,
|
||||||
@@ -1366,7 +1367,10 @@ class BulkEditSerializer(
|
|||||||
return bulk_edit.split
|
return bulk_edit.split
|
||||||
elif method == "delete_pages":
|
elif method == "delete_pages":
|
||||||
return bulk_edit.delete_pages
|
return bulk_edit.delete_pages
|
||||||
else:
|
elif method == "edit_pdf":
|
||||||
|
return bulk_edit.edit_pdf
|
||||||
|
else: # pragma: no cover
|
||||||
|
# This will never happen as it is handled by the ChoiceField
|
||||||
raise serializers.ValidationError("Unsupported method.")
|
raise serializers.ValidationError("Unsupported method.")
|
||||||
|
|
||||||
def _validate_parameters_tags(self, parameters):
|
def _validate_parameters_tags(self, parameters):
|
||||||
@@ -1520,6 +1524,47 @@ class BulkEditSerializer(
|
|||||||
else:
|
else:
|
||||||
parameters["archive_fallback"] = False
|
parameters["archive_fallback"] = False
|
||||||
|
|
||||||
|
def _validate_parameters_edit_pdf(self, parameters, document_id):
|
||||||
|
if "operations" not in parameters:
|
||||||
|
raise serializers.ValidationError("operations not specified")
|
||||||
|
if not isinstance(parameters["operations"], list):
|
||||||
|
raise serializers.ValidationError("operations must be a list")
|
||||||
|
for op in parameters["operations"]:
|
||||||
|
if not isinstance(op, dict):
|
||||||
|
raise serializers.ValidationError("invalid operation entry")
|
||||||
|
if "page" not in op or not isinstance(op["page"], int):
|
||||||
|
raise serializers.ValidationError("page must be an integer")
|
||||||
|
if "rotate" in op and not isinstance(op["rotate"], int):
|
||||||
|
raise serializers.ValidationError("rotate must be an integer")
|
||||||
|
if "doc" in op and not isinstance(op["doc"], int):
|
||||||
|
raise serializers.ValidationError("doc must be an integer")
|
||||||
|
if "update_document" in parameters:
|
||||||
|
if not isinstance(parameters["update_document"], bool):
|
||||||
|
raise serializers.ValidationError("update_document must be a boolean")
|
||||||
|
else:
|
||||||
|
parameters["update_document"] = False
|
||||||
|
if "include_metadata" in parameters:
|
||||||
|
if not isinstance(parameters["include_metadata"], bool):
|
||||||
|
raise serializers.ValidationError("include_metadata must be a boolean")
|
||||||
|
else:
|
||||||
|
parameters["include_metadata"] = True
|
||||||
|
|
||||||
|
if parameters["update_document"]:
|
||||||
|
max_idx = max(op.get("doc", 0) for op in parameters["operations"])
|
||||||
|
if max_idx > 0:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"update_document only allowed with a single output document",
|
||||||
|
)
|
||||||
|
|
||||||
|
doc = Document.objects.get(id=document_id)
|
||||||
|
# doc existence is already validated
|
||||||
|
if doc.page_count:
|
||||||
|
for op in parameters["operations"]:
|
||||||
|
if op["page"] < 1 or op["page"] > doc.page_count:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.",
|
||||||
|
)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
method = attrs["method"]
|
method = attrs["method"]
|
||||||
parameters = attrs["parameters"]
|
parameters = attrs["parameters"]
|
||||||
@@ -1554,6 +1599,12 @@ class BulkEditSerializer(
|
|||||||
self._validate_parameters_delete_pages(parameters)
|
self._validate_parameters_delete_pages(parameters)
|
||||||
elif method == bulk_edit.merge:
|
elif method == bulk_edit.merge:
|
||||||
self._validate_parameters_merge(parameters)
|
self._validate_parameters_merge(parameters)
|
||||||
|
elif method == bulk_edit.edit_pdf:
|
||||||
|
if len(attrs["documents"]) > 1:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Edit PDF method only supports one document",
|
||||||
|
)
|
||||||
|
self._validate_parameters_edit_pdf(parameters, attrs["documents"][0])
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@@ -660,6 +663,28 @@ def run_workflows_updated(sender, document: Document, logging_group=None, **kwar
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_public_ip(ip: str) -> bool:
|
||||||
|
try:
|
||||||
|
obj = ipaddress.ip_address(ip)
|
||||||
|
return not (
|
||||||
|
obj.is_private
|
||||||
|
or obj.is_loopback
|
||||||
|
or obj.is_link_local
|
||||||
|
or obj.is_multicast
|
||||||
|
or obj.is_unspecified
|
||||||
|
)
|
||||||
|
except ValueError: # pragma: no cover
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_first_ip(host: str) -> str | None:
|
||||||
|
try:
|
||||||
|
info = socket.getaddrinfo(host, None)
|
||||||
|
return info[0][4][0] if info else None
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@shared_task(
|
@shared_task(
|
||||||
retry_backoff=True,
|
retry_backoff=True,
|
||||||
autoretry_for=(httpx.HTTPStatusError,),
|
autoretry_for=(httpx.HTTPStatusError,),
|
||||||
@@ -674,11 +699,35 @@ def send_webhook(
|
|||||||
*,
|
*,
|
||||||
as_json: bool = False,
|
as_json: bool = False,
|
||||||
):
|
):
|
||||||
|
p = urlparse(url)
|
||||||
|
if p.scheme.lower() not in settings.WEBHOOKS_ALLOWED_SCHEMES or not p.hostname:
|
||||||
|
logger.warning("Webhook blocked: invalid scheme/hostname")
|
||||||
|
raise ValueError("Invalid URL scheme or hostname.")
|
||||||
|
|
||||||
|
port = p.port or (443 if p.scheme == "https" else 80)
|
||||||
|
if (
|
||||||
|
len(settings.WEBHOOKS_ALLOWED_PORTS) > 0
|
||||||
|
and port not in settings.WEBHOOKS_ALLOWED_PORTS
|
||||||
|
):
|
||||||
|
logger.warning("Webhook blocked: port not permitted")
|
||||||
|
raise ValueError("Destination port not permitted.")
|
||||||
|
|
||||||
|
ip = _resolve_first_ip(p.hostname)
|
||||||
|
if not ip or (
|
||||||
|
not _is_public_ip(ip) and not settings.WEBHOOKS_ALLOW_INTERNAL_REQUESTS
|
||||||
|
):
|
||||||
|
logger.warning("Webhook blocked: destination not allowed")
|
||||||
|
raise ValueError("Destination host is not allowed.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
post_args = {
|
post_args = {
|
||||||
"url": url,
|
"url": url,
|
||||||
"headers": headers,
|
"headers": {
|
||||||
"files": files,
|
k: v for k, v in (headers or {}).items() if k.lower() != "host"
|
||||||
|
},
|
||||||
|
"files": files or None,
|
||||||
|
"timeout": 5.0,
|
||||||
|
"follow_redirects": False,
|
||||||
}
|
}
|
||||||
if as_json:
|
if as_json:
|
||||||
post_args["json"] = data
|
post_args["json"] = data
|
||||||
@@ -699,15 +748,6 @@ def send_webhook(
|
|||||||
)
|
)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Webhook sent to {url}",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Failed attempt sending webhook to {url}: {e}",
|
|
||||||
)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def run_workflows(
|
def run_workflows(
|
||||||
trigger_type: WorkflowTrigger.WorkflowTriggerType,
|
trigger_type: WorkflowTrigger.WorkflowTriggerType,
|
||||||
|
@@ -41,6 +41,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
title="B",
|
title="B",
|
||||||
correspondent=self.c1,
|
correspondent=self.c1,
|
||||||
document_type=self.dt1,
|
document_type=self.dt1,
|
||||||
|
page_count=5,
|
||||||
)
|
)
|
||||||
self.doc3 = Document.objects.create(
|
self.doc3 = Document.objects.create(
|
||||||
checksum="C",
|
checksum="C",
|
||||||
@@ -1369,6 +1370,218 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn(b"pages must be a list of integers", response.content)
|
self.assertIn(b"pages must be a list of integers", response.content)
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.edit_pdf")
|
||||||
|
def test_edit_pdf(self, m):
|
||||||
|
self.setup_mock(m, "edit_pdf")
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 1}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
m.assert_called_once()
|
||||||
|
args, kwargs = m.call_args
|
||||||
|
self.assertCountEqual(args[0], [self.doc2.id])
|
||||||
|
self.assertEqual(kwargs["operations"], [{"page": 1}])
|
||||||
|
self.assertEqual(kwargs["user"], self.user)
|
||||||
|
|
||||||
|
def test_edit_pdf_invalid_params(self):
|
||||||
|
# multiple documents
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id, self.doc3.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 1}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"Edit PDF method only supports one document", response.content)
|
||||||
|
|
||||||
|
# no operations specified
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"operations not specified", response.content)
|
||||||
|
|
||||||
|
# operations not a list
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": "not_a_list"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"operations must be a list", response.content)
|
||||||
|
|
||||||
|
# invalid operation
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": ["invalid_operation"]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"invalid operation entry", response.content)
|
||||||
|
|
||||||
|
# page not an int
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": "not_an_int"}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"page must be an integer", response.content)
|
||||||
|
|
||||||
|
# rotate not an int
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 1, "rotate": "not_an_int"}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"rotate must be an integer", response.content)
|
||||||
|
|
||||||
|
# doc not an int
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 1, "doc": "not_an_int"}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"doc must be an integer", response.content)
|
||||||
|
|
||||||
|
# update_document not a boolean
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {
|
||||||
|
"update_document": "not_a_bool",
|
||||||
|
"operations": [{"page": 1}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"update_document must be a boolean", response.content)
|
||||||
|
|
||||||
|
# include_metadata not a boolean
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {
|
||||||
|
"include_metadata": "not_a_bool",
|
||||||
|
"operations": [{"page": 1}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"include_metadata must be a boolean", response.content)
|
||||||
|
|
||||||
|
# update_document True but output would be multiple documents
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {
|
||||||
|
"update_document": True,
|
||||||
|
"operations": [{"page": 1, "doc": 1}, {"page": 2, "doc": 2}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(
|
||||||
|
b"update_document only allowed with a single output document",
|
||||||
|
response.content,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.edit_pdf")
|
||||||
|
def test_edit_pdf_page_out_of_bounds(self, m):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- API data for editing PDF is called
|
||||||
|
- The page number is out of bounds
|
||||||
|
WHEN:
|
||||||
|
- API is called
|
||||||
|
THEN:
|
||||||
|
- The API fails with a correct error code
|
||||||
|
"""
|
||||||
|
self.setup_mock(m, "edit_pdf")
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 99}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"out of bounds", response.content)
|
||||||
|
|
||||||
@override_settings(AUDIT_LOG_ENABLED=True)
|
@override_settings(AUDIT_LOG_ENABLED=True)
|
||||||
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
||||||
"""
|
"""
|
||||||
|
@@ -909,3 +909,156 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
expected_str = "Error deleting pages from document"
|
expected_str = "Error deleting pages from document"
|
||||||
self.assertIn(expected_str, error_str)
|
self.assertIn(expected_str, error_str)
|
||||||
mock_update_archive_file.assert_not_called()
|
mock_update_archive_file.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_basic_operations(self, mock_consume_file, mock_group):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with two operations to split the doc and rotate pages
|
||||||
|
THEN:
|
||||||
|
- A grouped task is generated and delay() is called
|
||||||
|
"""
|
||||||
|
mock_group.return_value.delay.return_value = None
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1, "doc": 0}, {"page": 2, "doc": 1, "rotate": 90}]
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations)
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
mock_group.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_with_user_override(self, mock_consume_file, mock_group):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with user override
|
||||||
|
THEN:
|
||||||
|
- Task is created with user context
|
||||||
|
"""
|
||||||
|
mock_group.return_value.delay.return_value = None
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1, "doc": 0}, {"page": 2, "doc": 1}]
|
||||||
|
user = User.objects.create(username="editor")
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations, user=user)
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
mock_group.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.chord")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_with_delete_original(self, mock_consume_file, mock_chord):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with delete_original=True
|
||||||
|
THEN:
|
||||||
|
- Task group is triggered
|
||||||
|
"""
|
||||||
|
mock_chord.return_value.delay.return_value = None
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1}, {"page": 2}]
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations, delete_original=True)
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
mock_chord.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay")
|
||||||
|
def test_edit_pdf_with_update_document(self, mock_update_document):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A single existing PDF document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with update_document=True and a single output
|
||||||
|
THEN:
|
||||||
|
- The original document is updated in-place
|
||||||
|
- The update_document_content_maybe_archive_file task is triggered
|
||||||
|
"""
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1}, {"page": 2}]
|
||||||
|
original_checksum = self.doc2.checksum
|
||||||
|
original_page_count = self.doc2.page_count
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(
|
||||||
|
doc_ids,
|
||||||
|
operations=operations,
|
||||||
|
update_document=True,
|
||||||
|
delete_original=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
self.doc2.refresh_from_db()
|
||||||
|
self.assertNotEqual(self.doc2.checksum, original_checksum)
|
||||||
|
self.assertNotEqual(self.doc2.page_count, original_page_count)
|
||||||
|
mock_update_document.assert_called_once_with(document_id=self.doc2.id)
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_without_metadata(self, mock_consume_file, mock_group):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with include_metadata=False
|
||||||
|
THEN:
|
||||||
|
- Tasks are created with empty metadata
|
||||||
|
"""
|
||||||
|
mock_group.return_value.delay.return_value = None
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1}]
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations, include_metadata=False)
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
mock_group.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_open_failure(self, mock_consume_file, mock_group):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf fails to open PDF
|
||||||
|
THEN:
|
||||||
|
- Task group is not called
|
||||||
|
"""
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [
|
||||||
|
{"page": 9999}, # invalid page, forces error during PDF load
|
||||||
|
]
|
||||||
|
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
bulk_edit.edit_pdf(doc_ids, operations)
|
||||||
|
mock_group.assert_not_called()
|
||||||
|
mock_consume_file.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_multiple_outputs_with_update_flag_errors(
|
||||||
|
self,
|
||||||
|
mock_consume_file,
|
||||||
|
mock_group,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with multiple outputs and update_document=True
|
||||||
|
THEN:
|
||||||
|
- An error is logged and task group is not called
|
||||||
|
"""
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [
|
||||||
|
{"page": 1, "doc": 0},
|
||||||
|
{"page": 2, "doc": 1},
|
||||||
|
]
|
||||||
|
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
bulk_edit.edit_pdf(doc_ids, operations, update_document=True)
|
||||||
|
mock_group.assert_not_called()
|
||||||
|
mock_consume_file.assert_not_called()
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
@@ -10,6 +12,7 @@ from django.utils import timezone
|
|||||||
from guardian.shortcuts import assign_perm
|
from guardian.shortcuts import assign_perm
|
||||||
from guardian.shortcuts import get_groups_with_perms
|
from guardian.shortcuts import get_groups_with_perms
|
||||||
from guardian.shortcuts import get_users_with_perms
|
from guardian.shortcuts import get_users_with_perms
|
||||||
|
from httpx import HTTPError
|
||||||
from httpx import HTTPStatusError
|
from httpx import HTTPStatusError
|
||||||
from pytest_httpx import HTTPXMock
|
from pytest_httpx import HTTPXMock
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
@@ -2825,6 +2828,8 @@ class TestWorkflows(
|
|||||||
content="Test message",
|
content="Test message",
|
||||||
headers={},
|
headers={},
|
||||||
files=None,
|
files=None,
|
||||||
|
follow_redirects=False,
|
||||||
|
timeout=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_str = "Webhook sent to http://paperless-ngx.com"
|
expected_str = "Webhook sent to http://paperless-ngx.com"
|
||||||
@@ -2842,6 +2847,8 @@ class TestWorkflows(
|
|||||||
data={"message": "Test message"},
|
data={"message": "Test message"},
|
||||||
headers={},
|
headers={},
|
||||||
files=None,
|
files=None,
|
||||||
|
follow_redirects=False,
|
||||||
|
timeout=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch("httpx.post")
|
@mock.patch("httpx.post")
|
||||||
@@ -2962,3 +2969,164 @@ class TestWebhookSend:
|
|||||||
as_json=True,
|
as_json=True,
|
||||||
)
|
)
|
||||||
assert httpx_mock.get_request().headers["Content-Type"] == "application/json"
|
assert httpx_mock.get_request().headers["Content-Type"] == "application/json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def resolve_to(monkeypatch):
|
||||||
|
"""
|
||||||
|
Force DNS resolution to a specific IP for any hostname.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _set(ip: str):
|
||||||
|
def fake_getaddrinfo(host, *_args, **_kwargs):
|
||||||
|
return [(socket.AF_INET, None, None, "", (ip, 0))]
|
||||||
|
|
||||||
|
monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
|
||||||
|
|
||||||
|
return _set
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebhookSecurity:
|
||||||
|
def test_blocks_invalid_scheme_or_hostname(self, httpx_mock: HTTPXMock):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Invalid URL schemes or hostnames
|
||||||
|
WHEN:
|
||||||
|
- send_webhook is called with such URLs
|
||||||
|
THEN:
|
||||||
|
- ValueError is raised
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
send_webhook(
|
||||||
|
"ftp://example.com",
|
||||||
|
data="",
|
||||||
|
headers={},
|
||||||
|
files=None,
|
||||||
|
as_json=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
send_webhook(
|
||||||
|
"http:///nohost",
|
||||||
|
data="",
|
||||||
|
headers={},
|
||||||
|
files=None,
|
||||||
|
as_json=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(WEBHOOKS_ALLOWED_PORTS=[80, 443])
|
||||||
|
def test_blocks_disallowed_port(self, httpx_mock: HTTPXMock):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- URL with a disallowed port
|
||||||
|
WHEN:
|
||||||
|
- send_webhook is called with such URL
|
||||||
|
THEN:
|
||||||
|
- ValueError is raised
|
||||||
|
"""
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
send_webhook(
|
||||||
|
"http://paperless-ngx.com:8080",
|
||||||
|
data="",
|
||||||
|
headers={},
|
||||||
|
files=None,
|
||||||
|
as_json=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert httpx_mock.get_request() is None
|
||||||
|
|
||||||
|
@override_settings(WEBHOOKS_ALLOW_INTERNAL_REQUESTS=False)
|
||||||
|
def test_blocks_private_loopback_linklocal(self, httpx_mock: HTTPXMock, resolve_to):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- URL with a private, loopback, or link-local IP address
|
||||||
|
- WEBHOOKS_ALLOW_INTERNAL_REQUESTS is False
|
||||||
|
WHEN:
|
||||||
|
- send_webhook is called with such URL
|
||||||
|
THEN:
|
||||||
|
- ValueError is raised
|
||||||
|
"""
|
||||||
|
resolve_to("127.0.0.1")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
send_webhook(
|
||||||
|
"http://paperless-ngx.com",
|
||||||
|
data="",
|
||||||
|
headers={},
|
||||||
|
files=None,
|
||||||
|
as_json=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_allows_public_ip_and_sends(self, httpx_mock: HTTPXMock, resolve_to):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- URL with a public IP address
|
||||||
|
WHEN:
|
||||||
|
- send_webhook is called with such URL
|
||||||
|
THEN:
|
||||||
|
- Request is sent successfully
|
||||||
|
"""
|
||||||
|
resolve_to("52.207.186.75")
|
||||||
|
httpx_mock.add_response(content=b"ok")
|
||||||
|
|
||||||
|
send_webhook(
|
||||||
|
url="http://paperless-ngx.com",
|
||||||
|
data="hi",
|
||||||
|
headers={},
|
||||||
|
files=None,
|
||||||
|
as_json=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
req = httpx_mock.get_request()
|
||||||
|
assert req.url.host == "paperless-ngx.com"
|
||||||
|
|
||||||
|
def test_follow_redirects_disabled(self, httpx_mock: HTTPXMock, resolve_to):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A URL that redirects
|
||||||
|
WHEN:
|
||||||
|
- send_webhook is called with follow_redirects=False
|
||||||
|
THEN:
|
||||||
|
- Request is made to the original URL and does not follow the redirect
|
||||||
|
"""
|
||||||
|
resolve_to("52.207.186.75")
|
||||||
|
# Return a redirect and ensure we don't follow it (only one request recorded)
|
||||||
|
httpx_mock.add_response(
|
||||||
|
status_code=302,
|
||||||
|
headers={"location": "http://internal-service.local"},
|
||||||
|
content=b"",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPError):
|
||||||
|
send_webhook(
|
||||||
|
"http://paperless-ngx.com",
|
||||||
|
data="",
|
||||||
|
headers={},
|
||||||
|
files=None,
|
||||||
|
as_json=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(httpx_mock.get_requests()) == 1
|
||||||
|
|
||||||
|
def test_strips_user_supplied_host_header(self, httpx_mock: HTTPXMock, resolve_to):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A URL with a user-supplied Host header
|
||||||
|
WHEN:
|
||||||
|
- send_webhook is called with a malicious Host header
|
||||||
|
THEN:
|
||||||
|
- The Host header is stripped and replaced with the resolved hostname
|
||||||
|
"""
|
||||||
|
resolve_to("52.207.186.75")
|
||||||
|
httpx_mock.add_response(content=b"ok")
|
||||||
|
|
||||||
|
send_webhook(
|
||||||
|
url="http://paperless-ngx.com",
|
||||||
|
data="ok",
|
||||||
|
headers={"Host": "evil.test"},
|
||||||
|
files=None,
|
||||||
|
as_json=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
req = httpx_mock.get_request()
|
||||||
|
assert req.headers["Host"] == "paperless-ngx.com"
|
||||||
|
assert "evil.test" not in req.headers.get("Host", "")
|
||||||
|
@@ -1321,6 +1321,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
"delete_pages": "checksum",
|
"delete_pages": "checksum",
|
||||||
"split": None,
|
"split": None,
|
||||||
"merge": None,
|
"merge": None,
|
||||||
|
"edit_pdf": "checksum",
|
||||||
"reprocess": "checksum",
|
"reprocess": "checksum",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1339,6 +1340,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
if method in [
|
if method in [
|
||||||
bulk_edit.split,
|
bulk_edit.split,
|
||||||
bulk_edit.merge,
|
bulk_edit.merge,
|
||||||
|
bulk_edit.edit_pdf,
|
||||||
]:
|
]:
|
||||||
parameters["user"] = user
|
parameters["user"] = user
|
||||||
|
|
||||||
@@ -1358,6 +1360,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
|
|
||||||
# check ownership for methods that change original document
|
# check ownership for methods that change original document
|
||||||
if (
|
if (
|
||||||
|
(
|
||||||
has_perms
|
has_perms
|
||||||
and method
|
and method
|
||||||
in [
|
in [
|
||||||
@@ -1365,20 +1368,28 @@ class BulkEditView(PassUserMixin):
|
|||||||
bulk_edit.delete,
|
bulk_edit.delete,
|
||||||
bulk_edit.rotate,
|
bulk_edit.rotate,
|
||||||
bulk_edit.delete_pages,
|
bulk_edit.delete_pages,
|
||||||
|
bulk_edit.edit_pdf,
|
||||||
]
|
]
|
||||||
) or (
|
)
|
||||||
|
or (
|
||||||
method in [bulk_edit.merge, bulk_edit.split]
|
method in [bulk_edit.merge, bulk_edit.split]
|
||||||
and parameters["delete_originals"]
|
and parameters["delete_originals"]
|
||||||
|
)
|
||||||
|
or (method == bulk_edit.edit_pdf and parameters["update_document"])
|
||||||
):
|
):
|
||||||
has_perms = user_is_owner_of_all_documents
|
has_perms = user_is_owner_of_all_documents
|
||||||
|
|
||||||
# check global add permissions for methods that create documents
|
# check global add permissions for methods that create documents
|
||||||
if (
|
if (
|
||||||
has_perms
|
has_perms
|
||||||
and method in [bulk_edit.split, bulk_edit.merge]
|
and (
|
||||||
and not user.has_perm(
|
method in [bulk_edit.split, bulk_edit.merge]
|
||||||
"documents.add_document",
|
or (
|
||||||
|
method == bulk_edit.edit_pdf
|
||||||
|
and not parameters["update_document"]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
and not user.has_perm("documents.add_document")
|
||||||
):
|
):
|
||||||
has_perms = False
|
has_perms = False
|
||||||
|
|
||||||
@@ -1416,7 +1427,6 @@ class BulkEditView(PassUserMixin):
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: parameter validation
|
|
||||||
result = method(documents, **parameters)
|
result = method(documents, **parameters)
|
||||||
|
|
||||||
if settings.AUDIT_LOG_ENABLED and modified_field:
|
if settings.AUDIT_LOG_ENABLED and modified_field:
|
||||||
|
@@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-08-02 12:55+0000\n"
|
"POT-Creation-Date: 2025-08-11 17:31+0000\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@@ -1185,12 +1185,12 @@ msgstr ""
|
|||||||
msgid "Invalid color."
|
msgid "Invalid color."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:1649
|
#: documents/serialisers.py:1700
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "File type %(type)s not supported"
|
msgid "File type %(type)s not supported"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:1743
|
#: documents/serialisers.py:1794
|
||||||
msgid "Invalid variable detected."
|
msgid "Invalid variable detected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@@ -1423,10 +1423,31 @@ OUTLOOK_OAUTH_ENABLED = bool(
|
|||||||
and OUTLOOK_OAUTH_CLIENT_SECRET,
|
and OUTLOOK_OAUTH_CLIENT_SECRET,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Webhooks
|
||||||
|
###############################################################################
|
||||||
|
WEBHOOKS_ALLOWED_SCHEMES = set(
|
||||||
|
s.lower()
|
||||||
|
for s in __get_list(
|
||||||
|
"PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES",
|
||||||
|
["http", "https"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WEBHOOKS_ALLOWED_PORTS = set(
|
||||||
|
int(p)
|
||||||
|
for p in __get_list(
|
||||||
|
"PAPERLESS_WEBHOOKS_ALLOWED_PORTS",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WEBHOOKS_ALLOW_INTERNAL_REQUESTS = __get_boolean(
|
||||||
|
"PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS",
|
||||||
|
"true",
|
||||||
|
)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Remote Parser #
|
# Remote Parser #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
REMOTE_OCR_ENGINE = os.getenv("PAPERLESS_REMOTE_OCR_ENGINE")
|
REMOTE_OCR_ENGINE = os.getenv("PAPERLESS_REMOTE_OCR_ENGINE")
|
||||||
REMOTE_OCR_API_KEY = os.getenv("PAPERLESS_REMOTE_OCR_API_KEY")
|
REMOTE_OCR_API_KEY = os.getenv("PAPERLESS_REMOTE_OCR_API_KEY")
|
||||||
REMOTE_OCR_ENDPOINT = os.getenv("PAPERLESS_REMOTE_OCR_ENDPOINT")
|
REMOTE_OCR_ENDPOINT = os.getenv("PAPERLESS_REMOTE_OCR_ENDPOINT")
|
||||||
|
Reference in New Issue
Block a user