]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: implement document link custom field (#4799)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 5 Dec 2023 16:16:56 +0000 (08:16 -0800)
committerGitHub <noreply@github.com>
Tue, 5 Dec 2023 16:16:56 +0000 (08:16 -0800)
16 files changed:
docs/usage.md
src-ui/messages.xlf
src-ui/src/app/app.module.ts
src-ui/src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html
src-ui/src/app/components/common/input/document-link/document-link.component.html [new file with mode: 0644]
src-ui/src/app/components/common/input/document-link/document-link.component.scss [new file with mode: 0644]
src-ui/src/app/components/common/input/document-link/document-link.component.spec.ts [new file with mode: 0644]
src-ui/src/app/components/common/input/document-link/document-link.component.ts [new file with mode: 0644]
src-ui/src/app/components/common/input/tags/tags.component.html
src-ui/src/app/components/document-detail/document-detail.component.html
src-ui/src/app/data/paperless-custom-field.ts
src/documents/migrations/1042_consumptiontemplate_assign_custom_fields.py [deleted file]
src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py [new file with mode: 0644]
src/documents/models.py
src/documents/serialisers.py
src/documents/tests/test_api_custom_fields.py

index 4cb55613c0b38207e9a5119d016aad43e2281c59..fed7412b373549136ce872dc0340213562d5c7c9 100644 (file)
@@ -343,6 +343,7 @@ The following custom field types are supported:
 - `Integer`: integer number e.g. 12
 - `Number`: float number e.g. 12.3456
 - `Monetary`: float number with exactly two decimals, e.g. 12.30
+- `Document Link`: reference(s) to other document(s), displayed as links
 
 ## Share Links
 
index 1b94018f31f74819bb888fafaf685244d1c7576e..723abc0f94e13a5c9f395cc7adac5b9811ecd669 100644 (file)
         </context-group>
         <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">92</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1241348629231510663" datatype="html">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html</context>
           <context context-type="linenumber">30</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context>
+          <context context-type="linenumber">40</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
           <context context-type="linenumber">18</context>
           <context context-type="sourcefile">src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html</context>
           <context context-type="linenumber">14</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">248</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
           <context context-type="linenumber">95</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">221</context>
+          <context context-type="linenumber">216</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">46</context>
+          <context context-type="linenumber">47</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">65</context>
+          <context context-type="linenumber">66</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">54</context>
+          <context context-type="linenumber">55</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">73</context>
+          <context context-type="linenumber">74</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">79</context>
+          <context context-type="linenumber">80</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</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">34</context>
+          <context context-type="linenumber">45</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</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>
+          <context context-type="linenumber">22</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">90</context>
+          <context context-type="linenumber">91</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</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">93</context>
+          <context context-type="linenumber">104</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>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
-          <context context-type="linenumber">677</context>
+          <context context-type="linenumber">691</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</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">679</context>
+          <context context-type="linenumber">693</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">37</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="475685412372379925" datatype="html">
+        <source>Assign custom fields</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
+          <context context-type="linenumber">38</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5057200219587080996" datatype="html">
         <source>Assign owner</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">40</context>
+          <context context-type="linenumber">41</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1749184201773078639" datatype="html">
         <source>Assign view permissions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">42</context>
+          <context context-type="linenumber">43</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1744964187586405039" datatype="html">
         <source>Assign edit permissions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">61</context>
+          <context context-type="linenumber">62</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1519954996184640001" datatype="html">
         <source>Error</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">88</context>
+          <context context-type="linenumber">89</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
         <source>Cancel</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.html</context>
-          <context context-type="linenumber">89</context>
+          <context context-type="linenumber">90</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
         <source>Consume Folder</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.ts</context>
-          <context context-type="linenumber">25</context>
+          <context context-type="linenumber">27</context>
         </context-group>
       </trans-unit>
       <trans-unit id="526966086395145275" datatype="html">
         <source>API Upload</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.ts</context>
-          <context context-type="linenumber">29</context>
+          <context context-type="linenumber">31</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7502272564743467653" datatype="html">
         <source>Mail Fetch</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.ts</context>
-          <context context-type="linenumber">33</context>
+          <context context-type="linenumber">35</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2057608151109082984" datatype="html">
         <source>Create new consumption template</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.ts</context>
-          <context context-type="linenumber">83</context>
+          <context context-type="linenumber">92</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3745279073747685988" datatype="html">
         <source>Edit consumption template</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component.ts</context>
-          <context context-type="linenumber">87</context>
+          <context context-type="linenumber">96</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8743659855412792665" datatype="html">
         <source>Create new item</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/edit-dialog.component.ts</context>
-          <context context-type="linenumber">109</context>
+          <context context-type="linenumber">111</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5324147361912094446" datatype="html">
         <source>Edit item</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/edit-dialog/edit-dialog.component.ts</context>
-          <context context-type="linenumber">113</context>
+          <context context-type="linenumber">115</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7878445132438733225" datatype="html">
           <context context-type="sourcefile">src/app/components/common/input/date/date.component.html</context>
           <context context-type="linenumber">8</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context>
+          <context context-type="linenumber">8</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/input/number/number.component.html</context>
           <context context-type="linenumber">8</context>
           <context context-type="linenumber">155</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="6423278459497515329" datatype="html">
+        <source>No documents found</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.ts</context>
+          <context context-type="linenumber">44</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5554528553553249088" datatype="html">
         <source>Show password</source>
         <context-group purpose="location">
         <source>Note that permissions set here will override any existing permissions</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.ts</context>
-          <context context-type="linenumber">62</context>
+          <context context-type="linenumber">71</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5947558132119506443" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
-          <context context-type="linenumber">103</context>
+          <context context-type="linenumber">114</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</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">107</context>
+          <context context-type="linenumber">118</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</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">18</context>
+          <context context-type="linenumber">29</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
         <source>Page</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
-          <context context-type="linenumber">3</context>
+          <context context-type="linenumber">4</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
         <source>of <x id="INTERPOLATION" equiv-text="{{previewNumPages}}"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
-          <context context-type="linenumber">5,6</context>
+          <context context-type="linenumber">6,7</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="8590109102084543521" datatype="html">
+        <source>-</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
+          <context context-type="linenumber">9</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="8479257185772414452" datatype="html">
+        <source>+</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
+          <context context-type="linenumber">15</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8659635229098859487" datatype="html">
         <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">24</context>
+          <context context-type="linenumber">35</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">40</context>
+          <context context-type="linenumber">51</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
         <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">46</context>
+          <context context-type="linenumber">57</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>
         <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">71</context>
+          <context context-type="linenumber">82</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context>
         <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">76</context>
+          <context context-type="linenumber">87</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">89</context>
+          <context context-type="linenumber">100</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">91</context>
+          <context context-type="linenumber">102</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">92</context>
+          <context context-type="linenumber">103</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">100</context>
+          <context context-type="linenumber">111</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">104</context>
+          <context context-type="linenumber">115</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">105</context>
+          <context context-type="linenumber">116</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5066119607229701477" datatype="html">
         <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">109</context>
+          <context context-type="linenumber">120</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
         <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">111</context>
+          <context context-type="linenumber">122</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
         <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">112</context>
+          <context context-type="linenumber">123</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">130</context>
+          <context context-type="linenumber">142</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">139</context>
+          <context context-type="linenumber">151</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
         <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">145</context>
+          <context context-type="linenumber">157</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">149</context>
+          <context context-type="linenumber">161</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">153</context>
+          <context context-type="linenumber">165</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">157</context>
+          <context context-type="linenumber">169</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">161</context>
+          <context context-type="linenumber">173</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">165</context>
+          <context context-type="linenumber">177</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">169</context>
+          <context context-type="linenumber">181</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">173</context>
+          <context context-type="linenumber">185</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">177</context>
+          <context context-type="linenumber">189</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">183</context>
+          <context context-type="linenumber">195</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">184</context>
+          <context context-type="linenumber">196</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">190</context>
+          <context context-type="linenumber">202</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="8191371354890763172" datatype="html">
-        <source>Enter Password</source>
+      <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">206</context>
+          <context context-type="linenumber">209,210</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">249</context>
+          <context context-type="linenumber">237</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">214,215</context>
+          <context context-type="linenumber">275</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">265,267</context>
+          <context context-type="linenumber">279,281</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">413</context>
+          <context context-type="linenumber">427</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">434</context>
+          <context context-type="linenumber">448</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">552</context>
+          <context context-type="linenumber">566</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">561</context>
+          <context context-type="linenumber">575</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">565</context>
+          <context context-type="linenumber">579</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">606</context>
+          <context context-type="linenumber">620</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">632</context>
+          <context context-type="linenumber">646</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
         <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">633</context>
+          <context context-type="linenumber">647</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">634</context>
+          <context context-type="linenumber">648</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">636</context>
+          <context context-type="linenumber">650</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">655</context>
+          <context context-type="linenumber">669</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">675</context>
+          <context context-type="linenumber">689</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
         <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">676</context>
+          <context context-type="linenumber">690</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">687</context>
+          <context context-type="linenumber">701</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">698</context>
+          <context context-type="linenumber">712</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">781</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6857598786757174736" datatype="html">
         <source>Initiating upload...</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/file-drop/file-drop.component.ts</context>
-          <context context-type="linenumber">87</context>
+          <context context-type="linenumber">88</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3789574458951567089" datatype="html">
         <source>Boolean</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
-          <context context-type="linenumber">16</context>
+          <context context-type="linenumber">17</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3973931101896534797" datatype="html">
         <source>Date</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
-          <context context-type="linenumber">20</context>
+          <context context-type="linenumber">21</context>
         </context-group>
       </trans-unit>
       <trans-unit id="362956598863566327" datatype="html">
         <source>Integer</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
-          <context context-type="linenumber">24</context>
+          <context context-type="linenumber">25</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6370642728789544052" datatype="html">
         <source>Number</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
-          <context context-type="linenumber">28</context>
+          <context context-type="linenumber">29</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6430409302408843009" datatype="html">
         <source>Monetary</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
-          <context context-type="linenumber">32</context>
+          <context context-type="linenumber">33</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6162693758764653365" datatype="html">
         <source>Text</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
-          <context context-type="linenumber">36</context>
+          <context context-type="linenumber">37</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8308045076391224954" datatype="html">
         <source>Url</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
-          <context context-type="linenumber">40</context>
+          <context context-type="linenumber">41</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="3650316326183661476" datatype="html">
+        <source>Document Link</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/data/paperless-custom-field.ts</context>
+          <context context-type="linenumber">45</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5948496158474272829" datatype="html">
index 8d7ea5663091eeddbbf5889d39e551d4e8b67bd6..6910061d222c1467c200151f6999988e93291484 100644 (file)
@@ -106,6 +106,7 @@ import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/
 import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
 import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
 import { PdfViewerComponent } from './components/common/pdf-viewer/pdf-viewer.component'
+import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
 
 import localeAf from '@angular/common/locales/af'
 import localeAr from '@angular/common/locales/ar'
@@ -259,6 +260,7 @@ function initializeApp(settings: SettingsService) {
     CustomFieldsDropdownComponent,
     ProfileEditDialogComponent,
     PdfViewerComponent,
+    DocumentLinkComponent,
   ],
   imports: [
     BrowserModule,
index 94dc3297fe960baad886d38df84acd0c3c3dee4f..63f235b43d4b24ab439950109caddc8f28089f52 100644 (file)
@@ -1,16 +1,16 @@
 <form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
-    <div class="modal-header">
-      <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
-      <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
-      </button>
-    </div>
-    <div class="modal-body">
-      <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
-      <pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
-      <small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small>
-    </div>
-    <div class="modal-footer">
-      <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
-      <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
-    </div>
-  </form>
+  <div class="modal-header">
+    <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
+    <button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
+    </button>
+  </div>
+  <div class="modal-body">
+    <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
+    <pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
+    <small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
+    <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
+  </div>
+</form>
diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.html b/src-ui/src/app/components/common/input/document-link/document-link.component.html
new file mode 100644 (file)
index 0000000..03652a4
--- /dev/null
@@ -0,0 +1,50 @@
+<div class="mb-3 paperless-input-select" [class.disabled]="disabled">
+    <div class="row">
+      <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
+        <label *ngIf="title" class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
+        <button type="button" *ngIf="removable" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
+          <svg class="sidebaricon" fill="currentColor">
+            <use xlink:href="assets/bootstrap-icons.svg#x"/>
+          </svg>&nbsp;<ng-container i18n>Remove</ng-container>
+        </button>
+      </div>
+      <div [class.col-md-9]="horizontal">
+        <div>
+          <ng-select name="inputId" [(ngModel)]="selectedDocuments"
+            [disabled]="disabled"
+            [items]="foundDocuments$ | async"
+            placeholder="Search for documents"
+            [notFoundText]="notFoundText"
+            [multiple]="true"
+            bindValue="id"
+            [compareWith]="compareDocuments"
+            [trackByFn]="trackByFn"
+            [minTermLength]="2"
+            [loading]="loading"
+            [typeahead]="documentsInput$"
+            (change)="onChange(selectedDocuments)">
+                <ng-template ng-label-tmp let-document="item">
+                  <div class="d-flex align-items-center">
+                    <svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)">
+                      <use xlink:href="assets/bootstrap-icons.svg#x"/>
+                    </svg>
+                    <a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary">
+                      <svg class="sidebaricon-sm me-1" fill="currentColor">
+                        <use xlink:href="assets/bootstrap-icons.svg#file-text"/>
+                      </svg><span>{{document.title}}</span>
+                    </a>
+                  </div>
+                </ng-template>
+                <ng-template ng-loadingspinner-tmp>
+                  <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
+                  <div class="visually-hidden" i18n>Loading...</div>
+                </ng-template>
+                <ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
+                  <div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
+                </ng-template>
+          </ng-select>
+        </div>
+        <small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
+      </div>
+    </div>
+  </div>
diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.scss b/src-ui/src/app/components/common/input/document-link/document-link.component.scss
new file mode 100644 (file)
index 0000000..bcaa4e8
--- /dev/null
@@ -0,0 +1,14 @@
+::ng-deep .ng-select-container .ng-value-container .ng-value {
+    background-color: transparent !important;
+    border-color: transparent;
+}
+
+.sidebaricon {
+    cursor: pointer;
+}
+
+.badge {
+    font-size: .75rem;
+    // --bs-primary: var(--pngx-bg-alt);
+    // color: var(--pngx-primary-text-contrast);
+}
diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.spec.ts b/src-ui/src/app/components/common/input/document-link/document-link.component.spec.ts
new file mode 100644 (file)
index 0000000..d1af7ab
--- /dev/null
@@ -0,0 +1,118 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+  FormsModule,
+  NG_VALUE_ACCESSOR,
+  ReactiveFormsModule,
+} from '@angular/forms'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { of, throwError } from 'rxjs'
+import { DocumentService } from 'src/app/services/rest/document.service'
+import { DocumentLinkComponent } from './document-link.component'
+import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
+
+const documents = [
+  {
+    id: 1,
+    title: 'Document 1 foo',
+  },
+  {
+    id: 12,
+    title: 'Document 12 bar',
+  },
+  {
+    id: 23,
+    title: 'Document 23 bar',
+  },
+]
+
+describe('DocumentLinkComponent', () => {
+  let component: DocumentLinkComponent
+  let fixture: ComponentFixture<DocumentLinkComponent>
+  let documentService: DocumentService
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [DocumentLinkComponent],
+      imports: [
+        HttpClientTestingModule,
+        NgSelectModule,
+        FormsModule,
+        ReactiveFormsModule,
+      ],
+    })
+    documentService = TestBed.inject(DocumentService)
+    fixture = TestBed.createComponent(DocumentLinkComponent)
+    fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+    component = fixture.componentInstance
+    fixture.detectChanges()
+  })
+
+  it('should retrieve selected documents from APIs', () => {
+    const getSpy = jest.spyOn(documentService, 'getCachedMany')
+    getSpy.mockImplementation((ids) => {
+      return of(documents.filter((d) => ids.includes(d.id)))
+    })
+    component.writeValue([1])
+    expect(getSpy).toHaveBeenCalled()
+  })
+
+  it('should search API on select text input', () => {
+    const listSpy = jest.spyOn(documentService, 'listFiltered')
+    listSpy.mockImplementation(
+      (page, pageSize, sortField, sortReverse, filterRules, extraParams) => {
+        const docs = documents.filter((d) =>
+          d.title.includes(filterRules[0].value)
+        )
+        return of({
+          count: docs.length,
+          results: docs,
+          all: docs.map((d) => d.id),
+        })
+      }
+    )
+    component.documentsInput$.next('bar')
+    expect(listSpy).toHaveBeenCalledWith(
+      1,
+      null,
+      'created',
+      true,
+      [{ rule_type: FILTER_TITLE, value: 'bar' }],
+      { truncate_content: true }
+    )
+    listSpy.mockReturnValueOnce(throwError(() => new Error()))
+    component.documentsInput$.next('foo')
+  })
+
+  it('should load values correctly', () => {
+    jest.spyOn(documentService, 'getCachedMany').mockImplementation((ids) => {
+      return of(documents.filter((d) => ids.includes(d.id)))
+    })
+    component.writeValue([12, 23])
+    expect(component.value).toEqual([12, 23])
+    expect(component.selectedDocuments).toEqual([documents[1], documents[2]])
+    component.writeValue(null)
+    expect(component.value).toEqual([])
+    expect(component.selectedDocuments).toEqual([])
+    component.writeValue([])
+    expect(component.value).toEqual([])
+    expect(component.selectedDocuments).toEqual([])
+  })
+
+  it('should support unselect', () => {
+    const getSpy = jest.spyOn(documentService, 'getCachedMany')
+    getSpy.mockImplementation((ids) => {
+      return of(documents.filter((d) => ids.includes(d.id)))
+    })
+    component.writeValue([12, 23])
+    component.unselect({ id: 23 })
+    fixture.detectChanges()
+    expect(component.selectedDocuments).toEqual([documents[1]])
+  })
+
+  it('should use correct compare, trackBy functions', () => {
+    expect(component.compareDocuments(documents[0], { id: 1 })).toBeTruthy()
+    expect(component.compareDocuments(documents[0], { id: 2 })).toBeFalsy()
+    expect(component.trackByFn(documents[1])).toEqual(12)
+  })
+})
diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.ts b/src-ui/src/app/components/common/input/document-link/document-link.component.ts
new file mode 100644 (file)
index 0000000..dd71180
--- /dev/null
@@ -0,0 +1,120 @@
+import { Component, forwardRef, OnInit, Input, OnDestroy } from '@angular/core'
+import { NG_VALUE_ACCESSOR } from '@angular/forms'
+import {
+  Subject,
+  Observable,
+  takeUntil,
+  concat,
+  of,
+  distinctUntilChanged,
+  tap,
+  switchMap,
+  map,
+  catchError,
+} from 'rxjs'
+import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
+import { PaperlessDocument } from 'src/app/data/paperless-document'
+import { DocumentService } from 'src/app/services/rest/document.service'
+import { AbstractInputComponent } from '../abstract-input'
+
+@Component({
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => DocumentLinkComponent),
+      multi: true,
+    },
+  ],
+  selector: 'pngx-input-document-link',
+  templateUrl: './document-link.component.html',
+  styleUrls: ['./document-link.component.scss'],
+})
+export class DocumentLinkComponent
+  extends AbstractInputComponent<any[]>
+  implements OnInit, OnDestroy
+{
+  documentsInput$ = new Subject<string>()
+  foundDocuments$: Observable<PaperlessDocument[]>
+  loading = false
+  selectedDocuments: PaperlessDocument[] = []
+
+  private unsubscribeNotifier: Subject<any> = new Subject()
+
+  @Input()
+  notFoundText: string = $localize`No documents found`
+
+  constructor(private documentsService: DocumentService) {
+    super()
+  }
+
+  ngOnInit() {
+    this.loadDocs()
+  }
+
+  writeValue(documentIDs: number[]): void {
+    if (!documentIDs || documentIDs.length === 0) {
+      this.selectedDocuments = []
+      super.writeValue([])
+    } else {
+      this.loading = true
+      this.documentsService
+        .getCachedMany(documentIDs)
+        .pipe(takeUntil(this.unsubscribeNotifier))
+        .subscribe((documents) => {
+          this.loading = false
+          this.selectedDocuments = documents
+          super.writeValue(documentIDs)
+        })
+    }
+  }
+
+  private loadDocs() {
+    this.foundDocuments$ = concat(
+      of([]), // default items
+      this.documentsInput$.pipe(
+        distinctUntilChanged(),
+        takeUntil(this.unsubscribeNotifier),
+        tap(() => (this.loading = true)),
+        switchMap((title) =>
+          this.documentsService
+            .listFiltered(
+              1,
+              null,
+              'created',
+              true,
+              [{ rule_type: FILTER_TITLE, value: title }],
+              { truncate_content: true }
+            )
+            .pipe(
+              map((results) => results.results),
+              catchError(() => of([])), // empty on error
+              tap(() => (this.loading = false))
+            )
+        )
+      )
+    )
+  }
+
+  unselect(document: PaperlessDocument): void {
+    this.selectedDocuments = this.selectedDocuments.filter(
+      (d) => d.id !== document.id
+    )
+    this.onChange(this.selectedDocuments.map((d) => d.id))
+  }
+
+  compareDocuments(
+    document: PaperlessDocument,
+    selectedDocument: PaperlessDocument
+  ) {
+    return document.id === selectedDocument.id
+  }
+
+  trackByFn(item: PaperlessDocument) {
+    return item.id
+  }
+
+  ngOnDestroy(): void {
+    this.unsubscribeNotifier.next(true)
+    this.unsubscribeNotifier.complete()
+  }
+}
index a359bd387f43a4f463295070e54ce2b4d9b1af2f..3c93a167dcac33b44fcb65e4d58454b8c8c08088 100644 (file)
@@ -1,4 +1,4 @@
-<div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled" [class.pb-3]="suggestions">
+<div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled" [class.pb-3]="getSuggestions().length > 0">
   <div class="row">
     <div class="d-flex align-items-center" [class.col-md-3]="horizontal">
       <label class="form-label" [class.mb-md-0]="horizontal" for="tags" i18n>{{title}}</label>
