Feature: Share links (#3996)

* Implement share links

Basic implementation of share links

Make certain share link fields not editable, automatically grant permissions on migrate

Updated styling, error messages from expired / deleted links

frontend code linting, reversable sharelink migration

testing coverage

Update translation strings

No links message

* Consolidate file response methods

* improvements to share links on mobile devices

* Refactor share links file_version

* Add docs for share links

* Apply suggestions from code review

* When filtering share links, use the timezone aware now()

* Removes extra call to setup directories for usage in testing

* FIx copied badge display on some browsers

* Move copy to ngx-clipboard library

---------

Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
This commit is contained in:
shamoon
2023-09-14 13:32:43 -07:00
committed by GitHub
parent d04c533fc0
commit ef9d01fefe
35 changed files with 1740 additions and 454 deletions

View File

@@ -319,7 +319,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">55</context>
<context context-type="linenumber">65</context>
</context-group>
</trans-unit>
<trans-unit id="1241348629231510663" datatype="html">
@@ -1073,7 +1073,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">198</context>
<context context-type="linenumber">208</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>
@@ -1142,7 +1142,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">182</context>
<context context-type="linenumber">192</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -1549,6 +1549,10 @@
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
<context context-type="linenumber">9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">33</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">11</context>
@@ -2240,6 +2244,135 @@
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="686374493515618129" datatype="html">
<source>Share Links</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">6</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">25</context>
</context-group>
</trans-unit>
<trans-unit id="6617773613987957957" datatype="html">
<source> No existing links </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">10,12</context>
</context-group>
</trans-unit>
<trans-unit id="4323470180912194028" datatype="html">
<source>Copy</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="7419704019640008953" datatype="html">
<source>Share</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">28</context>
</context-group>
</trans-unit>
<trans-unit id="5392341774767336507" datatype="html">
<source>Copied!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
</trans-unit>
<trans-unit id="6811921365829755679" datatype="html">
<source>Share archive version</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">
<source>Create</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<context context-type="linenumber">55</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
</trans-unit>
<trans-unit id="4776429682428363094" datatype="html">
<source>1 day</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">18</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit id="8542568275115626925" datatype="html">
<source>7 days</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="7152095234138763013" datatype="html">
<source>30 days</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="8372007266188249803" datatype="html">
<source>Never</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">21</context>
</context-group>
</trans-unit>
<trans-unit id="3429210839568770054" datatype="html">
<source>Error retrieving links</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">69</context>
</context-group>
</trans-unit>
<trans-unit id="3242255798983858463" datatype="html">
<source><x id="PH" equiv-text="days"/> days</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit id="2897042887615940599" datatype="html">
<source>Error deleting link</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="8400747326190565173" datatype="html">
<source>Error creating link</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts</context>
<context context-type="linenumber">140</context>
</context-group>
</trans-unit>
<trans-unit id="5611592591303869712" datatype="html">
<source>Status</source>
<context-group purpose="location">
@@ -2310,7 +2443,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">75</context>
<context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@@ -2340,7 +2473,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">19</context>
<context context-type="linenumber">18</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -2538,14 +2671,65 @@
<source>Download original</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">25</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="3193976279273491157" datatype="html">
<source>Actions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">34</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">86</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">221</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">259</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">296</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">347</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">382</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">44</context>
</context-group>
</trans-unit>
<trans-unit id="8312409092917397847" datatype="html">
<source>Redo OCR</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">40</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -2556,7 +2740,7 @@
<source>More like this</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">40</context>
<context context-type="linenumber">46</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
@@ -2567,7 +2751,7 @@
<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">43</context>
<context context-type="linenumber">53</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context>
@@ -2578,35 +2762,35 @@
<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">50</context>
<context context-type="linenumber">60</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">72</context>
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="1379170675585571971" datatype="html">
<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">76</context>
<context context-type="linenumber">86</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">77</context>
<context context-type="linenumber">87</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">79</context>
<context context-type="linenumber">89</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -2629,7 +2813,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">81</context>
<context context-type="linenumber">91</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -2652,7 +2836,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">83</context>
<context context-type="linenumber">93</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -2671,21 +2855,21 @@
<source>Default</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">84</context>
<context context-type="linenumber">94</context>
</context-group>
</trans-unit>
<trans-unit id="6205355627445317276" datatype="html">
<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">91</context>
<context context-type="linenumber">101</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">100</context>
<context context-type="linenumber">110</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
@@ -2696,173 +2880,173 @@
<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">106</context>
<context context-type="linenumber">116</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">110</context>
<context context-type="linenumber">120</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">114</context>
<context context-type="linenumber">124</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">118</context>
<context context-type="linenumber">128</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">122</context>
<context context-type="linenumber">132</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">126</context>
<context context-type="linenumber">136</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">130</context>
<context context-type="linenumber">140</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">134</context>
<context context-type="linenumber">144</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">138</context>
<context context-type="linenumber">148</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">144</context>
<context context-type="linenumber">154</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">145</context>
<context context-type="linenumber">155</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">151</context>
<context context-type="linenumber">161</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">167</context>
<context context-type="linenumber">177</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">218</context>
<context context-type="linenumber">228</context>
</context-group>
</trans-unit>
<trans-unit id="8460995830263484763" datatype="html">
<source>Notes <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span *ngIf=&quot;document?.notes.length&quot; class=&quot;badge text-bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/a&gt;"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">175,176</context>
<context context-type="linenumber">185,186</context>
</context-group>
</trans-unit>
<trans-unit id="3823219296477075982" datatype="html">
<source>Discard</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">194</context>
<context context-type="linenumber">204</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">196</context>
<context context-type="linenumber">206</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">197</context>
<context context-type="linenumber">207</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">252,254</context>
<context context-type="linenumber">253,255</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">397</context>
<context context-type="linenumber">398</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">418</context>
<context context-type="linenumber">419</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">531</context>
<context context-type="linenumber">532</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">539</context>
<context context-type="linenumber">540</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">543</context>
<context context-type="linenumber">544</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">584</context>
<context context-type="linenumber">585</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">610</context>
<context context-type="linenumber">611</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
@@ -2873,35 +3057,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">611</context>
<context context-type="linenumber">612</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">612</context>
<context context-type="linenumber">613</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">614</context>
<context context-type="linenumber">615</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">633</context>
<context context-type="linenumber">634</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">653</context>
<context context-type="linenumber">654</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -2912,14 +3096,14 @@
<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">654</context>
<context context-type="linenumber">655</context>
</context-group>
</trans-unit>
<trans-unit id="5641451190833696892" datatype="html">
<source>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">655</context>
<context context-type="linenumber">656</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -2950,7 +3134,7 @@
<source>Proceed</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">657</context>
<context context-type="linenumber">658</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -2977,14 +3161,14 @@
<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">665</context>
<context context-type="linenumber">666</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">676</context>
<context context-type="linenumber">677</context>
</context-group>
</trans-unit>
<trans-unit id="6857598786757174736" datatype="html">
@@ -3045,53 +3229,6 @@
<context context-type="linenumber">52</context>
</context-group>
</trans-unit>
<trans-unit id="3193976279273491157" datatype="html">
<source>Actions</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">86</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">221</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">259</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">296</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">347</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">382</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
<context context-type="linenumber">44</context>
</context-group>
</trans-unit>
<trans-unit id="1015374532025907183" datatype="html">
<source>Include:</source>
<context-group purpose="location">
@@ -3945,25 +4082,6 @@
<context context-type="linenumber">44</context>
</context-group>
</trans-unit>
<trans-unit id="5674286808255988565" datatype="html">
<source>Create</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
<context context-type="linenumber">2</context>
</context-group>
</trans-unit>
<trans-unit id="4010735610815226758" datatype="html">
<source>Filter by:</source>
<context-group purpose="location">

View File

@@ -24,6 +24,7 @@
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
"ng2-pdf-viewer": "^10.0.0",
"ngx-clipboard": "^16.0.0",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1",
"ngx-file-drop": "^16.0.0",
@@ -14404,6 +14405,19 @@
"pdfjs-dist": "~2.16.105"
}
},
"node_modules/ngx-clipboard": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/ngx-clipboard/-/ngx-clipboard-16.0.0.tgz",
"integrity": "sha512-rZ/Eo1PqiKMiyF8tdjhmUkoUu68f7OzBJ7YH1YFeh2RAaNrerTaW8XfFOzppSckjFQqA1fwGSYuTTJlDhDag5w==",
"dependencies": {
"ngx-window-token": ">=7.0.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": ">=13.0.0",
"@angular/core": ">=13.0.0"
}
},
"node_modules/ngx-color": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz",
@@ -14474,6 +14488,21 @@
"@ng-bootstrap/ng-bootstrap": "^15.0.0"
}
},
"node_modules/ngx-window-token": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/ngx-window-token/-/ngx-window-token-7.0.0.tgz",
"integrity": "sha512-5+XfRVSY7Dciu8xyCNMkOlH2UfwR9W2P1Pirz7caaZgOZDjFbL8aEO2stjfJJm2FFf1D6dlVHNzhLWGk9HGkqA==",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/common": ">=13.0.0",
"@angular/core": ">=13.0.0"
}
},
"node_modules/nice-napi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",

