]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Allow filtering on multiple correspondents, doctypes, storage paths
authorshamoon <4887959+shamoon@users.noreply.github.com>
Fri, 10 Mar 2023 22:53:32 +0000 (14:53 -0800)
committershamoon <4887959+shamoon@users.noreply.github.com>
Sat, 18 Mar 2023 00:57:54 +0000 (17:57 -0700)
Preserve 'Not assigned' option
Fix default logical operator
Update frontend strings
Fix radio button name overlaps
Use include / exclude with multi-select for OneToOne objects

15 files changed:
src-ui/messages.xlf
src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
src-ui/src/app/components/document-list/document-list.component.html
src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts
src-ui/src/app/components/manage/document-type-list/document-type-list.component.ts
src-ui/src/app/components/manage/storage-path-list/storage-path-list.component.ts
src-ui/src/app/data/filter-rule-type.ts
src-ui/src/app/utils/query-params.ts
src/documents/filters.py
src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py [new file with mode: 0644]
src/documents/models.py

index f7cc20fc3a2da9fc98e02b1c070e904688ebf8b3..a89aca2388f149123fdb2fa3836255cd9832a5de 100644 (file)
         </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">17</context>
+          <context context-type="linenumber">21</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/common/permissions-dialog/permissions-dialog.component.html</context>
-          <context context-type="linenumber">16</context>
+          <context context-type="linenumber">20</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/select-dialog/select-dialog.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">213</context>
+          <context context-type="linenumber">208</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
           <context context-type="linenumber">18</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="6381578200008167206" datatype="html">
+        <source>Include</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
+          <context context-type="linenumber">24</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5668077948386857930" datatype="html">
+        <source>Exclude</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="4391289919356861627" datatype="html">
         <source>Apply</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
-          <context context-type="linenumber">32</context>
+          <context context-type="linenumber">40</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7780041345210191160" datatype="html">
         <source>Click again to exclude items.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
-          <context context-type="linenumber">38</context>
+          <context context-type="linenumber">46</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7593728289020204896" datatype="html">
         <source>Not assigned</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
-          <context context-type="linenumber">262</context>
+          <context context-type="linenumber">321</context>
         </context-group>
         <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
       </trans-unit>
           <context context-type="linenumber">12</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="3894950702316166331" datatype="html">
+        <source>Loading...</source>
+        <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-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
+          <context context-type="linenumber">26</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html</context>
+          <context context-type="linenumber">7</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/manage/settings/settings.component.html</context>
+          <context context-type="linenumber">230</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
+          <context context-type="linenumber">320</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
+          <context context-type="linenumber">406</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
+          <context context-type="linenumber">19</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
+          <context context-type="linenumber">27</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="8105421668262723483" datatype="html">
         <source>Set Permissions</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">41</context>
+          <context context-type="linenumber">43</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8650499415827640724" datatype="html">
           <context context-type="linenumber">20</context>
         </context-group>
       </trans-unit>
-      <trans-unit id="3894950702316166331" datatype="html">
-        <source>Loading...</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
-          <context context-type="linenumber">26</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html</context>
-          <context context-type="linenumber">7</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/manage/settings/settings.component.html</context>
-          <context context-type="linenumber">230</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
-          <context context-type="linenumber">320</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
-          <context context-type="linenumber">406</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
-          <context context-type="linenumber">19</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">src/app/components/manage/tasks/tasks.component.html</context>
-          <context context-type="linenumber">27</context>
-        </context-group>
-      </trans-unit>
       <trans-unit id="1865646076514070962" datatype="html">
         <source>Hello <x id="PH" equiv-text="this.settingsService.displayName"/>, welcome to Paperless-ngx</source>
         <context-group purpose="location">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">162</context>
+          <context context-type="linenumber">172</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
         <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">319</context>
+          <context context-type="linenumber">325</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">432</context>
+          <context context-type="linenumber">439</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">476</context>
+          <context context-type="linenumber">483</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">505</context>
+          <context context-type="linenumber">512</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">209</context>
+          <context context-type="linenumber">204</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5382975254277698192" datatype="html">
         <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">506</context>
+          <context context-type="linenumber">513</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">507</context>
+          <context context-type="linenumber">514</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">509</context>
+          <context context-type="linenumber">516</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1844801255494293730" datatype="html">
         <source>Error deleting document: <x id="PH" equiv-text="JSON.stringify(error)"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
-          <context context-type="linenumber">525</context>
+          <context context-type="linenumber">532</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">545</context>
+          <context context-type="linenumber">552</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">546</context>
+          <context context-type="linenumber">553</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">547</context>
+          <context context-type="linenumber">554</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
         <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">549</context>
+          <context context-type="linenumber">556</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
         <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">557</context>
+          <context context-type="linenumber">564</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8008978164775353960" datatype="html">
               )"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
-          <context context-type="linenumber">568,570</context>
+          <context context-type="linenumber">575,577</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6857598786757174736" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</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/services/rest/document.service.ts</context>
         <source>Correspondent: <x id="PH" equiv-text="this.correspondents.find((c) =&gt; c.id == +rule.value)?.name"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">98,100</context>