index 8b50e6f2e61cf9e3381116db425b8ee5b815cdc2..ea14b750d8e9be1c829913c409f0f59a711f99e4 100644 (file)
                                     <pngx-input-number *ngSwitchCase="PaperlessCustomFieldDataType.Monetary" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".01" [error]="getCustomFieldError(i)"></pngx-input-number>
                                     <pngx-input-check *ngSwitchCase="PaperlessCustomFieldDataType.Boolean" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true"></pngx-input-check>
                                     <pngx-input-url *ngSwitchCase="PaperlessCustomFieldDataType.Url" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-url>
+                                    <pngx-input-document-link *ngSwitchCase="PaperlessCustomFieldDataType.DocumentLink" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-document-link>
                                 </div>
                             </ng-container>
                         </div>
index 663e1507f5ca484f8bf9703bd7aa055a8a893f33..93bd34e33ccf3cf03c62a1eb7e59d01f6e016312 100644 (file)
@@ -8,6 +8,7 @@ export enum PaperlessCustomFieldDataType {
   Integer = 'integer',
   Float = 'float',
   Monetary = 'monetary',
+  DocumentLink = 'documentlink',
 }
 
 export const DATA_TYPE_LABELS = [
@@ -39,6 +40,10 @@ export const DATA_TYPE_LABELS = [
     id: PaperlessCustomFieldDataType.Url,
     name: $localize`Url`,
   },
+  {
+    id: PaperlessCustomFieldDataType.DocumentLink,
+    name: $localize`Document Link`,
+  },
 ]
 
 export interface PaperlessCustomField extends ObjectWithId {
diff --git a/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields.py b/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields.py
deleted file mode 100644 (file)
index 08d6062..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 4.2.7 on 2023-11-30 17:44
-
-from django.db import migrations
-from django.db import models
-
-
-class Migration(migrations.Migration):
-    dependencies = [
-        ("documents", "1041_alter_consumptiontemplate_sources"),
-    ]
-
-    operations = [
-        migrations.AddField(
-            model_name="consumptiontemplate",
-            name="assign_custom_fields",
-            field=models.ManyToManyField(
-                blank=True,
-                related_name="+",
-                to="documents.customfield",
-                verbose_name="assign these custom fields",
-            ),
-        ),
-    ]
diff --git a/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py b/src/documents/migrations/1042_consumptiontemplate_assign_custom_fields_and_more.py
new file mode 100644 (file)
index 0000000..ffd0dbe
--- /dev/null
@@ -0,0 +1,47 @@
+# Generated by Django 4.2.7 on 2023-12-04 04:03
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("documents", "1041_alter_consumptiontemplate_sources"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="consumptiontemplate",
+            name="assign_custom_fields",
+            field=models.ManyToManyField(
+                blank=True,
+                related_name="+",
+                to="documents.customfield",
+                verbose_name="assign these custom fields",
+            ),
+        ),
+        migrations.AddField(
+            model_name="customfieldinstance",
+            name="value_document_ids",
+            field=models.JSONField(null=True),
+        ),
+        migrations.AlterField(
+            model_name="customfield",
+            name="data_type",
+            field=models.CharField(
+                choices=[
+                    ("string", "String"),
+                    ("url", "URL"),
+                    ("date", "Date"),
+                    ("boolean", "Boolean"),
+                    ("integer", "Integer"),
+                    ("float", "Float"),
+                    ("monetary", "Monetary"),
+                    ("documentlink", "Document Link"),
+                ],
+                editable=False,
+                max_length=50,
+                verbose_name="data type",
+            ),
+        ),
+    ]
index d688253de7a6458aa26fdcd3009a49cbf39ac0e4..250a9d35b2e1f25470c0ba98235762fac4c8edc7 100644 (file)
@@ -756,6 +756,7 @@ class CustomField(models.Model):
         INT = ("integer", _("Integer"))
         FLOAT = ("float", _("Float"))
         MONETARY = ("monetary", _("Monetary"))
