Enhancement: delete pages PDF action (#6772)

This commit is contained in:
shamoon 2024-05-22 16:01:15 -07:00 committed by GitHub
parent 2116964f67
commit 3e62f13f96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 658 additions and 142 deletions

View File

@ -424,6 +424,10 @@ The following methods are supported:
- `rotate`
- Requires `parameters`:
- `"degrees": DEGREES`. Must be an integer i.e. 90, 180, 270
- `delete_pages`
- Requires `parameters`:
- `"pages": [..]` The list should be a list of integers e.g. `"[2,3,4]"`
- The delete_pages operation only accepts a single document.
### Objects

View File

@ -462,15 +462,16 @@ Paperless-ngx added the ability to create shareable links to files in version 2.
## PDF Actions
Paperless-ngx supports 3 basic editing operations for PDFs (these operations cannot be performed on non-PDF files):
Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):
- 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.
- Splitting documents: available from an individual document's details page
- Splitting documents: available from an individual document's details page.
- Deleting pages: available from an individual document's details page.
!!! important
Note that rotation alters the Paperless-ngx _original_ file, which would, for example, invalidate a digital signature.
Note that rotation and deleting pages alter the Paperless-ngx _original_ file, which would, for example, invalidate a digital signature.
## Document History

View File

@ -368,7 +368,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">91</context>
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="1241348629231510663" datatype="html">
@ -540,7 +540,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">333</context>
<context context-type="linenumber">337</context>
</context-group>
</trans-unit>
<trans-unit id="3768927257183755959" datatype="html">
@ -599,7 +599,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">325</context>
<context context-type="linenumber">329</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
@ -729,7 +729,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">342</context>
<context context-type="linenumber">346</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -1069,7 +1069,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">301</context>
<context context-type="linenumber">305</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -1567,7 +1567,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">113</context>
<context context-type="linenumber">117</context>
</context-group>
</trans-unit>
<trans-unit id="7877440816920439876" datatype="html">
@ -2249,7 +2249,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">809</context>
<context context-type="linenumber">818</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@ -2292,15 +2292,19 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">811</context>
<context context-type="linenumber">820</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">1104</context>
<context context-type="linenumber">1113</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">1142</context>
<context context-type="linenumber">1150</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">1191</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@ -2909,6 +2913,47 @@
<context context-type="linenumber">579</context>
</context-group>
</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">
<source>Documents:</source>
<context-group purpose="location">
@ -2941,33 +2986,7 @@
<source>Note that only PDFs will be rotated.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component.html</context>
<context context-type="linenumber">35</context>
</context-group>
</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/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/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 context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="6567555383934959967" datatype="html">
@ -5383,15 +5402,22 @@
<context context-type="linenumber">114</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">65</context>
</context-group>
</trans-unit>
<trans-unit id="7819314041543176992" datatype="html">
<source>Close</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">85</context>
<context context-type="linenumber">89</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">1160</context>
<context context-type="linenumber">1168</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context>
@ -5402,21 +5428,21 @@
<source>Previous</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">88</context>
<context context-type="linenumber">92</context>
</context-group>
</trans-unit>
<trans-unit id="5028777105388019087" datatype="html">
<source>Details</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">101</context>
<context context-type="linenumber">105</context>
</context-group>
</trans-unit>
<trans-unit id="5701618810648052610" datatype="html">
<source>Title</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">104</context>
<context context-type="linenumber">108</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -5439,21 +5465,21 @@
<source>Archive serial number</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">105</context>
<context context-type="linenumber">109</context>
</context-group>
</trans-unit>
<trans-unit id="5114742157723900905" datatype="html">
<source>Date created</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">106</context>
<context context-type="linenumber">110</context>
</context-group>
</trans-unit>
<trans-unit id="2691296884221415710" datatype="html">
<source>Correspondent</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">108</context>
<context context-type="linenumber">112</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -5480,7 +5506,7 @@
<source>Document type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">110</context>
<context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -5507,7 +5533,7 @@
<source>Storage path</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">112</context>
<context context-type="linenumber">116</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -5530,14 +5556,14 @@
<source>Content</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">197</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit id="218403386307979629" datatype="html">
<source>Metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">206</context>
<context context-type="linenumber">210</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
@ -5548,175 +5574,175 @@
<source>Date modified</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">213</context>
<context context-type="linenumber">217</context>
</context-group>
</trans-unit>
<trans-unit id="6392918669949841614" datatype="html">
<source>Date added</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">217</context>
<context context-type="linenumber">221</context>
</context-group>
</trans-unit>
<trans-unit id="146828917013192897" datatype="html">
<source>Media filename</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">221</context>
<context context-type="linenumber">225</context>
</context-group>
</trans-unit>
<trans-unit id="4500855521601039868" datatype="html">
<source>Original filename</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">225</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="7985558498848210210" datatype="html">
<source>Original MD5 checksum</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">229</context>
<context context-type="linenumber">233</context>
</context-group>
</trans-unit>
<trans-unit id="5888243105821763422" datatype="html">
<source>Original file size</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">237</context>
</context-group>
</trans-unit>
<trans-unit id="2696647325713149563" datatype="html">
<source>Original mime type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">237</context>
<context context-type="linenumber">241</context>
</context-group>
</trans-unit>
<trans-unit id="342875990758166588" datatype="html">
<source>Archive MD5 checksum</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">242</context>
<context context-type="linenumber">246</context>
</context-group>
</trans-unit>
<trans-unit id="6033581412811562084" datatype="html">
<source>Archive file size</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">248</context>
<context context-type="linenumber">252</context>
</context-group>
</trans-unit>
<trans-unit id="6992781481378431874" datatype="html">
<source>Original document metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">257</context>
<context context-type="linenumber">261</context>
</context-group>
</trans-unit>
<trans-unit id="2846565152091361585" datatype="html">
<source>Archived document metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">260</context>
<context context-type="linenumber">264</context>
</context-group>
</trans-unit>
<trans-unit id="1295614462098694869" datatype="html">
<source>Preview</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">267</context>
<context context-type="linenumber">271</context>
</context-group>
</trans-unit>
<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="&lt;span class=&quot;badge text-bg-secondary ms-1&quot;&gt;"/><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 context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">279,282</context>
<context context-type="linenumber">283,286</context>
</context-group>
</trans-unit>
<trans-unit id="186236568870281953" datatype="html">
<source>History</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">290</context>
<context context-type="linenumber">294</context>
</context-group>
</trans-unit>
<trans-unit id="5129524307369213584" datatype="html">
<source>Save &amp; next</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">327</context>
<context context-type="linenumber">331</context>
</context-group>
</trans-unit>
<trans-unit id="4910102545766233758" datatype="html">
<source>Save &amp; close</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">330</context>
<context context-type="linenumber">334</context>
</context-group>
</trans-unit>
<trans-unit id="8191371354890763172" datatype="html">
<source>Enter Password</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">381</context>
<context context-type="linenumber">385</context>
</context-group>
</trans-unit>
<trans-unit id="2218903673684131427" datatype="html">
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">330,332</context>
<context context-type="linenumber">339,341</context>
</context-group>
</trans-unit>
<trans-unit id="3200733026060976258" datatype="html">
<source>Document changes detected</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">353</context>
<context context-type="linenumber">362</context>
</context-group>
</trans-unit>
<trans-unit id="2887155916749964" datatype="html">
<source>The version of this document in your browser session appears older than the existing version.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">354</context>
<context context-type="linenumber">363</context>
</context-group>
</trans-unit>
<trans-unit id="237142428785956348" datatype="html">
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">355</context>
<context context-type="linenumber">364</context>
</context-group>
</trans-unit>
<trans-unit id="8720977247725652816" datatype="html">
<source>Ok</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">357</context>
<context context-type="linenumber">366</context>
</context-group>
</trans-unit>
<trans-unit id="6142395741265832184" datatype="html">
<source>Next document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">464</context>
<context context-type="linenumber">473</context>
</context-group>
</trans-unit>
<trans-unit id="651985345816518480" datatype="html">
<source>Previous document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">474</context>
<context context-type="linenumber">483</context>
</context-group>
</trans-unit>
<trans-unit id="2885986061416655600" datatype="html">
<source>Close document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">482</context>
<context context-type="linenumber">491</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/open-documents.service.ts</context>
@ -5727,50 +5753,50 @@
<source>Save document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">489</context>
<context context-type="linenumber">498</context>
</context-group>
</trans-unit>
<trans-unit id="5758784066858623886" datatype="html">
<source>Error retrieving metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">531</context>
<context context-type="linenumber">540</context>
</context-group>
</trans-unit>
<trans-unit id="3456881259945295697" datatype="html">
<source>Error retrieving suggestions.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">556</context>
<context context-type="linenumber">565</context>
</context-group>
</trans-unit>
<trans-unit id="8348337312757497317" datatype="html">
<source>Document saved successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">678</context>
<context context-type="linenumber">687</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">692</context>
<context context-type="linenumber">701</context>
</context-group>
</trans-unit>
<trans-unit id="448882439049417053" datatype="html">
<source>Error saving document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">696</context>
<context context-type="linenumber">705</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">737</context>
<context context-type="linenumber">746</context>
</context-group>
</trans-unit>
<trans-unit id="9021887951960049161" datatype="html">
<source>Confirm delete</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">764</context>
<context context-type="linenumber">773</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
@ -5785,35 +5811,35 @@
<source>Do you really want to delete document &quot;<x id="PH" equiv-text="this.document.title"/>&quot;?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">765</context>
<context context-type="linenumber">774</context>
</context-group>
</trans-unit>
<trans-unit id="6691075929777935948" datatype="html">
<source>The files for this document will be deleted permanently. This operation cannot be undone.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">766</context>
<context context-type="linenumber">775</context>
</context-group>
</trans-unit>
<trans-unit id="719892092227206532" datatype="html">
<source>Delete document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">768</context>
<context context-type="linenumber">777</context>
</context-group>
</trans-unit>
<trans-unit id="7295637485862454066" datatype="html">
<source>Error deleting document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">787</context>
<context context-type="linenumber">796</context>
</context-group>
</trans-unit>
<trans-unit id="7362691899087997122" datatype="html">
<source>Redo OCR confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">807</context>
<context context-type="linenumber">816</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@ -5824,63 +5850,63 @@
<source>This operation will permanently redo OCR for this document.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">808</context>
<context context-type="linenumber">817</context>
</context-group>
</trans-unit>
<trans-unit id="5729001209753056399" datatype="html">
<source>Redo OCR operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">819</context>
<context context-type="linenumber">828</context>
</context-group>
</trans-unit>
<trans-unit id="4409560272830824468" datatype="html">
<source>Error executing operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">830</context>
<context context-type="linenumber">839</context>
</context-group>
</trans-unit>
<trans-unit id="4458954481601077369" datatype="html">
<source>Page Fit</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">899</context>
<context context-type="linenumber">908</context>
</context-group>
</trans-unit>
<trans-unit id="1217563727923422413" datatype="html">
<source>Split confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1102</context>
<context context-type="linenumber">1111</context>
</context-group>
</trans-unit>
<trans-unit id="2805304563009985503" datatype="html">
<source>This operation will split the selected document(s) into new documents.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1103</context>
<context context-type="linenumber">1112</context>
</context-group>
</trans-unit>
<trans-unit id="4158171846914923744" datatype="html">
<source>Split operation 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">1118</context>
<context context-type="linenumber">1127</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">1127</context>
<context context-type="linenumber">1136</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">1139</context>
<context context-type="linenumber">1148</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@ -5891,32 +5917,49 @@
<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">1140</context>
</context-group>
</trans-unit>
<trans-unit id="4233432423256408453" datatype="html">
<source>This will alter the original copy.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1141</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">786</context>
<context context-type="linenumber">1149</context>
</context-group>
</trans-unit>
<trans-unit id="4069543875319587651" datatype="html">
<source>Rotation 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">1157</context>
<context context-type="linenumber">1165</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">1169</context>
<context context-type="linenumber">1177</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">1189</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">1190</context>
</context-group>
</trans-unit>
<trans-unit id="8617528702531167646" datatype="html">
<source>Delete pages operation 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">1205</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">1214</context>
</context-group>
</trans-unit>
<trans-unit id="4958946940233632319" datatype="html">
@ -6277,6 +6320,13 @@
<context context-type="linenumber">785</context>
</context-group>
</trans-unit>
<trans-unit id="4233432423256408453" datatype="html">
<source>This will alter the original copy.</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">786</context>
</context-group>
</trans-unit>
<trans-unit id="7910756456450124185" datatype="html">
<source>Merge confirm</source>
<context-group purpose="location">