View File

@@ -26,6 +26,7 @@
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
"ng2-pdf-viewer": "^10.0.0",
"ngx-clipboard": "^16.0.0",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1",
"ngx-file-drop": "^16.0.0",

View File

@@ -86,6 +86,7 @@ Object.defineProperty(navigator, 'clipboard', {
writeText: async () => {},
},
})
Object.defineProperty(navigator, 'canShare', { value: () => true })
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
HTMLCanvasElement.prototype.getContext = <

View File

@@ -94,6 +94,7 @@ import { PermissionsFilterDropdownComponent } from './components/common/permissi
import { UsernamePipe } from './pipes/username.pipe'
import { LogoComponent } from './components/common/logo/logo.component'
import { IsNumberPipe } from './pipes/is-number.pipe'
import { ShareLinksDropdownComponent } from './components/common/share-links-dropdown/share-links-dropdown.component'
import localeAf from '@angular/common/locales/af'
import localeAr from '@angular/common/locales/ar'
@@ -231,6 +232,7 @@ function initializeApp(settings: SettingsService) {
UsernamePipe,
LogoComponent,
IsNumberPipe,
ShareLinksDropdownComponent,
],
imports: [
BrowserModule,

View File

@@ -1,7 +1,6 @@
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
tick,
} from '@angular/core/testing'