+        DOCUMENTLINK = ("documentlink", _("Document Link"))
 
     created = models.DateTimeField(
         _("created"),
@@ -834,6 +835,8 @@ class CustomFieldInstance(models.Model):
 
     value_monetary = models.DecimalField(null=True, decimal_places=2, max_digits=12)
 
+    value_document_ids = models.JSONField(null=True)
+
     class Meta:
         ordering = ("created",)
         verbose_name = _("custom field instance")
@@ -868,6 +871,8 @@ class CustomFieldInstance(models.Model):
             return self.value_float
         elif self.field.data_type == CustomField.FieldDataType.MONETARY:
             return self.value_monetary
+        elif self.field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
+            return self.value_document_ids
         raise NotImplementedError(self.field.data_type)
 
 
index 2373a25dd5f8aed897bc37114b330521137eab48..f01d1fc3ac7cf5ead667b856bb49bc5e0df7a51e 100644 (file)
@@ -440,6 +440,7 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
             CustomField.FieldDataType.INT: "value_int",
             CustomField.FieldDataType.FLOAT: "value_float",
             CustomField.FieldDataType.MONETARY: "value_monetary",
+            CustomField.FieldDataType.DOCUMENTLINK: "value_document_ids",
         }
         # An instance is attached to a document
         document: Document = validated_data["document"]