View File

@ -124,6 +124,7 @@ import { DragDropSelectComponent } from './components/common/input/drag-drop-sel
import { CustomFieldDisplayComponent } from './components/common/custom-field-display/custom-field-display.component'
import { GlobalSearchComponent } from './components/app-frame/global-search/global-search.component'
import { HotkeyDialogComponent } from './components/common/hotkey-dialog/hotkey-dialog.component'
import { DeletePagesConfirmDialogComponent } from './components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
import {
airplane,
archive,
@ -160,6 +161,7 @@ import {
clipboardCheckFill,
clipboardFill,
dash,
dashCircle,
diagram3,
dice5,
doorOpen,
@ -174,6 +176,7 @@ import {
fileEarmarkCheck,
fileEarmarkFill,
fileEarmarkLock,
fileEarmarkMinus,
files,
fileText,
filter,
@ -259,6 +262,7 @@ const icons = {
clipboardCheckFill,
clipboardFill,
dash,
dashCircle,
diagram3,
dice5,
doorOpen,
@ -273,6 +277,7 @@ const icons = {
fileEarmarkCheck,
fileEarmarkFill,
fileEarmarkLock,
fileEarmarkMinus,
files,
fileText,
filter,
@ -491,6 +496,7 @@ function initializeApp(settings: SettingsService) {
CustomFieldDisplayComponent,
GlobalSearchComponent,
HotkeyDialogComponent,
DeletePagesConfirmDialogComponent,
],
imports: [
BrowserModule,

View File

@ -0,0 +1,54 @@
<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>

View File

@ -0,0 +1,28 @@
.pdf-viewer-container {
background-color: gray;
height: 350px;
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);
}
}

View File

@ -0,0 +1,55 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { PdfViewerComponent } from 'ng2-pdf-viewer'
describe('DeletePagesConfirmDialogComponent', () => {
let component: DeletePagesConfirmDialogComponent
let fixture: ComponentFixture<DeletePagesConfirmDialogComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DeletePagesConfirmDialogComponent, PdfViewerComponent],
providers: [NgbActiveModal, SafeHtmlPipe],
imports: [
HttpClientTestingModule,
NgxBootstrapIconsModule.pick(allIcons),
FormsModule,
ReactiveFormsModule,
],
}).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([])
})
})