View File

@@ -0,0 +1,61 @@
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary me-2" id="shareLinksDropdown" [disabled]="disabled" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#link" />
</svg>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Share Links</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="shareLinksDropdown" class="shadow share-links-dropdown">
<ul class="list-group list-group-flush">
<li *ngIf="!shareLinks || shareLinks.length === 0" class="list-group-item fst-italic small text-center text-secondary" i18n>
No existing links
</li>
<li class="list-group-item" *ngFor="let link of shareLinks">
<div class="input-group input-group-sm w-100">
<input type="text" class="form-control" aria-label="Share link" [value]="getShareUrl(link)" readonly>
<span *ngIf="link.expiration" class="input-group-text">
{{ getDaysRemaining(link) }}
</span>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="copy(link)">
<svg class="buttonicon" fill="currentColor">
<use *ngIf="copied !== link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-fill" />
<use *ngIf="copied === link.id" xlink:href="assets/bootstrap-icons.svg#clipboard-check-fill" />
</svg><span class="visually-hidden" i18n>Copy</span>
</button>
<button *ngIf="canShare(link)" type="button" class="btn btn-sm btn-outline-primary" (click)="share(link)">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up" />
</svg><span class="visually-hidden" i18n>Share</span>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete(link)">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg><span class="visually-hidden" i18n>Delete</span>
</button>
</div>
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied === link.id" i18n>Copied!</span>
</li>
<li class="list-group-item pt-3 pb-2">
<div class="input-group input-group-sm w-100">
<div class="form-check form-switch ms-auto">
<input class="form-check-input" type="checkbox" role="switch" id="versionSwitch" [(ngModel)]="archiveVersion">
<label class="form-check-label small" for="versionSwitch" i18n>Share archive version</label>
</div>
</div>
<div class="input-group input-group-sm w-100 mt-2">
<label class="input-group-text" for="addLink">Expires:</label>
<select class="form-select form-select-sm" [(ngModel)]="expirationDays">
<option *ngFor="let option of EXPIRATION_OPTIONS" [ngValue]="option.value">{{ option.label }}</option>
</select>
<button class="btn btn-sm btn-outline-primary ms-auto" type="button" (click)="createLink()" [disabled]="loading">
<div *ngIf="loading" class="spinner-border spinner-border-sm me-2" role="status"></div>
<svg *ngIf="!loading" class="buttonicon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
<ng-container i18n>Create</ng-container>
</button>
</div>
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,14 @@
.share-links-dropdown {
min-width: 350px;
// correct position on mobile
@media (max-width: 575.98px) {
&.show {
margin-left: -175px !important;
}
}
}
.copied-badge {
right: 7.5em;
}