index 725bd92542711e9999c253f58ee63df58def1f87..cde5f302ce1d4bb31e2ccd6600ccc42a887c65f3 100644 (file)
@@ -34,7 +34,9 @@ class TestCustomField(DirectoriesMixin, APITestCase):
             ("date", "Invoiced Date"),
             ("integer", "Invoice #"),
             ("boolean", "Is Active"),
-            ("float", "Total Paid"),
+            ("float", "Average Value"),
+            ("monetary", "Total Paid"),
+            ("documentlink", "Related Documents"),
         ]:
             resp = self.client.post(
                 self.ENDPOINT,
@@ -96,6 +98,10 @@ class TestCustomField(DirectoriesMixin, APITestCase):
             name="Test Custom Field Monetary",
             data_type=CustomField.FieldDataType.MONETARY,
         )
+        custom_field_documentlink = CustomField.objects.create(
+            name="Test Custom Field Doc Link",
+            data_type=CustomField.FieldDataType.DOCUMENTLINK,
+        )
 
         date_value = date.today()
 
@@ -131,6 +137,10 @@ class TestCustomField(DirectoriesMixin, APITestCase):
                         "field": custom_field_monetary.id,
                         "value": 11.10,
                     },
+                    {
+                        "field": custom_field_documentlink.id,
+                        "value": [1, 2, 3],
+                    },
                 ],
             },
             format="json",
@@ -150,11 +160,12 @@ class TestCustomField(DirectoriesMixin, APITestCase):
                 {"field": custom_field_url.id, "value": "https://example.com"},
                 {"field": custom_field_float.id, "value": 12.3456},
                 {"field": custom_field_monetary.id, "value": 11.10},
+                {"field": custom_field_documentlink.id, "value": [1, 2, 3]},
             ],
         )
 
         doc.refresh_from_db()
-        self.assertEqual(len(doc.custom_fields.all()), 7)
+        self.assertEqual(len(doc.custom_fields.all()), 8)
 
     def test_change_custom_field_instance_value(self):
         """