+          <context context-type="linenumber">108,110</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8170755470576301659" datatype="html">
         <source>Without correspondent</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">102</context>
+          <context context-type="linenumber">112</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8705701325879965907" datatype="html">
         <source>Type: <x id="PH" equiv-text="this.documentTypes.find((dt) =&gt; dt.id == +rule.value)?.name"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">107,109</context>
+          <context context-type="linenumber">117,119</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4362173610367509215" datatype="html">
         <source>Without document type</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">111</context>
+          <context context-type="linenumber">121</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8180755793012580465" datatype="html">
         <source>Tag: <x id="PH" equiv-text="this.tags.find((t) =&gt; t.id == +rule.value)?.name"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">115,117</context>
+          <context context-type="linenumber">125,127</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6494566478302448576" datatype="html">
         <source>Without any tag</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">121</context>
+          <context context-type="linenumber">131</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6523384805359286307" datatype="html">
         <source>Title: <x id="PH" equiv-text="rule.value"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">125</context>
+          <context context-type="linenumber">135</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1872523635812236432" datatype="html">
         <source>ASN: <x id="PH" equiv-text="rule.value"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">128</context>
+          <context context-type="linenumber">138</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3100631071441658964" datatype="html">
         <source>Title &amp; content</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">165</context>
+          <context context-type="linenumber">175</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1010505078885609376" datatype="html">
         <source>Advanced search</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">170</context>
+          <context context-type="linenumber">180</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2649431021108393503" datatype="html">
         <source>More like</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">176</context>
+          <context context-type="linenumber">186</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3697582909018473071" datatype="html">
         <source>equals</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">195</context>
+          <context context-type="linenumber">205</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5325481293405718739" datatype="html">
         <source>is empty</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">199</context>
+          <context context-type="linenumber">209</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6166785695326182482" datatype="html">
         <source>is not empty</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">203</context>
+          <context context-type="linenumber">213</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4686622206659266699" datatype="html">
         <source>greater than</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">207</context>
+          <context context-type="linenumber">217</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8014012170270529279" datatype="html">
         <source>less than</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
-          <context context-type="linenumber">211</context>
+          <context context-type="linenumber">221</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7210076240260527720" datatype="html">
         <source>Error occurred while creating <x id="PH" equiv-text="this.typeName"/> : <x id="PH_1" equiv-text="activeModal.componentInstance.error"/>.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">142,144</context>
+          <context context-type="linenumber">142</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">155,157</context>
+          <context context-type="linenumber">153,155</context>
         </context-group>
       </trans-unit>
       <trans-unit id="211408744872436427" datatype="html">
         <source>Successfully created <x id="PH" equiv-text="this.typeName"/>.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">149</context>
+          <context context-type="linenumber">147</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6151710751857751783" datatype="html">
         <source>Error occurred while saving <x id="PH" equiv-text="this.typeName"/> : <x id="PH_1" equiv-text="activeModal.componentInstance.error"/>.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">173,175</context>
+          <context context-type="linenumber">171</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">187,189</context>
+          <context context-type="linenumber">182,184</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2541368547549828690" datatype="html">
         <source>Successfully updated <x id="PH" equiv-text="this.typeName"/>.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">181</context>
+          <context context-type="linenumber">176</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4012132330507560812" datatype="html">
         <source>Do you really want to delete the <x id="PH" equiv-text="this.typeName"/>?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">196</context>
+          <context context-type="linenumber">191</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8371896857609524947" datatype="html">
         <source>Associated documents will not be deleted.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">211</context>
+          <context context-type="linenumber">206</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5467489005440577210" datatype="html">
             )"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
-          <context context-type="linenumber">224,226</context>
+          <context context-type="linenumber">219,221</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1685061484835793745" datatype="html">
index 48fe74ad7d0286caa70c2a21fe8d85550cea46d4..5bf75d62d4887d90b3c4eb28a62d861b6ed312d7 100644 (file)
@@ -1,21 +1,29 @@
 <div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
-  <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
+  <button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
     <svg class="toolbaricon" fill="currentColor">
       <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
     </svg>
     <div class="d-none d-sm-inline">&nbsp;{{title}}</div>
     <ng-container *ngIf="!editing && selectionModel.totalCount > 0">
-      <app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
+      <app-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
     </ng-container>
   </button>
-  <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
+  <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
     <div class="list-group list-group-flush">
-      <div *ngIf="!editing && multiple" class="list-group-item d-flex">
-        <div class="btn-group btn-group-xs flex-fill">
-          <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd" value="and">
-          <label class="btn btn-outline-primary" for="logicalOperatorAnd" i18n>All</label>
-          <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr" value="or">
-          <label class="btn btn-outline-primary" for="logicalOperatorOr" i18n>Any</label>
+      <div *ngIf="!editing && manyToOne" class="list-group-item d-flex">
+        <div class="btn-group btn-group-xs flex-fill" role="group">
+          <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and">
+          <label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label>
+          <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or">
+          <label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label>
+        </div>
+      </div>
+      <div *ngIf="!editing && !manyToOne" class="list-group-item d-flex">
+        <div class="btn-group btn-group-xs flex-fill" role="group">
+          <input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include">
+          <label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label>
+          <input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude">
+          <label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label>
         </div>
       </div>
       <div class="list-group-item">