View File

@@ -0,0 +1,195 @@
import {
HttpTestingController,
HttpClientTestingModule,
} from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { of, throwError } from 'rxjs'
import {
PaperlessFileVersion,
PaperlessShareLink,
} from 'src/app/data/paperless-share-link'
import { ShareLinkService } from 'src/app/services/rest/share-link.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ShareLinksDropdownComponent } from './share-links-dropdown.component'
import { ClipboardService } from 'ngx-clipboard'
describe('ShareLinksDropdownComponent', () => {
let component: ShareLinksDropdownComponent
let fixture: ComponentFixture<ShareLinksDropdownComponent>
let shareLinkService: ShareLinkService
let toastService: ToastService
let httpController: HttpTestingController
let clipboardService: ClipboardService
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ShareLinksDropdownComponent],
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
})
fixture = TestBed.createComponent(ShareLinksDropdownComponent)
shareLinkService = TestBed.inject(ShareLinkService)
toastService = TestBed.inject(ToastService)
httpController = TestBed.inject(HttpTestingController)
clipboardService = TestBed.inject(ClipboardService)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support refresh to retrieve links', () => {
const getSpy = jest.spyOn(shareLinkService, 'getLinksForDocument')
component.documentId = 99
const now = new Date()
const expiration7days = new Date()
expiration7days.setDate(now.getDate() + 7)
getSpy.mockReturnValue(
of([
{
id: 1,
slug: '1234slug',
created: now.toISOString(),
document: 99,
file_version: PaperlessFileVersion.Archive,
expiration: expiration7days.toISOString(),
},
{
id: 1,
slug: '1234slug',
created: now.toISOString(),
document: 99,
file_version: PaperlessFileVersion.Original,
expiration: null,
},
])
)
component.refresh()
expect(getSpy).toHaveBeenCalled()
fixture.detectChanges()
expect(component.shareLinks).toHaveLength(2)
})
it('should show error on refresh if needed', () => {
const toastSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(shareLinkService, 'getLinksForDocument')
.mockReturnValueOnce(throwError(() => new Error('Unable to get links')))
component.documentId = 99
component.refresh()
fixture.detectChanges()
expect(toastSpy).toHaveBeenCalled()
})
it('should support link creation then refresh & copy url', fakeAsync(() => {
const createSpy = jest.spyOn(shareLinkService, 'createLinkForDocument')
component.documentId = 99
component.expirationDays = 7
component.archiveVersion = false
const expiration = new Date()
expiration.setDate(expiration.getDate() + 7)
const copySpy = jest.spyOn(clipboardService, 'copy')
const refreshSpy = jest.spyOn(component, 'refresh')
component.createLink()
expect(createSpy).toHaveBeenCalledWith(99, 'original', expiration)
httpController.expectOne(`${environment.apiBaseUrl}share_links/`).flush({
id: 1,
slug: '1234slug',
document: 99,
expiration: expiration.toISOString(),
})
fixture.detectChanges()
tick(3000)
expect(copySpy).toHaveBeenCalled()
expect(refreshSpy).toHaveBeenCalled()
}))
it('should show error on link creation if needed', () => {
component.documentId = 99
component.expirationDays = 7
const expiration = new Date()
expiration.setDate(expiration.getDate() + 7)
const toastSpy = jest.spyOn(toastService, 'showError')
component.createLink()
httpController
.expectOne(`${environment.apiBaseUrl}share_links/`)
.flush(
{ error: 'Share link error' },
{ status: 500, statusText: 'error' }
)
fixture.detectChanges()
expect(toastSpy).toHaveBeenCalled()
})
it('should support delete links & refresh', () => {
const deleteSpy = jest.spyOn(shareLinkService, 'delete')
deleteSpy.mockReturnValue(of(true))
const refreshSpy = jest.spyOn(component, 'refresh')
component.delete({ id: 12 } as PaperlessShareLink)
fixture.detectChanges()
expect(deleteSpy).toHaveBeenCalledWith({ id: 12 })
expect(refreshSpy).toHaveBeenCalled()
})
it('should show error on delete if needed', () => {
const toastSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(shareLinkService, 'delete')
.mockReturnValueOnce(throwError(() => new Error('Unable to delete link')))
component.delete(null)
fixture.detectChanges()
expect(toastSpy).toHaveBeenCalled()
})
it('should format days remaining', () => {
const now = new Date()
const expiration7days = new Date()
expiration7days.setDate(now.getDate() + 7)
const expiration1day = new Date()
expiration1day.setDate(now.getDate() + 1)
expect(
component.getDaysRemaining({
expiration: expiration7days.toISOString(),
} as PaperlessShareLink)
).toEqual('7 days')
expect(
component.getDaysRemaining({
expiration: expiration1day.toISOString(),
} as PaperlessShareLink)
).toEqual('1 day')
})
// coverage
it('should support share', () => {
const link = { slug: '12345slug' } as PaperlessShareLink
if (!('share' in navigator))
Object.defineProperty(navigator, 'share', { value: (obj: any) => {} })
// const navigatorSpy = jest.spyOn(navigator, 'share')
component.share(link)
// expect(navigatorSpy).toHaveBeenCalledWith({ url: component.getShareUrl(link) })
})
})