View File

@ -0,0 +1,64 @@
import { Component, TemplateRef, ViewChild } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
import { PDFDocumentProxy, PdfViewerComponent } from 'ng2-pdf-viewer'
@Component({
selector: 'pngx-delete-pages-confirm-dialog',
templateUrl: './delete-pages-confirm-dialog.component.html',
styleUrl: './delete-pages-confirm-dialog.component.scss',
})
export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent {
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(
activeModal: NgbActiveModal,
private documentService: DocumentService
) {
super(activeModal)
}
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)
})
}
}

View File

@ -21,21 +21,19 @@
</button>
</div>
</div>
<div class="row mt-4">
<div class="col">
@if (messageBold) {
<p><b>{{messageBold}}</b></p>
}
@if (message) {
<p class="mb-0" [innerHTML]="message | safeHtml"></p>
}
</div>
</div>
@if (showPDFNote) {
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be rotated.</p>
}
</div>
<div class="modal-footer">
<div class="modal-footer flex-nowrap">
<div class="col">
@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>

View File

@ -1,5 +1,5 @@
<pngx-page-header [(title)]="title">
@if (contentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
@if (archiveContentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
@if (previewNumPages) {
<div class="input-group input-group-sm d-none d-md-flex">
<div class="input-group-text" i18n>Page</div>
@ -53,13 +53,17 @@
<i-bs width="1em" height="1em" name="diagram-3"></i-bs>&nbsp;<span i18n>More like this</span>
</button>
<button ngbDropdownItem (click)="splitDocument()" [disabled]="contentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
<button ngbDropdownItem (click)="splitDocument()" [disabled]="archiveContentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
<i-bs width="1em" height="1em" name="scissors"></i-bs>&nbsp;<span i18n>Split</span>
</button>
<button ngbDropdownItem (click)="rotateDocument()" [disabled]="!userIsOwner || metadata?.original_mime_type !== 'application/pdf'">
<button ngbDropdownItem (click)="rotateDocument()" [disabled]="!userIsOwner || originalContentRenderType !== ContentRenderType.PDF">
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button>
<button ngbDropdownItem (click)="deletePages()" [disabled]="!userIsOwner || originalContentRenderType !== ContentRenderType.PDF || previewNumPages === 1">
<i-bs name="file-earmark-minus"></i-bs>&nbsp;<ng-container i18n>Delete page(s)</ng-container>
</button>
</div>
</div>
@ -343,7 +347,7 @@
</div>
</div>
} @else {
@switch (contentRenderType) {
@switch (archiveContentRenderType) {
@case (ContentRenderType.PDF) {
@if (!useNativePdfViewer) {
<div class="preview-sticky pdf-viewer-container">

View File

@ -81,6 +81,7 @@ import { environment } from 'src/environments/environment'
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 { PdfViewerModule } from 'ng2-pdf-viewer'
import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
const doc: Document = {
id: 3,
@ -178,6 +179,7 @@ describe('DocumentDetailComponent', () => {
CustomFieldsDropdownComponent,
SplitConfirmDialogComponent,
RotateConfirmDialogComponent,
DeletePagesConfirmDialogComponent,
],
providers: [
DocumentTitlePipe,
@ -1035,7 +1037,9 @@ describe('DocumentDetailComponent', () => {
component.metadata = { has_archive_version: true }
initNormally()
fixture.detectChanges()
expect(component.contentRenderType).toEqual(component.ContentRenderType.PDF)
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.PDF
)
expect(
fixture.debugElement.query(By.css('pdf-viewer-container'))
).not.toBeUndefined()
@ -1045,7 +1049,7 @@ describe('DocumentDetailComponent', () => {
original_mime_type: 'text/plain',
}
fixture.detectChanges()
expect(component.contentRenderType).toEqual(
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.Text
)
expect(
@ -1057,7 +1061,7 @@ describe('DocumentDetailComponent', () => {
original_mime_type: 'image/jpg',
}
fixture.detectChanges()
expect(component.contentRenderType).toEqual(
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.Image
)
expect(
@ -1070,7 +1074,7 @@ describe('DocumentDetailComponent', () => {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}
fixture.detectChanges()
expect(component.contentRenderType).toEqual(
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.Other
)
expect(
@ -1130,6 +1134,31 @@ describe('DocumentDetailComponent', () => {
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()
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/bulk_edit/`
)
req.flush(true)
})
it('should support keyboard shortcuts', () => {
initNormally()

View File

@ -68,6 +68,7 @@ import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
import { HotKeyService } from 'src/app/services/hot-key.service'
import { PDFDocumentProxy } from 'ng2-pdf-viewer'
@ -216,19 +217,27 @@ export class DocumentDetailComponent
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
}
get contentRenderType(): ContentRenderType {
if (!this.metadata) return ContentRenderType.Unknown
const contentType = this.metadata?.has_archive_version
? 'application/pdf'
: this.metadata?.original_mime_type
get archiveContentRenderType(): ContentRenderType {
return this.getRenderType(
this.metadata?.has_archive_version
? 'application/pdf'
: this.metadata?.original_mime_type
)
}
if (contentType === 'application/pdf') {
get originalContentRenderType(): ContentRenderType {
return this.getRenderType(this.metadata?.original_mime_type)
}
private getRenderType(mimeType: string): ContentRenderType {
if (!mimeType) return ContentRenderType.Unknown
if (mimeType === 'application/pdf') {
return ContentRenderType.PDF
} else if (
['text/plain', 'application/csv', 'text/csv'].includes(contentType)
['text/plain', 'application/csv', 'text/csv'].includes(mimeType)
) {
return ContentRenderType.Text
} else if (contentType?.indexOf('image/') === 0) {
} else if (mimeType?.indexOf('image/') === 0) {
return ContentRenderType.Image
}
return ContentRenderType.Other
@ -1138,7 +1147,6 @@ export class DocumentDetailComponent
})
modal.componentInstance.title = $localize`Rotate confirm`
modal.componentInstance.messageBold = $localize`This operation will permanently rotate the original version of the current document.`
modal.componentInstance.message = $localize`This will alter the original copy.`
modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.documentID = this.document.id
modal.componentInstance.showPDFNote = false
@ -1173,4 +1181,41 @@ export class DocumentDetailComponent
})
})
}
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 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
)
},
})
})
}
}

View File

@ -325,3 +325,29 @@ def split(doc_ids: list[int], pages: list[list[int]]):
logger.exception(f"Error splitting document {doc.id}: {e}")
return "OK"
def delete_pages(doc_ids: list[int], pages: list[int]):
logger.info(
f"Attempting to delete pages {pages} from {len(doc_ids)} documents",
)
doc = Document.objects.get(id=doc_ids[0])
pages = sorted(pages) # sort pages to avoid index issues
import pikepdf
try:
with pikepdf.open(doc.source_path, allow_overwriting_input=True) as pdf:
offset = 1 # pages are 1-indexed
for page_num in pages:
pdf.pages.remove(pdf.pages[page_num - offset])
offset += 1 # remove() changes the index of the pages
pdf.remove_unreferenced_resources()
pdf.save()
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
doc.save()
update_document_archive_file.delay(document_id=doc.id)
logger.info(f"Deleted pages {pages} from document {doc.id}")
except Exception as e:
logger.exception(f"Error deleting pages from document {doc.id}: {e}")
return "OK"

View File

@ -944,6 +944,7 @@ class BulkEditSerializer(
"rotate",
"merge",
"split",
"delete_pages",
],
label="Method",
write_only=True,
@ -1000,6 +1001,8 @@ class BulkEditSerializer(
return bulk_edit.merge
elif method == "split":
return bulk_edit.split
elif method == "delete_pages":
return bulk_edit.delete_pages
else:
raise serializers.ValidationError("Unsupported method.")
@ -1128,6 +1131,14 @@ class BulkEditSerializer(
except ValueError:
raise serializers.ValidationError("invalid pages specified")
def _validate_parameters_delete_pages(self, parameters):
if "pages" not in parameters:
raise serializers.ValidationError("pages not specified")
if not isinstance(parameters["pages"], list):
raise serializers.ValidationError("pages must be a list")
if not all(isinstance(i, int) for i in parameters["pages"]):
raise serializers.ValidationError("pages must be a list of integers")
def validate(self, attrs):
method = attrs["method"]
parameters = attrs["parameters"]
@ -1154,6 +1165,12 @@ class BulkEditSerializer(
"Split method only supports one document",
)
self._validate_parameters_split(parameters)
elif method == bulk_edit.delete_pages:
if len(attrs["documents"]) > 1:
raise serializers.ValidationError(
"Delete pages method only supports one document",
)
self._validate_parameters_delete_pages(parameters)
return attrs

View File

@ -1065,3 +1065,95 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"Split method only supports one document", response.content)
@mock.patch("documents.serialisers.bulk_edit.delete_pages")
def test_delete_pages(self, m):
m.return_value = "OK"
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "delete_pages",
"parameters": {"pages": [1, 2, 3, 4]},
},
),
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["pages"], [1, 2, 3, 4])
def test_delete_pages_invalid_params(self):
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [
self.doc1.id,
self.doc2.id,
], # only one document supported
"method": "delete_pages",
"parameters": {
"pages": [1, 2, 3, 4],
},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(
b"Delete pages method only supports one document",
response.content,
)
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "delete_pages",
"parameters": {}, # pages not specified
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"pages not specified", response.content)
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "delete_pages",
"parameters": {"pages": "1-3"}, # not a list
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"pages must be a list", response.content)
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "delete_pages",
"parameters": {"pages": ["1-3"]}, # not ints
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"pages must be a list of integers", response.content)

View File

@ -585,3 +585,46 @@ class TestPDFActions(DirectoriesMixin, TestCase):
mock_update_documents.assert_called_once()
mock_chord.assert_called_once()
self.assertEqual(result, "OK")
@mock.patch("documents.tasks.update_document_archive_file.delay")
@mock.patch("pikepdf.Pdf.save")
def test_delete_pages(self, mock_pdf_save, mock_update_archive_file):
"""
GIVEN:
- Existing documents
WHEN:
- Delete pages action is called with 1 document and 2 pages
THEN:
- Save should be called once
- Archive file should be updated once
"""
doc_ids = [self.doc2.id]
pages = [1, 3]
result = bulk_edit.delete_pages(doc_ids, pages)
mock_pdf_save.assert_called_once()
mock_update_archive_file.assert_called_once()
self.assertEqual(result, "OK")
@mock.patch("documents.tasks.update_document_archive_file.delay")
@mock.patch("pikepdf.Pdf.save")
def test_delete_pages_with_error(self, mock_pdf_save, mock_update_archive_file):
"""
GIVEN:
- Existing documents
WHEN:
- Delete pages action is called with 1 document and 2 pages
- PikePDF raises an error
THEN:
- Save should be called once
- Archive file should not be updated
"""
mock_pdf_save.side_effect = Exception("Error saving PDF")
doc_ids = [self.doc2.id]
pages = [1, 3]
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
bulk_edit.delete_pages(doc_ids, pages)
error_str = cm.output[0]
expected_str = "Error deleting pages from document"
self.assertIn(expected_str, error_str)
mock_update_archive_file.assert_not_called()