@@ -34,7 +42,7 @@
           <use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
         </svg>
       </button>
-      <div *ngIf="!editing && multiple" class="list-group-item list-group-item-note pt-1 pb-2">
+      <div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2">
         <small i18n>Click again to exclude items.</small>
       </div>
     </div>
index b2352f869c6234f77fa9a70a952f7032b7308635..12de82693f9ccecb77fb4ccab22ce976976ddaaf 100644 (file)
@@ -18,12 +18,25 @@ export interface ChangedItems {
   itemsToRemove: MatchingModel[]
 }
 
+export enum LogicalOperator {
+  And = 'and',
+  Or = 'or',
+}
+
+export enum Intersection {
+  Include = 'include',
+  Exclude = 'exclude',
+}
+
 export class FilterableDropdownSelectionModel {
   changed = new Subject<FilterableDropdownSelectionModel>()
 
-  multiple = false
-  private _logicalOperator = 'and'
-  temporaryLogicalOperator = this._logicalOperator
+  manyToOne = false
+  singleSelect = false
+  private _logicalOperator: LogicalOperator = LogicalOperator.And
+  temporaryLogicalOperator: LogicalOperator = this._logicalOperator
+  private _intersection: Intersection = Intersection.Include
+  temporaryIntersection: Intersection = this._intersection
 
   items: MatchingModel[] = []
 
@@ -86,7 +99,30 @@ export class FilterableDropdownSelectionModel {
       (state != ToggleableItemState.Selected &&
         state != ToggleableItemState.Excluded)
     ) {
-      this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
+      if (this.manyToOne || this.singleSelect) {
+        this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
+
+        if (this.singleSelect) {
+          for (let key of this.temporarySelectionStates.keys()) {
+            if (key != id) {
+              this.temporarySelectionStates.delete(key)
+            }
+          }
+        }
+      } else {
+        let newState =
+          this.intersection == Intersection.Include
+            ? ToggleableItemState.Selected
+            : ToggleableItemState.Excluded
+        if (!id) newState = ToggleableItemState.Selected
+        if (
+          state == ToggleableItemState.Excluded &&
+          this.intersection == Intersection.Exclude
+        ) {
+          newState = ToggleableItemState.NotSelected
+        }
+        this.temporarySelectionStates.set(id, newState)
+      }
     } else if (
       state == ToggleableItemState.Selected ||
       state == ToggleableItemState.Excluded
@@ -94,14 +130,6 @@ export class FilterableDropdownSelectionModel {
       this.temporarySelectionStates.delete(id)
     }
 
-    if (!this.multiple) {
-      for (let key of this.temporarySelectionStates.keys()) {
-        if (key != id) {
-          this.temporarySelectionStates.delete(key)
-        }
-      }
-    }
-
     if (!id) {
       for (let key of this.temporarySelectionStates.keys()) {
         if (key) {
@@ -119,19 +147,36 @@ export class FilterableDropdownSelectionModel {
 
   exclude(id: number, fireEvent: boolean = true) {
     let state = this.temporarySelectionStates.get(id)
-    if (state == null || state != ToggleableItemState.Excluded) {
-      this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
-      this.temporaryLogicalOperator = this._logicalOperator = 'and'
-    } else if (state == ToggleableItemState.Excluded) {
-      this.temporarySelectionStates.delete(id)
-    }
-
-    if (!this.multiple) {
-      for (let key of this.temporarySelectionStates.keys()) {
-        if (key != id) {
-          this.temporarySelectionStates.delete(key)
+    if (id && (state == null || state != ToggleableItemState.Excluded)) {
+      this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne
+        ? LogicalOperator.And
+        : LogicalOperator.Or
+
+      if (this.manyToOne || this.singleSelect) {
+        this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
+
+        if (this.singleSelect) {
+          for (let key of this.temporarySelectionStates.keys()) {
+            if (key != id) {
+              this.temporarySelectionStates.delete(key)
+            }
+          }
+        }
+      } else {
+        let newState =
+          this.intersection == Intersection.Include
+            ? ToggleableItemState.Selected
+            : ToggleableItemState.Excluded
+        if (
+          state == ToggleableItemState.Selected &&
+          this.intersection == Intersection.Include
+        ) {
+          newState = ToggleableItemState.NotSelected
         }
+        this.temporarySelectionStates.set(id, newState)
       }
+    } else if (!id || state == ToggleableItemState.Excluded) {
+      this.temporarySelectionStates.delete(id)
     }
 
     if (fireEvent) {
@@ -143,11 +188,11 @@ export class FilterableDropdownSelectionModel {
     return this.selectionStates.get(id) || ToggleableItemState.NotSelected
   }
 
-  get logicalOperator(): string {
+  get logicalOperator(): LogicalOperator {
     return this.temporaryLogicalOperator
   }
 
-  set logicalOperator(operator: string) {
+  set logicalOperator(operator: LogicalOperator) {
     this.temporaryLogicalOperator = operator
   }
 
@@ -155,6 +200,26 @@ export class FilterableDropdownSelectionModel {
     this.changed.next(this)
   }
 
+  get intersection(): Intersection {
+    return this.temporaryIntersection
+  }
+
+  set intersection(intersection: Intersection) {
+    this.temporaryIntersection = intersection
+  }
+
+  toggleIntersection() {
+    if (this.temporarySelectionStates.size === 0) return
+    let newState =
+      this.intersection == Intersection.Include
+        ? ToggleableItemState.Selected
+        : ToggleableItemState.Excluded
+    this.temporarySelectionStates.forEach((state, key) => {
+      this.temporarySelectionStates.set(key, newState)
+    })
+    this.changed.next(this)
+  }
+
   get(id: number) {
     return (
       this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
@@ -171,7 +236,8 @@ export class FilterableDropdownSelectionModel {
 
   clear(fireEvent = true) {
     this.temporarySelectionStates.clear()
-    this.temporaryLogicalOperator = this._logicalOperator = 'and'
+    this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
+    this.temporaryIntersection = this._intersection = Intersection.Include
     if (fireEvent) {
       this.changed.next(this)
     }
@@ -194,6 +260,8 @@ export class FilterableDropdownSelectionModel {
       return true
     } else if (this.temporaryLogicalOperator !== this._logicalOperator) {
       return true
+    } else if (this.temporaryIntersection !== this._intersection) {
+      return true
     } else {
       return false
     }
@@ -217,13 +285,18 @@ export class FilterableDropdownSelectionModel {
       this.selectionStates.set(key, value)
     })
     this._logicalOperator = this.temporaryLogicalOperator
+    this._intersection = this.temporaryIntersection
   }
 
-  reset() {
+  reset(complete: boolean = false) {
     this.temporarySelectionStates.clear()
-    this.selectionStates.forEach((value, key) => {
-      this.temporarySelectionStates.set(key, value)
-    })
+    if (complete) {
+      this.selectionStates.clear()
+    } else {
+      this.selectionStates.forEach((value, key) => {
+        this.temporarySelectionStates.set(key, value)
+      })
+    }
   }
 
   diff(): ChangedItems {
@@ -269,14 +342,16 @@ export class FilterableDropdownComponent {
     return this._selectionModel.items
   }
 
-  _selectionModel = new FilterableDropdownSelectionModel()
+  _selectionModel: FilterableDropdownSelectionModel =
+    new FilterableDropdownSelectionModel()
 
   @Input()
   set selectionModel(model: FilterableDropdownSelectionModel) {
     if (this.selectionModel) {
       this.selectionModel.changed.complete()
       model.items = this.selectionModel.items
-      model.multiple = this.selectionModel.multiple
+      model.manyToOne = this.selectionModel.manyToOne
+      model.singleSelect = this.editing && !this.selectionModel.manyToOne
     }
     model.changed.subscribe((updatedModel) => {
       this.selectionModelChange.next(updatedModel)
@@ -292,12 +367,12 @@ export class FilterableDropdownComponent {
   selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
 
   @Input()
-  set multiple(value: boolean) {
-    this.selectionModel.multiple = value
+  set manyToOne(manyToOne: boolean) {
+    this.selectionModel.manyToOne = manyToOne
   }
 
-  get multiple() {
-    return this.selectionModel.multiple
+  get manyToOne() {
+    return this.selectionModel.manyToOne
   }
 
   @Input()
@@ -327,16 +402,20 @@ export class FilterableDropdownComponent {
   @Output()
   opened = new EventEmitter()
 
-  get operatorToggleEnabled(): boolean {
-    return (
-      this.selectionModel.selectionSize() > 1 &&
-      this.selectionModel.getExcludedItems().length == 0
-    )
+  get modifierToggleEnabled(): boolean {
+    return this.manyToOne
+      this.selectionModel.selectionSize() > 1 &&
+          this.selectionModel.getExcludedItems().length == 0
+      : !this.selectionModel.isNoneSelected()
   }
 
   @Input()
   documentCounts: SelectionDataItem[]
 
+  get name(): string {
+    return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
+  }
+
   getUpdatedDocumentCount(id: number) {
     if (this.documentCounts) {
       return this.documentCounts.find((c) => c.id === id)?.document_count
@@ -346,7 +425,6 @@ export class FilterableDropdownComponent {
   modelIsDirty: boolean = false
 
   constructor(private filterPipe: FilterPipe) {
-    this.selectionModel = new FilterableDropdownSelectionModel()
     this.selectionModelChange.subscribe((updatedModel) => {
       this.modelIsDirty = updatedModel.isDirty()
     })
@@ -400,7 +478,7 @@ export class FilterableDropdownComponent {
   }
 
   reset() {
-    this.selectionModel.reset()
+    this.selectionModel.reset(true)
     this.selectionModelChange.emit(this.selectionModel)
   }
 }
index b6cc92e40872d2e62075867269d1ff17e06c66af..dc60bceb9a25a3d5163ddf6162fdfd250d0d7f33 100644 (file)
@@ -30,7 +30,7 @@
         [items]="tags"
         [disabled]="!userCanEditAll"
         [editing]="true"
-        [multiple]="true"
+        [manyToOne]="true"
         [applyOnClose]="applyOnClose"
         (opened)="openTagsDropdown()"
         [(selectionModel)]="tagSelectionModel"
index 081c013bb7fc38d8022fc8795ddf722c42167f2e..25a75c1b6e77d21bc01f120fdd58777611e53fa5 100644 (file)
     </div>
   </div>
   <div class="btn-group flex-fill" role="group">
-    <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails">
+    <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails">
     <label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
       <svg class="toolbaricon" fill="currentColor">
         <use xlink:href="assets/bootstrap-icons.svg#list-ul" />
       </svg>
     </label>
-    <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall">
+    <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall">
     <label for="displayModeSmall" class="btn btn-outline-primary btn-sm">
       <svg class="toolbaricon" fill="currentColor">
         <use xlink:href="assets/bootstrap-icons.svg#grid" />
       </svg>
     </label>
-    <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge">
+    <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge">
     <label for="displayModeLarge" class="btn btn-outline-primary btn-sm">
       <svg class="toolbaricon" fill="currentColor">
         <use xlink:href="assets/bootstrap-icons.svg#hdd-stack" />
index f6f4a6b326feb8ab30f9eb370e1c0c78cce280a1..4e7851a57739819e128e3f58d532e87c1fec5fdc 100644 (file)
@@ -27,7 +27,7 @@
           <app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title
             filterPlaceholder="Filter tags" i18n-filterPlaceholder
             [items]="tags"
-            [multiple]="true"
+            [manyToOne]="true"
             [(selectionModel)]="tagSelectionModel"
             (selectionModelChange)="updateRules()"
             (opened)="onTagsDropdownOpen()"
index b71715ef0e2d68783cdbcb745f4685655cf9ec32..10048f7d76688b96d075e76abe4a8dea1eeec3f9 100644 (file)
@@ -21,10 +21,10 @@ import {
   FILTER_ADDED_AFTER,
   FILTER_ADDED_BEFORE,
   FILTER_ASN,
-  FILTER_CORRESPONDENT,
+  FILTER_HAS_CORRESPONDENT_ANY,
   FILTER_CREATED_AFTER,
   FILTER_CREATED_BEFORE,
-  FILTER_DOCUMENT_TYPE,
+  FILTER_HAS_DOCUMENT_TYPE_ANY,
   FILTER_FULLTEXT_MORELIKE,
   FILTER_FULLTEXT_QUERY,
   FILTER_HAS_ANY_TAG,
@@ -33,12 +33,22 @@ import {
   FILTER_DOES_NOT_HAVE_TAG,
   FILTER_TITLE,
   FILTER_TITLE_CONTENT,
-  FILTER_STORAGE_PATH,
+  FILTER_HAS_STORAGE_PATH_ANY,
   FILTER_ASN_ISNULL,
   FILTER_ASN_GT,
   FILTER_ASN_LT,
+  FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+  FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+  FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+  FILTER_DOCUMENT_TYPE,
+  FILTER_CORRESPONDENT,
+  FILTER_STORAGE_PATH,
 } from 'src/app/data/filter-rule-type'
-import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component'
+import {
+  FilterableDropdownSelectionModel,
+  Intersection,
+  LogicalOperator,
+} from '../../common/filterable-dropdown/filterable-dropdown.component'
 import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
 import {
   DocumentService,
@@ -93,7 +103,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
     if (this.filterRules.length == 1) {
       let rule = this.filterRules[0]
       switch (this.filterRules[0].rule_type) {
-        case FILTER_CORRESPONDENT:
+        case FILTER_HAS_CORRESPONDENT_ANY:
           if (rule.value) {
             return $localize`Correspondent: ${
               this.correspondents.find((c) => c.id == +rule.value)?.name
@@ -102,7 +112,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
             return $localize`Without correspondent`
           }
 
-        case FILTER_DOCUMENT_TYPE:
+        case FILTER_HAS_DOCUMENT_TYPE_ANY:
           if (rule.value) {
             return $localize`Type: ${
               this.documentTypes.find((dt) => dt.id == +rule.value)?.name
@@ -335,6 +345,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
           this.dateAddedBefore = rule.value
           break
         case FILTER_HAS_TAGS_ALL:
+          this.tagSelectionModel.logicalOperator = LogicalOperator.And
           this.tagSelectionModel.set(
             rule.value ? +rule.value : null,
             ToggleableItemState.Selected,
@@ -342,7 +353,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
           )
           break
         case FILTER_HAS_TAGS_ANY:
-          this.tagSelectionModel.logicalOperator = 'or'
+          this.tagSelectionModel.logicalOperator = LogicalOperator.Or
           this.tagSelectionModel.set(
             rule.value ? +rule.value : null,
             ToggleableItemState.Selected,
@@ -360,26 +371,59 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
           )
           break
         case FILTER_CORRESPONDENT:
+        case FILTER_HAS_CORRESPONDENT_ANY:
+          this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
+          this.correspondentSelectionModel.intersection = Intersection.Include
           this.correspondentSelectionModel.set(
             rule.value ? +rule.value : null,
             ToggleableItemState.Selected,
             false
           )
           break
+        case FILTER_DOES_NOT_HAVE_CORRESPONDENT:
+          this.correspondentSelectionModel.intersection = Intersection.Exclude
+          this.correspondentSelectionModel.set(
+            rule.value ? +rule.value : null,
+            ToggleableItemState.Excluded,
+            false
+          )
+          break
         case FILTER_DOCUMENT_TYPE:
+        case FILTER_HAS_DOCUMENT_TYPE_ANY:
+          this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
+          this.documentTypeSelectionModel.intersection = Intersection.Include
           this.documentTypeSelectionModel.set(
             rule.value ? +rule.value : null,
             ToggleableItemState.Selected,
             false
           )
           break
+        case FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE:
+          this.documentTypeSelectionModel.intersection = Intersection.Exclude
+          this.documentTypeSelectionModel.set(
+            rule.value ? +rule.value : null,
+            ToggleableItemState.Excluded,
+            false
+          )
+          break
         case FILTER_STORAGE_PATH:
+        case FILTER_HAS_STORAGE_PATH_ANY:
+          this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
+          this.storagePathSelectionModel.intersection = Intersection.Include
           this.storagePathSelectionModel.set(
             rule.value ? +rule.value : null,
             ToggleableItemState.Selected,
             false
           )
           break
+        case FILTER_DOES_NOT_HAVE_STORAGE_PATH:
+          this.storagePathSelectionModel.intersection = Intersection.Exclude
+          this.storagePathSelectionModel.set(
+            rule.value ? +rule.value : null,
+            ToggleableItemState.Excluded,
+            false
+          )
+          break
         case FILTER_ASN_ISNULL:
           this.textFilterTarget = TEXT_FILTER_TARGET_ASN
           this.textFilterModifier =
@@ -469,7 +513,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
       filterRules.push({ rule_type: FILTER_HAS_ANY_TAG, value: 'false' })
     } else {
       const tagFilterType =
-        this.tagSelectionModel.logicalOperator == 'and'
+        this.tagSelectionModel.logicalOperator == LogicalOperator.And
           ? FILTER_HAS_TAGS_ALL
           : FILTER_HAS_TAGS_ANY
       this.tagSelectionModel
@@ -491,28 +535,66 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
           })
         })
     }
-    this.correspondentSelectionModel
-      .getSelectedItems()
-      .forEach((correspondent) => {
-        filterRules.push({
-          rule_type: FILTER_CORRESPONDENT,
-          value: correspondent.id?.toString(),
+    if (this.correspondentSelectionModel.isNoneSelected()) {
+      filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
+    } else {
+      this.correspondentSelectionModel
+        .getSelectedItems()
+        .forEach((correspondent) => {
+          filterRules.push({
+            rule_type: FILTER_HAS_CORRESPONDENT_ANY,
+            value: correspondent.id?.toString(),
+          })
         })
-      })
-    this.documentTypeSelectionModel
-      .getSelectedItems()
-      .forEach((documentType) => {
-        filterRules.push({
-          rule_type: FILTER_DOCUMENT_TYPE,
-          value: documentType.id?.toString(),
+      this.correspondentSelectionModel
+        .getExcludedItems()
+        .forEach((correspondent) => {
+          filterRules.push({
+            rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+            value: correspondent.id?.toString(),
+          })
         })
-      })
-    this.storagePathSelectionModel.getSelectedItems().forEach((storagePath) => {
-      filterRules.push({
-        rule_type: FILTER_STORAGE_PATH,
-        value: storagePath.id?.toString(),
-      })
-    })
+    }
+    if (this.documentTypeSelectionModel.isNoneSelected()) {
+      filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
+    } else {
+      this.documentTypeSelectionModel
+        .getSelectedItems()
+        .forEach((documentType) => {
+          filterRules.push({
+            rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
+            value: documentType.id?.toString(),
+          })
+        })
+      this.documentTypeSelectionModel
+        .getExcludedItems()
+        .forEach((documentType) => {
+          filterRules.push({
+            rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+            value: documentType.id?.toString(),
+          })
+        })
+    }
+    if (this.storagePathSelectionModel.isNoneSelected()) {
+      filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
+    } else {
+      this.storagePathSelectionModel
+        .getSelectedItems()
+        .forEach((storagePath) => {
+          filterRules.push({
+            rule_type: FILTER_HAS_STORAGE_PATH_ANY,
+            value: storagePath.id?.toString(),
+          })
+        })
+      this.storagePathSelectionModel
+        .getExcludedItems()
+        .forEach((storagePath) => {
+          filterRules.push({
+            rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+            value: storagePath.id?.toString(),
+          })
+        })
+    }
     if (this.dateCreatedBefore) {
       filterRules.push({
         rule_type: FILTER_CREATED_BEFORE,
index d40b2fe0f0b69dbe94ce734c3362e9248d970ee7..d75d208ef64126a23a94a9902793d6f7ec7c6824 100644 (file)
@@ -1,6 +1,6 @@
 import { Component } from '@angular/core'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
-import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'
+import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type'
 import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
 import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
 import { DocumentListViewService } from 'src/app/services/document-list-view.service'
@@ -35,7 +35,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles
       toastService,
       documentListViewService,
       permissionsService,
-      FILTER_CORRESPONDENT,
+      FILTER_HAS_CORRESPONDENT_ANY,
       $localize`correspondent`,
       $localize`correspondents`,
       PermissionType.Correspondent,
index ca21e59d265aab062eb05795ac29f294732bb509..ee209e738ff17a20bba52f120b2358e87cdb8002 100644 (file)
@@ -1,6 +1,6 @@
 import { Component } from '@angular/core'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
-import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'
+import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type'
 import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
 import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 import {
@@ -32,7 +32,7 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless
       toastService,
       documentListViewService,
       permissionsService,
-      FILTER_DOCUMENT_TYPE,
+      FILTER_HAS_DOCUMENT_TYPE_ANY,
       $localize`document type`,
       $localize`document types`,
       PermissionType.DocumentType,
index d7c0ec21a110ba0bf3e7a4d4aa066f36af6d8ea1..043a9ad412494f31d041e08e8bd21960e0175a7b 100644 (file)
@@ -1,6 +1,6 @@
 import { Component } from '@angular/core'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
-import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type'
+import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type'
 import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
 import { DocumentListViewService } from 'src/app/services/document-list-view.service'
 import {
@@ -32,7 +32,7 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS
       toastService,
       documentListViewService,
       permissionsService,
-      FILTER_STORAGE_PATH,
+      FILTER_HAS_STORAGE_PATH_ANY,
       $localize`storage path`,
       $localize`storage paths`,
       PermissionType.StoragePath,
index 3c115e7727cd5ed8220772e03ffdc3ea7f47c09e..50b80b13b45abb1dbd5d797cd5066162febd4ba1 100644 (file)
@@ -8,8 +8,12 @@ export const FILTER_ASN_GT = 23
 export const FILTER_ASN_LT = 24
 
 export const FILTER_CORRESPONDENT = 3
+export const FILTER_HAS_CORRESPONDENT_ANY = 26
+export const FILTER_DOES_NOT_HAVE_CORRESPONDENT = 27
 
 export const FILTER_DOCUMENT_TYPE = 4
+export const FILTER_HAS_DOCUMENT_TYPE_ANY = 28
+export const FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE = 29
 
 export const FILTER_IS_IN_INBOX = 5
 export const FILTER_HAS_TAGS_ALL = 6
@@ -18,6 +22,8 @@ export const FILTER_DOES_NOT_HAVE_TAG = 17
 export const FILTER_HAS_TAGS_ANY = 22
 
 export const FILTER_STORAGE_PATH = 25
+export const FILTER_HAS_STORAGE_PATH_ANY = 30
+export const FILTER_DOES_NOT_HAVE_STORAGE_PATH = 31
 
 export const FILTER_CREATED_BEFORE = 8
 export const FILTER_CREATED_AFTER = 9
@@ -63,6 +69,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
     datatype: 'correspondent',
     multi: false,
   },
+  {
+    id: FILTER_HAS_CORRESPONDENT_ANY,
+    filtervar: 'correspondent__id__in',
+    datatype: 'correspondent',
+    multi: true,
+  },
+  {
+    id: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
+    filtervar: 'correspondent__id__none',
+    datatype: 'correspondent',
+    multi: true,
+  },
   {
     id: FILTER_STORAGE_PATH,
     filtervar: 'storage_path__id',
@@ -70,6 +88,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
     datatype: 'storage_path',
     multi: false,
   },
+  {
+    id: FILTER_HAS_STORAGE_PATH_ANY,
+    filtervar: 'storage_path__id__in',
+    datatype: 'storage_path',
+    multi: true,
+  },
+  {
+    id: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
+    filtervar: 'storage_path__id__none',
+    datatype: 'storage_path',
+    multi: true,
+  },
   {
     id: FILTER_DOCUMENT_TYPE,
     filtervar: 'document_type__id',
@@ -77,6 +107,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
     datatype: 'document_type',
     multi: false,
   },
+  {
+    id: FILTER_HAS_DOCUMENT_TYPE_ANY,
+    filtervar: 'document_type__id__in',
+    datatype: 'document_type',
+    multi: true,
+  },
+  {
+    id: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
+    filtervar: 'document_type__id__none',
+    datatype: 'document_type',
+    multi: true,
+  },
   {
     id: FILTER_IS_IN_INBOX,
     filtervar: 'is_in_inbox',
index 9694442be21ebd24e33d4f1641dc07d79b56daff..b98801f7b89f1a14995a2333765466a331eeaaef 100644 (file)
@@ -86,12 +86,12 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
     let params = {}
     for (let rule of filterRules) {
       let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
-      if (ruleType.multi) {
+      if (ruleType.isnull_filtervar && rule.value == null) {
+        params[ruleType.isnull_filtervar] = 1
+      } else if (ruleType.multi) {
         params[ruleType.filtervar] = params[ruleType.filtervar]
           ? params[ruleType.filtervar] + ',' + rule.value
           : rule.value
-      } else if (ruleType.isnull_filtervar && rule.value == null) {
-        params[ruleType.isnull_filtervar] = 1
       } else {
         params[ruleType.filtervar] = rule.value
         if (ruleType.datatype == 'boolean')
index ffedb22dad22970eccee0441ac9ca3de7c2350b0..271b9110851121dc480486af271470598b855b8f 100644 (file)
@@ -36,29 +36,30 @@ class DocumentTypeFilterSet(FilterSet):
         fields = {"name": CHAR_KWARGS}
 
 
-class TagsFilter(Filter):
-    def __init__(self, exclude=False, in_list=False):
+class ObjectFilter(Filter):
+    def __init__(self, exclude=False, in_list=False, field_name=""):
         super().__init__()
         self.exclude = exclude
         self.in_list = in_list
+        self.field_name = field_name
 
     def filter(self, qs, value):
         if not value:
             return qs
 
         try:
-            tag_ids = [int(x) for x in value.split(",")]
+            object_ids = [int(x) for x in value.split(",")]
         except ValueError:
             return qs
 
         if self.in_list:
-            qs = qs.filter(tags__id__in=tag_ids).distinct()
+            qs = qs.filter(**{f"{self.field_name}__id__in": object_ids}).distinct()
         else:
-            for tag_id in tag_ids:
+            for obj_id in object_ids:
                 if self.exclude:
-                    qs = qs.exclude(tags__id=tag_id)
+                    qs = qs.exclude(**{f"{self.field_name}__id": obj_id})
                 else:
-                    qs = qs.filter(tags__id=tag_id)
+                    qs = qs.filter(**{f"{self.field_name}__id": obj_id})
 
         return qs
 
@@ -90,11 +91,17 @@ class DocumentFilterSet(FilterSet):
         exclude=True,
     )
 
-    tags__id__all = TagsFilter()
+    tags__id__all = ObjectFilter(field_name="tags")
 
-    tags__id__none = TagsFilter(exclude=True)
+    tags__id__none = ObjectFilter(field_name="tags", exclude=True)
 
-    tags__id__in = TagsFilter(in_list=True)
+    tags__id__in = ObjectFilter(field_name="tags", in_list=True)
+
+    correspondent__id__none = ObjectFilter(field_name="correspondent", exclude=True)
+
+    document_type__id__none = ObjectFilter(field_name="document_type", exclude=True)
+
+    storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
 
     is_in_inbox = InboxFilter()
 
diff --git a/src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1034_alter_savedviewfilterrule_rule_type.py
new file mode 100644 (file)
index 0000000..aa56d96
--- /dev/null
@@ -0,0 +1,54 @@
+# Generated by Django 4.1.5 on 2023-03-15 07:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("documents", "1033_alter_documenttype_options_alter_tag_options_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="savedviewfilterrule",
+            name="rule_type",
+            field=models.PositiveIntegerField(
+                choices=[
+                    (0, "title contains"),
+                    (1, "content contains"),
+                    (2, "ASN is"),
+                    (3, "correspondent is"),
+                    (4, "document type is"),
+                    (5, "is in inbox"),
+                    (6, "has tag"),
+                    (7, "has any tag"),
+                    (8, "created before"),
+                    (9, "created after"),
+                    (10, "created year is"),
+                    (11, "created month is"),
+                    (12, "created day is"),
+                    (13, "added before"),
+                    (14, "added after"),
+                    (15, "modified before"),
+                    (16, "modified after"),
+                    (17, "does not have tag"),
+                    (18, "does not have ASN"),
+                    (19, "title or content contains"),
+                    (20, "fulltext query"),
+                    (21, "more like this"),
+                    (22, "has tags in"),
+                    (23, "ASN greater than"),
+                    (24, "ASN less than"),
+                    (25, "storage path is"),
+                    (26, "has correspondent in"),
+                    (27, "does not have correspondent in"),
+                    (28, "has document type in"),
+                    (29, "does not have document type in"),
+                    (30, "has storage path in"),
+                    (31, "does not have storage path in"),
+                ],
+                verbose_name="rule type",
+            ),
+        ),
+    ]
index 303abe7303ea03cb7e83889fd67d64814661e15f..c42cb4f91c91c23b60158888eda691684e2b62b9 100644 (file)
@@ -447,6 +447,12 @@ class SavedViewFilterRule(models.Model):
         (23, _("ASN greater than")),
         (24, _("ASN less than")),
         (25, _("storage path is")),
+        (26, _("has correspondent in")),
+        (27, _("does not have correspondent in")),
+        (28, _("has document type in")),
+        (29, _("does not have document type in")),
+        (30, _("has storage path in")),
+        (31, _("does not have storage path in")),
     ]
 
     saved_view = models.ForeignKey(