View File

@@ -0,0 +1,149 @@
import { Component, Input, OnInit } from '@angular/core'
import { first } from 'rxjs'
import {
PaperlessShareLink,
PaperlessFileVersion,
} from 'src/app/data/paperless-share-link'
import { ShareLinkService } from 'src/app/services/rest/share-link.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ClipboardService } from 'ngx-clipboard'
@Component({
selector: 'app-share-links-dropdown',
templateUrl: './share-links-dropdown.component.html',
styleUrls: ['./share-links-dropdown.component.scss'],
})
export class ShareLinksDropdownComponent implements OnInit {
EXPIRATION_OPTIONS = [
{ label: $localize`1 day`, value: 1 },
{ label: $localize`7 days`, value: 7 },
{ label: $localize`30 days`, value: 30 },
{ label: $localize`Never`, value: null },
]
@Input()
title = $localize`Share Links`
_documentId: number
@Input()
set documentId(id: number) {
if (id !== undefined) {
this._documentId = id
this.refresh()
}
}
@Input()
disabled: boolean = false
shareLinks: PaperlessShareLink[]
loading: boolean = false
copied: number
expirationDays: number = 7
archiveVersion: boolean = true
constructor(
private shareLinkService: ShareLinkService,
private toastService: ToastService,
private clipboardService: ClipboardService
) {}
ngOnInit(): void {
if (this._documentId !== undefined) this.refresh()
}
refresh() {
if (this._documentId === undefined) return
this.loading = true
this.shareLinkService
.getLinksForDocument(this._documentId)
.pipe(first())
.subscribe({
next: (results) => {
this.loading = false
this.shareLinks = results
},
error: (e) => {
this.toastService.showError(
$localize`Error retrieving links`,
10000,
e
)
},
})
}
getShareUrl(link: PaperlessShareLink): string {
return `${environment.apiBaseUrl.replace('api', 'share')}${link.slug}`
}
getDaysRemaining(link: PaperlessShareLink): string {
const days: number = Math.ceil(
(Date.parse(link.expiration) - Date.now()) / (1000 * 60 * 60 * 24)
)
return days === 1 ? $localize`1 day` : $localize`${days} days`
}
copy(link: PaperlessShareLink) {
this.clipboardService.copy(this.getShareUrl(link))
this.copied = link.id
setTimeout(() => {
this.copied = null
}, 3000)
}
canShare(link: PaperlessShareLink): boolean {
return (
navigator?.canShare && navigator.canShare({ url: this.getShareUrl(link) })
)
}
share(link: PaperlessShareLink) {
navigator.share({ url: this.getShareUrl(link) })
}
delete(link: PaperlessShareLink) {
this.shareLinkService.delete(link).subscribe({
next: () => {
this.refresh()
},
error: (e) => {
this.toastService.showError($localize`Error deleting link`, 10000, e)
},
})
}
createLink() {
let expiration
if (this.expirationDays) {
expiration = new Date()
expiration.setDate(expiration.getDate() + this.expirationDays)
}
this.loading = true
this.shareLinkService
.createLinkForDocument(
this._documentId,
this.archiveVersion
? PaperlessFileVersion.Archive
: PaperlessFileVersion.Original,
expiration
)
.subscribe({
next: (result) => {
this.loading = false
this.copy(result)
this.refresh()
},
error: (e) => {
this.loading = false
this.toastService.showError($localize`Error creating link`, 10000, e)
},
})
}
}

View File

@@ -10,6 +10,7 @@ import { ComponentFixture } from '@angular/core/testing'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { of } from 'rxjs'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { ClipboardService } from 'ngx-clipboard'
const toasts = [
{
@@ -41,6 +42,7 @@ describe('ToastsComponent', () => {
let component: ToastsComponent
let fixture: ComponentFixture<ToastsComponent>
let toastService: ToastService
let clipboardService: ClipboardService
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -57,9 +59,10 @@ describe('ToastsComponent', () => {
}).compileComponents()
fixture = TestBed.createComponent(ToastsComponent)
component = fixture.componentInstance
toastService = TestBed.inject(ToastService)
clipboardService = TestBed.inject(ClipboardService)
component = fixture.componentInstance
fixture.detectChanges()
})
@@ -114,7 +117,7 @@ describe('ToastsComponent', () => {
'Error 2 message details'
)
const copySpy = jest.spyOn(navigator.clipboard, 'writeText')
const copySpy = jest.spyOn(clipboardService, 'copy')
component.copyError(toasts[2].error)
expect(copySpy).toHaveBeenCalled()

View File

@@ -1,6 +1,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { Subscription } from 'rxjs'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { ClipboardService } from 'ngx-clipboard'
@Component({
selector: 'app-toasts',
@@ -8,7 +9,10 @@ import { Toast, ToastService } from 'src/app/services/toast.service'
styleUrls: ['./toasts.component.scss'],
})
export class ToastsComponent implements OnInit, OnDestroy {
constructor(private toastService: ToastService) {}
constructor(
private toastService: ToastService,
private clipboardService: ClipboardService
) {}
private subscription: Subscription
@@ -45,7 +49,7 @@ export class ToastsComponent implements OnInit, OnDestroy {
}
public copyError(error: any) {
navigator.clipboard.writeText(JSON.stringify(error))
this.clipboardService.copy(JSON.stringify(error))
this.copied = true
setTimeout(() => {
this.copied = false

View File

@@ -5,16 +5,15 @@
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger me-2 ms-auto" (click)="delete()" [disabled]="!userIsOwner" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
<button type="button" class="btn btn-sm btn-outline-danger me-4" (click)="delete()" [disabled]="!userIsOwner" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
</button>
<div class="btn-group me-2">
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
<svg class="buttonicon" fill="currentColor">
<svg class="buttonicon me-md-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download" />
</svg><span class="d-none d-lg-inline ps-1" i18n>Download</span>
</a>
@@ -25,20 +24,31 @@
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="redoOcr()" [disabled]="!userCanEdit">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
</svg><span class="d-none d-lg-inline ps-1" i18n>Redo OCR</span>
</button>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary me-2" id="actionsDropdown" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
</svg>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="actionsDropdown" class="shadow">
<button ngbDropdownItem (click)="redoOcr()" [disabled]="!userCanEdit">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-counterclockwise" />
</svg><span class="ps-1" i18n>Redo OCR</span>
</button>
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="moreLike()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#diagram-3" />
</svg><span class="d-none d-lg-inline ps-1" i18n>More like this</span>
</button>
<button ngbDropdownItem (click)="moreLike()">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#diagram-3" />
</svg><span class="ps-1" i18n>More like this</span>
</button>
</div>
</div>
<app-share-links-dropdown [documentId]="documentId" [disabled]="!userIsOwner" *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ShareLink }"></app-share-links-dropdown>
<button type="button" class="btn btn-sm btn-outline-primary me-2" i18n-title title="Close" (click)="close()">
<svg class="buttonicon" fill="currentColor">

View File

@@ -66,6 +66,7 @@ import { TextComponent } from '../common/input/text/text.component'
import { PageHeaderComponent } from '../common/page-header/page-header.component'
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { DocumentDetailComponent } from './document-detail.component'
import { ShareLinksDropdownComponent } from '../common/share-links-dropdown/share-links-dropdown.component'
const doc: PaperlessDocument = {
id: 3,
@@ -134,6 +135,7 @@ describe('DocumentDetailComponent', () => {
ConfirmDialogComponent,
PdfViewerComponent,
SafeUrlPipe,
ShareLinksDropdownComponent,
],
providers: [
DocumentTitlePipe,

View File

@@ -63,6 +63,7 @@ import { EditDialogMode } from '../common/edit-dialog/edit-dialog.component'
import { ObjectWithId } from 'src/app/data/object-with-id'
import { FilterRule } from 'src/app/data/filter-rule'
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import { ShareLinksDropdownComponent } from '../common/share-links-dropdown/share-links-dropdown.component'
enum DocumentDetailNavIDs {
Details = 1,

View File

@@ -0,0 +1,18 @@
import { ObjectWithPermissions } from './object-with-permissions'
export enum PaperlessFileVersion {
Archive = 'archive',
Original = 'original',
}
export interface PaperlessShareLink extends ObjectWithPermissions {
created: string // Date
expiration?: string // Date
slug: string
document: number // PaperlessDocument
file_version: string
}

View File

@@ -248,6 +248,10 @@ describe('PermissionsService', () => {
'view_log',
'view_comment',
'change_frontendsettings',
'add_sharelink',
'view_sharelink',
'change_sharelink',
'delete_sharelink',
],
{
username: 'testuser',

View File

@@ -24,6 +24,7 @@ export enum PermissionType {
User = '%s_user',
Group = '%s_group',
Admin = '%s_logentry',
ShareLink = '%s_sharelink',
}
@Injectable({

View File

@@ -10,7 +10,7 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
constructor(
protected http: HttpClient,
private resourceName: string
protected resourceName: string
) {}
protected getResourceUrl(id: number = null, action: string = null): string {

View File

@@ -0,0 +1,42 @@
import { HttpTestingController } from '@angular/common/http/testing'
import { TestBed } from '@angular/core/testing'
import { Subscription } from 'rxjs'
import { environment } from 'src/environments/environment'
import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
import { ShareLinkService } from './share-link.service'
let httpTestingController: HttpTestingController
let service: ShareLinkService
let subscription: Subscription
const endpoint = 'share_links'
// run common tests
commonAbstractPaperlessServiceTests(endpoint, ShareLinkService)
describe(`Additional service tests for ShareLinkService`, () => {
beforeEach(() => {
// Dont need to setup again
httpTestingController = TestBed.inject(HttpTestingController)
service = TestBed.inject(ShareLinkService)
})
afterEach(() => {
subscription?.unsubscribe()
httpTestingController.verify()
})
it('should support creating link for document', () => {
subscription = service.createLinkForDocument(0).subscribe()
httpTestingController
.expectOne(`${environment.apiBaseUrl}${endpoint}/`)
.flush({})
})
it('should support get links for a document', () => {
subscription = service.getLinksForDocument(0).subscribe()
httpTestingController
.expectOne(`${environment.apiBaseUrl}documents/0/${endpoint}/`)
.flush({})
})
})

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core'
import {
PaperlessShareLink,
PaperlessFileVersion,
} from 'src/app/data/paperless-share-link'
import { AbstractNameFilterService } from './abstract-name-filter-service'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class ShareLinkService extends AbstractNameFilterService<PaperlessShareLink> {
constructor(http: HttpClient) {
super(http, 'share_links')
}
getLinksForDocument(documentId: number): Observable<PaperlessShareLink[]> {
return this.http.get<PaperlessShareLink[]>(
`${this.baseUrl}documents/${documentId}/${this.resourceName}/`
)
}
createLinkForDocument(
documentId: number,
file_version: PaperlessFileVersion = PaperlessFileVersion.Archive,
expiration: Date = null
) {
this.clearCache()
return this.http.post<PaperlessShareLink>(this.getResourceUrl(), {
document: documentId,
file_version,
expiration: expiration?.toISOString(),
})
}
}