]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: support more 'not assigned' filtering, refactor (#9429)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 18 Mar 2025 06:35:03 +0000 (23:35 -0700)
committerGitHub <noreply@github.com>
Tue, 18 Mar 2025 06:35:03 +0000 (23:35 -0700)
12 files changed:
src-ui/messages.xlf
src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts
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/bulk-editor/bulk-editor.component.spec.ts
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
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.spec.ts
src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
src-ui/src/app/data/filter-rule-type.ts
src-ui/src/app/utils/query-params.spec.ts
src-ui/src/app/utils/query-params.ts

index 67e8be0fca11f0438bb1a1042bb2b8c9d0dfdf50..f609139d8207272ba1730627d3dd9cbc977e43d2 100644 (file)
@@ -5,14 +5,14 @@
       <trans-unit id="ngb.alert.close" datatype="html">
         <source>Close</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/alert/alert.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/alert/alert.ts</context>
           <context context-type="linenumber">51</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.carousel.slide-number" datatype="html">
         <source> Slide <x id="INTERPOLATION" equiv-text="ueryList&lt;NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/carousel/carousel.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/carousel/carousel.ts</context>
           <context context-type="linenumber">132,136</context>
         </context-group>
         <note priority="1" from="description">Currently selected slide number read by screen reader</note>
       <trans-unit id="ngb.carousel.previous" datatype="html">
         <source>Previous</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/carousel/carousel.ts</context>
-          <context context-type="linenumber">148,149</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/carousel/carousel.ts</context>
+          <context context-type="linenumber">156,160</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.carousel.next" datatype="html">
         <source>Next</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/carousel/carousel.ts</context>
-          <context context-type="linenumber">167,170</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/carousel/carousel.ts</context>
+          <context context-type="linenumber">196,199</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.datepicker.previous-month" datatype="html">
         <source>Previous month</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/datepicker/datepicker-navigation.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/datepicker/datepicker-navigation.ts</context>
           <context context-type="linenumber">77,79</context>
         </context-group>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/datepicker/datepicker-navigation.ts</context>
-          <context context-type="linenumber">97,98</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/datepicker/datepicker-navigation.ts</context>
+          <context context-type="linenumber">102</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.datepicker.next-month" datatype="html">
         <source>Next month</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/datepicker/datepicker-navigation.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/datepicker/datepicker-navigation.ts</context>
           <context context-type="linenumber">102</context>
         </context-group>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/datepicker/datepicker-navigation.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/datepicker/datepicker-navigation.ts</context>
           <context context-type="linenumber">102</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.HH" datatype="html">
         <source>HH</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.toast.close-aria" datatype="html">
         <source>Close</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.datepicker.select-month" datatype="html">
         <source>Select month</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.pagination.first" datatype="html">
         <source>««</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.hours" datatype="html">
         <source>Hours</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.pagination.previous" datatype="html">
         <source>«</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.MM" datatype="html">
         <source>MM</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.pagination.next" datatype="html">
         <source>»</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.datepicker.select-year" datatype="html">
         <source>Select year</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.minutes" datatype="html">
         <source>Minutes</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.pagination.last" datatype="html">
         <source>»»</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.pagination.first-aria" datatype="html">
         <source>First</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.increment-hours" datatype="html">
         <source>Increment hours</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.pagination.previous-aria" datatype="html">
         <source>Previous</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
         <source>Decrement hours</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.pagination.next-aria" datatype="html">
         <source>Next</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
         <source>Increment minutes</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.pagination.last-aria" datatype="html">
         <source>Last</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
         <source>Decrement minutes</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.SS" datatype="html">
         <source>SS</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.seconds" datatype="html">
         <source>Seconds</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
         <source>Increment seconds</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
         <source>Decrement seconds</source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
       <trans-unit id="ngb.timepicker.PM" datatype="html">
         <source><x id="INTERPOLATION"/></source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/ngb-config.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/ngb-config.ts</context>
           <context context-type="linenumber">13</context>
         </context-group>
       </trans-unit>
         <source><x id="INTERPOLATION" equiv-text="barConfig);
        pu"/></source>
         <context-group purpose="location">
-          <context context-type="sourcefile">node_modules/src/progressbar/progressbar.ts</context>
+          <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.0_@angular+core@19.2.0_rxjs@7.8._5a874a4dc94176c096bfcabea2b4273a/node_modules/src/progressbar/progressbar.ts</context>
           <context context-type="linenumber">41,42</context>
         </context-group>
       </trans-unit>
         </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">193</context>
+          <context context-type="linenumber">190</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8508424367627989968" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">104</context>
+          <context context-type="linenumber">97</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">106</context>
+          <context context-type="linenumber">101</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">110</context>
+          <context context-type="linenumber">103</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">164</context>
+          <context context-type="linenumber">157</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</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">796</context>
+          <context context-type="linenumber">794</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">829</context>
+          <context context-type="linenumber">827</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">848</context>
+          <context context-type="linenumber">846</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">136</context>
+          <context context-type="linenumber">129</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>
         </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">439</context>
+          <context context-type="linenumber">437</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">479</context>
+          <context context-type="linenumber">477</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">517</context>
+          <context context-type="linenumber">515</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">555</context>
+          <context context-type="linenumber">553</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">617</context>
+          <context context-type="linenumber">615</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">750</context>
+          <context context-type="linenumber">748</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1407560924967345762" 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">392</context>
+          <context context-type="linenumber">81</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>
         <source>Open <x id="PH" equiv-text="this.title"/> filter</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
-          <context context-type="linenumber">513</context>
+          <context context-type="linenumber">554</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7005745151564974365" datatype="html">
         </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">384</context>
+          <context context-type="linenumber">382</context>
         </context-group>
         <note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note>
       </trans-unit>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">114</context>
+          <context context-type="linenumber">107</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1418444397960583910" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">117</context>
+          <context context-type="linenumber">110</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4399672576012609374" 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">180</context>
+          <context context-type="linenumber">177</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/document.ts</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">37</context>
+          <context context-type="linenumber">35</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-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">52</context>
+          <context context-type="linenumber">50</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/document.ts</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">52</context>
+          <context context-type="linenumber">49</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-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">64</context>
+          <context context-type="linenumber">61</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/document.ts</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">67</context>
+          <context context-type="linenumber">63</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-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">76</context>
+          <context context-type="linenumber">72</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/document.ts</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">752</context>
+          <context context-type="linenumber">750</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2048798344356757326" datatype="html">
         </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">754</context>
+          <context context-type="linenumber">752</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7295637485862454066" datatype="html">
         </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">792</context>
+          <context context-type="linenumber">790</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2951161989614003846" datatype="html">
         </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">826</context>
+          <context context-type="linenumber">824</context>
         </context-group>
       </trans-unit>
       <trans-unit id="857641176955257111" datatype="html">
         <source>Filter correspondents</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">38</context>
+          <context context-type="linenumber">36</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">53</context>
+          <context context-type="linenumber">51</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2947613869920454977" datatype="html">
         <source>Filter document types</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">53</context>
+          <context context-type="linenumber">50</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">65</context>
+          <context context-type="linenumber">62</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8816999377397522522" datatype="html">
         <source>Filter storage paths</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">68</context>
+          <context context-type="linenumber">64</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">77</context>
+          <context context-type="linenumber">73</context>
         </context-group>
       </trans-unit>
       <trans-unit id="9149498548977462220" datatype="html">
         <source>Custom fields</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">82</context>
+          <context context-type="linenumber">77</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">89</context>
+          <context context-type="linenumber">84</context>
         </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">188</context>
+          <context context-type="linenumber">185</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6475890479659129881" datatype="html">
         <source>Filter custom fields</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">83</context>
+          <context context-type="linenumber">78</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5139192806922838657" datatype="html">
         <source>Set values</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">93</context>
+          <context context-type="linenumber">86</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3206542606001340679" datatype="html">
         <source>Merge</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">120</context>
+          <context context-type="linenumber">113</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1015374532025907183" datatype="html">
         <source>Include:</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">142</context>
+          <context context-type="linenumber">135</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1537670659786159738" datatype="html">
         <source>Archived files</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">146</context>
+          <context context-type="linenumber">139</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2520291319362448498" datatype="html">
         <source>Original files</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">150</context>
+          <context context-type="linenumber">143</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8009862506882713059" datatype="html">
         <source>Use formatted filename</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
-          <context context-type="linenumber">155</context>
+          <context context-type="linenumber">148</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1215215387232313677" datatype="html">
         <source>Error executing bulk operation</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">288</context>
+          <context context-type="linenumber">286</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7894972847287473517" datatype="html">
         <source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot;</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">376</context>
+          <context context-type="linenumber">374</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">382</context>
+          <context context-type="linenumber">380</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8639884465898458690" datatype="html">
         <source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">378</context>
+          <context context-type="linenumber">376</context>
         </context-group>
         <note priority="1" from="description">This is for messages like &apos;modify &quot;tag1&quot; and &quot;tag2&quot;&apos;</note>
       </trans-unit>
         <source><x id="PH" equiv-text="list"/> and &quot;<x id="PH_1" equiv-text="items[items.length - 1].name"/>&quot;</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">386,388</context>
+          <context context-type="linenumber">384,386</context>
         </context-group>
         <note priority="1" from="description">this is for messages like &apos;modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;&apos;</note>
       </trans-unit>
         <source>Confirm tags assignment</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">403</context>
+          <context context-type="linenumber">401</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6619516195038467207" datatype="html">
         <source>This operation will add the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">409</context>
+          <context context-type="linenumber">407</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1894412783609570695" datatype="html">
         )"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">414,416</context>
+          <context context-type="linenumber">412,414</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7181166515756808573" datatype="html">
         <source>This operation will remove the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">422</context>
+          <context context-type="linenumber">420</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3819792277998068944" datatype="html">
         )"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">427,429</context>
+          <context context-type="linenumber">425,427</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2739066218579571288" datatype="html">
         )"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">431,435</context>
+          <context context-type="linenumber">429,433</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2996713129519325161" datatype="html">
         <source>Confirm correspondent assignment</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">472</context>
+          <context context-type="linenumber">470</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6900893559485781849" datatype="html">
         <source>This operation will assign the correspondent &quot;<x id="PH" equiv-text="correspondent.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">474</context>
+          <context context-type="linenumber">472</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1257522660364398440" datatype="html">
         <source>This operation will remove the correspondent from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">476</context>
+          <context context-type="linenumber">474</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5393409374423140648" datatype="html">
         <source>Confirm document type assignment</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">510</context>
+          <context context-type="linenumber">508</context>
         </context-group>
       </trans-unit>
       <trans-unit id="332180123895325027" datatype="html">
         <source>This operation will assign the document type &quot;<x id="PH" equiv-text="documentType.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">512</context>
+          <context context-type="linenumber">510</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2236642492594872779" datatype="html">
         <source>This operation will remove the document type from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">514</context>
+          <context context-type="linenumber">512</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6386555513013840736" datatype="html">
         <source>Confirm storage path assignment</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">548</context>
+          <context context-type="linenumber">546</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8750527458618415924" datatype="html">
         <source>This operation will assign the storage path &quot;<x id="PH" equiv-text="storagePath.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">550</context>
+          <context context-type="linenumber">548</context>
         </context-group>
       </trans-unit>
       <trans-unit id="60728365335056946" datatype="html">
         <source>This operation will remove the storage path from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">552</context>
+          <context context-type="linenumber">550</context>
         </context-group>
       </trans-unit>
       <trans-unit id="4187352575310415704" datatype="html">
         <source>Confirm custom field assignment</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">581</context>
+          <context context-type="linenumber">579</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7966494636326273856" datatype="html">
         <source>This operation will assign the custom field &quot;<x id="PH" equiv-text="customField.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">587</context>
+          <context context-type="linenumber">585</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5789455969634598553" datatype="html">
         )"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">592,594</context>
+          <context context-type="linenumber">590,592</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5648572354333199245" datatype="html">
         <source>This operation will remove the custom field &quot;<x id="PH" equiv-text="customField.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">600</context>
+          <context context-type="linenumber">598</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6666899594015948817" datatype="html">
         )"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">605,607</context>
+          <context context-type="linenumber">603,605</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8050047262594964176" datatype="html">
         )"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">609,613</context>
+          <context context-type="linenumber">607,611</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8615059324209654051" datatype="html">
         <source>Move <x id="PH" equiv-text="this.list.selected.size"/> selected document(s) to the trash?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">751</context>
+          <context context-type="linenumber">749</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8585195717323764335" datatype="html">
         <source>This operation will permanently recreate the archive files for <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">793</context>
+          <context context-type="linenumber">791</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7366623494074776040" datatype="html">
         <source>The archive files will be re-generated with the current settings.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">794</context>
+          <context context-type="linenumber">792</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6390006284731990222" datatype="html">
         <source>This operation will permanently rotate the original version of <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">827</context>
+          <context context-type="linenumber">825</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7910756456450124185" datatype="html">
         <source>Merge confirm</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">846</context>
+          <context context-type="linenumber">844</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7643543647233874431" datatype="html">
         <source>This operation will merge <x id="PH" equiv-text="this.list.selected.size"/> selected documents into a new document.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">847</context>
+          <context context-type="linenumber">845</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7869008840945899895" datatype="html">
         <source>Merged document will be queued for consumption.</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
-          <context context-type="linenumber">866</context>
+          <context context-type="linenumber">864</context>
         </context-group>
       </trans-unit>
       <trans-unit id="476913782630693351" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">112</context>
+          <context context-type="linenumber">107</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1559883523769732271" 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">185</context>
+          <context context-type="linenumber">182</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/data/document.ts</context>
         <source>Dates</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
-          <context context-type="linenumber">95</context>
+          <context context-type="linenumber">90</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">183</context>
+          <context context-type="linenumber">180</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7408932238599462499" datatype="html">
         <source>File 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">190</context>
+          <context context-type="linenumber">187</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">199</context>
+          <context context-type="linenumber">196</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">205</context>
+          <context context-type="linenumber">202</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">209</context>
+          <context context-type="linenumber">206</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">213</context>
+          <context context-type="linenumber">210</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">217</context>
+          <context context-type="linenumber">214</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">221</context>
+          <context context-type="linenumber">218</context>
         </context-group>
       </trans-unit>
       <trans-unit id="5195932016807797291" datatype="html">
-        <source>Correspondent: <x id="PH" equiv-text="this.correspondents.find((c) =&gt; c.id == +rule.value)?.name"/></source>
+        <source>Correspondent: <x id="PH" equiv-text="this.correspondentSelectionModel.items.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">253,255</context>
+          <context context-type="linenumber">250,254</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">257</context>
+          <context context-type="linenumber">256</context>
         </context-group>
       </trans-unit>
       <trans-unit id="317796810569008208" datatype="html">
-        <source>Document type: <x id="PH" equiv-text="this.documentTypes.find((dt) =&gt; dt.id == +rule.value)?.name"/></source>
+        <source>Document type: <x id="PH" equiv-text="this.documentTypeSelectionModel.items.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">263,265</context>
+          <context context-type="linenumber">262,266</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">267</context>
+          <context context-type="linenumber">268</context>
         </context-group>
       </trans-unit>
       <trans-unit id="232202047340644471" datatype="html">
-        <source>Storage path: <x id="PH" equiv-text="this.storagePaths.find((sp) =&gt; sp.id == +rule.value)?.name"/></source>
+        <source>Storage path: <x id="PH" equiv-text="this.storagePathSelectionModel.items.find(
+                (sp) =&gt; sp.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">273,275</context>
+          <context context-type="linenumber">274,278</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1562820715074533164" datatype="html">
         <source>Without storage path</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">277</context>
+          <context context-type="linenumber">280</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>
+        <source>Tag: <x id="PH" equiv-text="this.tagSelectionModel.items.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">281,283</context>
+          <context context-type="linenumber">284,286</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">287</context>
+          <context context-type="linenumber">290</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8644099678903817943" datatype="html">
         <source>Custom fields query</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">291</context>
+          <context context-type="linenumber">294</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">294</context>
+          <context context-type="linenumber">297</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">297</context>
+          <context context-type="linenumber">300</context>
         </context-group>
       </trans-unit>
       <trans-unit id="102674688969746976" datatype="html">
         <source>Owner: <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">300</context>
+          <context context-type="linenumber">303</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3550877650686009106" datatype="html">
         <source>Owner not in: <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">303</context>
+          <context context-type="linenumber">306</context>
         </context-group>
       </trans-unit>
       <trans-unit id="1082034558646673343" datatype="html">
         <source>Without an owner</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">306</context>
+          <context context-type="linenumber">309</context>
         </context-group>
       </trans-unit>
       <trans-unit id="7210076240260527720" datatype="html">
index 6ba15eacd30679ad220463aece08b5ce1171e56a..cd279b1b5321a0fa9c5614e524aa98c4872ac8f7 100644 (file)
@@ -7,6 +7,7 @@ import {
   tick,
 } from '@angular/core/testing'
 import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
+import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
 import {
   DEFAULT_MATCHING_ALGORITHM,
   MATCH_ALL,
@@ -44,6 +45,11 @@ const nullItem = {
   name: 'Not assigned',
 }
 
+const negativeNullItem = {
+  id: NEGATIVE_NULL_FILTER_VALUE,
+  name: 'Not assigned',
+}
+
 let selectionModel: FilterableDropdownSelectionModel
 
 describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
@@ -64,6 +70,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
     hotkeyService = TestBed.inject(HotKeyService)
     fixture = TestBed.createComponent(FilterableDropdownComponent)
     component = fixture.componentInstance
+    component.selectionModel = new FilterableDropdownSelectionModel()
     selectionModel = new FilterableDropdownSelectionModel()
   })
 
@@ -74,7 +81,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should support reset', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.selectionModel = selectionModel
     selectionModel.set(items[0].id, ToggleableItemState.Selected)
     expect(selectionModel.getSelectedItems()).toHaveLength(1)
@@ -96,7 +103,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should emit change when items selected', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.selectionModel = selectionModel
     let newModel: FilterableDropdownSelectionModel
     component.selectionModelChange.subscribe((model) => (newModel = model))
@@ -110,11 +117,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
     selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
     expect(newModel.getSelectedItems()).toEqual([])
 
-    expect(component.items).toEqual([nullItem, ...items])
+    expect(component.selectionModel.items).toEqual([nullItem, ...items])
   })
 
   it('should emit change when items excluded', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.selectionModel = selectionModel
     let newModel: FilterableDropdownSelectionModel
     component.selectionModelChange.subscribe((model) => (newModel = model))
@@ -124,7 +131,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should emit change when items excluded', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.selectionModel = selectionModel
     let newModel: FilterableDropdownSelectionModel
     component.selectionModelChange.subscribe((model) => (newModel = model))
@@ -139,8 +146,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should exclude items when excluded and not editing', () => {
-    component.items = items
-    component.manyToOne = true
+    component.selectionModel.items = items
+    component.selectionModel.manyToOne = true
     component.selectionModel = selectionModel
     selectionModel.set(items[0].id, ToggleableItemState.Selected)
     component.excludeClicked(items[0].id)
@@ -149,8 +156,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should toggle when items excluded and editing', () => {
-    component.items = items
-    component.manyToOne = true
+    component.selectionModel.items = items
+    component.selectionModel.manyToOne = true
     component.editing = true
     component.selectionModel = selectionModel
     selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
@@ -160,8 +167,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should hide count for item if adding will increase size of set', () => {
-    component.items = items
-    component.manyToOne = true
+    component.selectionModel.items = items
+    component.selectionModel.manyToOne = true
     component.selectionModel = selectionModel
     expect(component.hideCount(items[0])).toBeFalsy()
     selectionModel.logicalOperator = LogicalOperator.Or
@@ -170,7 +177,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
 
   it('should enforce single select when editing', () => {
     component.editing = true
-    component.items = items
+    component.selectionModel.items = items
     component.selectionModel = selectionModel
     let newModel: FilterableDropdownSelectionModel
     component.selectionModelChange.subscribe((model) => (newModel = model))
@@ -182,11 +189,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should support manyToOne selecting', () => {
-    component.items = items
+    component.selectionModel.items = items
     selectionModel.manyToOne = false
     component.selectionModel = selectionModel
-    component.manyToOne = true
-    expect(component.manyToOne).toBeTruthy()
+    component.selectionModel.manyToOne = true
+    expect(component.selectionModel.manyToOne).toBeTruthy()
     let newModel: FilterableDropdownSelectionModel
     component.selectionModelChange.subscribe((model) => (newModel = model))
 
@@ -197,12 +204,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should dynamically enable / disable modifier toggle', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.selectionModel = selectionModel
     expect(component.modifierToggleEnabled).toBeTruthy()
-    selectionModel.toggle(null)
-    expect(component.modifierToggleEnabled).toBeFalsy()
-    component.manyToOne = true
+    component.selectionModel.manyToOne = true
     expect(component.modifierToggleEnabled).toBeFalsy()
     selectionModel.toggle(items[0].id)
     selectionModel.toggle(items[1].id)
@@ -210,7 +215,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should apply changes and close when apply button clicked', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     component.editing = true
     component.selectionModel = selectionModel
@@ -232,7 +237,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should apply on close if enabled', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     component.editing = true
     component.applyOnClose = true
@@ -250,7 +255,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     fixture.nativeElement
       .querySelector('button')
@@ -277,7 +282,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   }))
 
   it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     expect(component.selectionModel.getSelectedItems()).toEqual([])
     fixture.nativeElement
@@ -297,7 +302,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   }))
 
   it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     component.editing = true
     let applyResult: ChangedItems
@@ -319,7 +324,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   }))
 
   it('should support arrow keyboard navigation', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     fixture.nativeElement
       .querySelector('button')
@@ -364,7 +369,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   }))
 
   it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     fixture.nativeElement
       .querySelector('button')
@@ -400,7 +405,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   }))
 
   it('should support arrow keyboard navigation after click', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     fixture.nativeElement
       .querySelector('button')
@@ -425,9 +430,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   }))
 
   it('should toggle logical operator', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
-    component.manyToOne = true
+    component.selectionModel.manyToOne = true
     selectionModel.set(items[0].id, ToggleableItemState.Selected)
     selectionModel.set(items[1].id, ToggleableItemState.Selected)
     component.selectionModel = selectionModel
@@ -454,7 +459,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   }))
 
   it('should toggle intersection include / exclude', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     selectionModel.set(items[0].id, ToggleableItemState.Selected)
     selectionModel.set(items[1].id, ToggleableItemState.Selected)
@@ -483,22 +488,55 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
     expect(changedResult.getExcludedItems()).toEqual(items)
   }))
 
+  it('should update null item selection on toggleIntersection', () => {
+    component.selectionModel.items = items
+    component.selectionModel = selectionModel
+    component.selectionModel.intersection = Intersection.Include
+    console.log(component.selectionModel.items[0])
+    component.selectionModel.set(null, ToggleableItemState.Selected)
+    component.selectionModel.intersection = Intersection.Exclude
+    component.selectionModel.toggleIntersection()
+    console.log(component.selectionModel)
+    expect(component.selectionModel.getExcludedItems()).toEqual([
+      negativeNullItem,
+    ])
+
+    component.selectionModel.intersection = Intersection.Include
+    component.selectionModel.toggleIntersection()
+    expect(component.selectionModel.getSelectedItems()).toEqual([nullItem])
+  })
+
   it('selection model should sort items by state', () => {
-    component.items = items.concat([{ id: null, name: 'Null B' }])
     component.selectionModel = selectionModel
+    component.selectionModel.items = items.concat([{ id: 3, name: 'Item3' }])
     selectionModel.toggle(items[1].id)
     selectionModel.apply()
+    expect(selectionModel.items.length).toEqual(4)
     expect(selectionModel.items).toEqual([
       nullItem,
-      { id: null, name: 'Null B' },
       items[1],
+      { id: 3, name: 'Item3' },
       items[0],
     ])
+
+    selectionModel.intersection = Intersection.Exclude
+    selectionModel.toggleIntersection()
+    selectionModel.apply()
+    expect(selectionModel.items).toEqual([
+      negativeNullItem,
+      items[1],
+      { id: 3, name: 'Item3' },
+      items[0],
+    ])
+
+    // coverage
+    selectionModel.items = selectionModel.items.reverse()
+    selectionModel.apply()
   })
 
   it('selection model should sort items by state and document counts = 0, if set', () => {
     const tagA = { id: 4, name: 'Tag A' }
-    component.items = items.concat([tagA])
+    component.selectionModel.items = items.concat([tagA])
     component.selectionModel = selectionModel
     component.documentCounts = [
       { id: 1, document_count: 0 }, // Tag1
@@ -529,7 +567,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should set support create, keep open model and call createRef method', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     component.selectionModel = selectionModel
     fixture.nativeElement
@@ -549,7 +587,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   }))
 
   it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     component.editing = true
     component.createRef = jest.fn()
@@ -569,7 +607,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
     const id = 1
     const state = ToggleableItemState.Selected
     component.selectionModel = selectionModel
-    component.manyToOne = true
+    component.selectionModel.manyToOne = true
     component.selectionModel.singleSelect = true
     component.selectionModel.intersection = Intersection.Include
     component.selectionModel['temporarySelectionStates'].set(id, state)
@@ -596,7 +634,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should support shortcut keys', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     component.shortcutKey = 't'
     fixture.detectChanges()
@@ -606,7 +644,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
   })
 
   it('should support an extra button and not apply changes when clicked', () => {
-    component.items = items
+    component.selectionModel.items = items
     component.icon = 'tag-fill'
     component.extraButtonTitle = 'Extra'
     component.selectionModel = selectionModel
index 34320003e2049c9989b5fcb5d79f1f0315a8f581..45c776df699149a8bd9415808ddd190eacc89748 100644 (file)
@@ -12,6 +12,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
 import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
 import { Subject, filter, takeUntil } from 'rxjs'
+import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
 import { MatchingModel } from 'src/app/data/matching-model'
 import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
 import { FilterPipe } from 'src/app/pipes/filter.pipe'
@@ -61,15 +62,56 @@ export class FilterableDropdownSelectionModel {
   }
 
   set items(items: MatchingModel[]) {
-    this._items = items
-    this.sortItems()
+    if (items) {
+      this._items = Array.from(items)
+      this.sortItems()
+      this.setNullItem()
+    }
+  }
+
+  private setNullItem() {
+    if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
+      if (this._items[0]?.id === null) {
+        this._items.shift()
+      }
+      return
+    }
+
+    const item = {
+      name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
+      id:
+        this.manyToOne || this.intersection === Intersection.Include
+          ? null
+          : NEGATIVE_NULL_FILTER_VALUE,
+    }
+
+    if (
+      this._items[0]?.id === null ||
+      this._items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
+    ) {
+      this._items[0] = item
+    } else if (this._items) {
+      this._items.unshift(item)
+    }
+  }
+
+  constructor(manyToOne: boolean = false) {
+    this.manyToOne = manyToOne
   }
 
   private sortItems() {
     this._items.sort((a, b) => {
-      if (a.id == null && b.id != null) {
+      if (
+        (a.id == null && b.id != null) ||
+        (a.id == NEGATIVE_NULL_FILTER_VALUE &&
+          b.id != NEGATIVE_NULL_FILTER_VALUE)
+      ) {
         return -1
-      } else if (a.id != null && b.id == null) {
+      } else if (
+        (a.id != null && b.id == null) ||
+        (a.id != NEGATIVE_NULL_FILTER_VALUE &&
+          b.id == NEGATIVE_NULL_FILTER_VALUE)
+      ) {
         return 1
       } else if (
         this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
@@ -230,6 +272,7 @@ export class FilterableDropdownSelectionModel {
 
   set logicalOperator(operator: LogicalOperator) {
     this.temporaryLogicalOperator = operator
+    this.setNullItem()
   }
 
   toggleOperator() {
@@ -242,6 +285,7 @@ export class FilterableDropdownSelectionModel {
 
   set intersection(intersection: Intersection) {
     this.temporaryIntersection = intersection
+    this.setNullItem()
   }
 
   toggleIntersection() {
@@ -250,9 +294,20 @@ export class FilterableDropdownSelectionModel {
       this.intersection == Intersection.Include
         ? ToggleableItemState.Selected
         : ToggleableItemState.Excluded
+
     this.temporarySelectionStates.forEach((state, key) => {
-      this.temporarySelectionStates.set(key, newState)
+      if (key === null && this.intersection === Intersection.Exclude) {
+        this.temporarySelectionStates.set(NEGATIVE_NULL_FILTER_VALUE, newState)
+      } else if (
+        key === NEGATIVE_NULL_FILTER_VALUE &&
+        this.intersection === Intersection.Include
+      ) {
+        this.temporarySelectionStates.set(null, newState)
+      } else {
+        this.temporarySelectionStates.set(key, newState)
+      }
     })
+
     this.changed.next(this)
   }
 
@@ -274,6 +329,7 @@ export class FilterableDropdownSelectionModel {
     this.temporarySelectionStates.clear()
     this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
     this.temporaryIntersection = this._intersection = Intersection.Include
+    this.setNullItem()
     if (fireEvent) {
       this.changed.next(this)
     }
@@ -305,8 +361,10 @@ export class FilterableDropdownSelectionModel {
 
   isNoneSelected() {
     return (
-      this.selectionSize() == 1 &&
-      this.get(null) == ToggleableItemState.Selected
+      (this.selectionSize() == 1 &&
+        this.get(null) == ToggleableItemState.Selected) ||
+      (this.intersection == Intersection.Exclude &&
+        this.get(NEGATIVE_NULL_FILTER_VALUE) == ToggleableItemState.Excluded)
     )
   }
 
@@ -384,25 +442,13 @@ export class FilterableDropdownComponent
 
   filterText: string
 
-  @Input()
-  set items(items: MatchingModel[]) {
-    if (items) {
-      this._selectionModel.items = Array.from(items)
-      this._selectionModel.items.unshift({
-        name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
-        id: null,
-      })
-    }
-  }
+  _selectionModel: FilterableDropdownSelectionModel
 
   get items(): MatchingModel[] {
     return this._selectionModel.items
   }
 
-  _selectionModel: FilterableDropdownSelectionModel =
-    new FilterableDropdownSelectionModel()
-
-  @Input()
+  @Input({ required: true })
   set selectionModel(model: FilterableDropdownSelectionModel) {
     if (this.selectionModel) {
       this.selectionModel.changed.complete()
@@ -423,11 +469,6 @@ export class FilterableDropdownComponent
   @Output()
   selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
 
-  @Input()
-  set manyToOne(manyToOne: boolean) {
-    this.selectionModel.manyToOne = manyToOne
-  }
-
   get manyToOne() {
     return this.selectionModel.manyToOne
   }
@@ -484,7 +525,7 @@ export class FilterableDropdownComponent
     return this.manyToOne
       ? this.selectionModel.selectionSize() > 1 &&
           this.selectionModel.getExcludedItems().length == 0
-      : !this.selectionModel.isNoneSelected()
+      : true
   }
 
   get name(): string {
index ac8f476c7d3f94fe7862767827d40e2512669ebf..0eb655a21d00345248731d8f5f2edcfa33b1750e 100644 (file)
           @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
             <pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
               filterPlaceholder="Filter tags" i18n-filterPlaceholder
-              [items]="tags"
               [disabled]="!userCanEditAll || disabled"
               [editing]="true"
-              [manyToOne]="true"
               [applyOnClose]="applyOnClose"
               [createRef]="createTag.bind(this)"
               (opened)="openTagsDropdown()"
@@ -36,7 +34,6 @@
           @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
             <pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
               filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
-              [items]="correspondents"
               [disabled]="!userCanEditAll || disabled"
               [editing]="true"
               [applyOnClose]="applyOnClose"
@@ -51,7 +48,6 @@
           @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
             <pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
               filterPlaceholder="Filter document types" i18n-filterPlaceholder
-              [items]="documentTypes"
               [disabled]="!userCanEditAll || disabled"
               [editing]="true"
               [applyOnClose]="applyOnClose"
@@ -66,7 +62,6 @@
           @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
             <pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
               filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
-              [items]="storagePaths"
               [disabled]="!userCanEditAll || disabled"
               [editing]="true"
               [applyOnClose]="applyOnClose"
           @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
             <pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
               filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
-              [items]="customFields"
               [disabled]="!userCanEditAll"
               [editing]="true"
-              [manyToOne]="true"
               [applyOnClose]="applyOnClose"
               [createRef]="createCustomField.bind(this)"
               (opened)="openCustomFieldsDropdown()"
index 8f89e541a30d32b392efadc1c902cd6648c92983..3fddb4b689050e9cdcc2087d2bf0bee1f835210f 100644 (file)
@@ -1150,10 +1150,10 @@ describe('BulkEditorComponent', () => {
 
   it('should not attempt to retrieve objects if user does not have permissions', () => {
     jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
-    expect(component.tags).toBeUndefined()
-    expect(component.correspondents).toBeUndefined()
-    expect(component.documentTypes).toBeUndefined()
-    expect(component.storagePaths).toBeUndefined()
+    expect(component.tagSelectionModel.items.length).toEqual(0)
+    expect(component.correspondentSelectionModel.items.length).toEqual(0)
+    expect(component.documentTypeSelectionModel.items.length).toEqual(0)
+    expect(component.storagePathsSelectionModel.items.length).toEqual(0)
     httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
     httpTestingController.expectNone(
       `${environment.apiBaseUrl}documents/correspondents/`
@@ -1204,7 +1204,9 @@ describe('BulkEditorComponent', () => {
     expect(tagListAllSpy).toHaveBeenCalled()
 
     expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id)
-    expect(component.tags).toEqual(tags.results)
+    expect(component.tagSelectionModel.items).toEqual(
+      [{ id: null, name: 'Not assigned' }].concat(tags.results as any)
+    )
   })
 
   it('should support create new correspondent', () => {
@@ -1251,7 +1253,9 @@ describe('BulkEditorComponent', () => {
     expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith(
       newCorrespondent.id
     )
-    expect(component.correspondents).toEqual(correspondents.results)
+    expect(component.correspondentSelectionModel.items).toEqual(
+      [{ id: null, name: 'Not assigned' }].concat(correspondents.results as any)
+    )
   })
 
   it('should support create new document type', () => {
@@ -1295,7 +1299,9 @@ describe('BulkEditorComponent', () => {
     expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith(
       newDocumentType.id
     )
-    expect(component.documentTypes).toEqual(documentTypes.results)
+    expect(component.documentTypeSelectionModel.items).toEqual(
+      [{ id: null, name: 'Not assigned' }].concat(documentTypes.results as any)
+    )
   })
 
   it('should support create new storage path', () => {
@@ -1339,7 +1345,9 @@ describe('BulkEditorComponent', () => {
     expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith(
       newStoragePath.id
     )
-    expect(component.storagePaths).toEqual(storagePaths.results)
+    expect(component.storagePathsSelectionModel.items).toEqual(
+      [{ id: null, name: 'Not assigned' }].concat(storagePaths.results as any)
+    )
   })
 
   it('should support create new custom field', () => {
@@ -1391,7 +1399,9 @@ describe('BulkEditorComponent', () => {
     expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith(
       newCustomField.id
     )
-    expect(component.customFields).toEqual(customFields.results)
+    expect(component.customFieldsSelectionModel.items).toEqual(
+      [{ id: null, name: 'Not assigned' }].concat(customFields.results as any)
+    )
   })
 
   it('should open the bulk edit custom field values dialog with correct parameters', () => {
@@ -1416,17 +1426,17 @@ describe('BulkEditorComponent', () => {
     const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
     const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
 
-    component.customFields = [
+    component.customFieldsSelectionModel.items = [
       { id: 1, name: 'Custom Field 1', data_type: CustomFieldDataType.String },
       { id: 2, name: 'Custom Field 2', data_type: CustomFieldDataType.String },
-    ]
+    ] as any
 
     component.setCustomFieldValues({
       itemsToAdd: [{ id: 1 }, { id: 2 }],
       itemsToRemove: [1],
     } as any)
 
-    expect(modal.componentInstance.customFields).toEqual(component.customFields)
+    expect(modal.componentInstance.customFields.length).toEqual(2)
     expect(modal.componentInstance.fieldsToAddIds).toEqual([1, 2])
     expect(modal.componentInstance.documents).toEqual([3, 4])
 
index aa32497f31d3d23db37041840c9f3833946f855c..bf6d06cd4d835962459b8f40bfad83242d92f3fc 100644 (file)
@@ -14,12 +14,8 @@ import { saveAs } from 'file-saver'
 import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
 import { first, map, Subject, switchMap, takeUntil } from 'rxjs'
 import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
-import { Correspondent } from 'src/app/data/correspondent'
 import { CustomField } from 'src/app/data/custom-field'
-import { DocumentType } from 'src/app/data/document-type'
 import { MatchingModel } from 'src/app/data/matching-model'
-import { StoragePath } from 'src/app/data/storage-path'
-import { Tag } from 'src/app/data/tag'
 import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
 import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
 import { DocumentListViewService } from 'src/app/services/document-list-view.service'
@@ -75,17 +71,11 @@ export class BulkEditorComponent
   extends ComponentWithPermissions
   implements OnInit, OnDestroy
 {
-  tags: Tag[]
-  correspondents: Correspondent[]
-  documentTypes: DocumentType[]
-  storagePaths: StoragePath[]
-  customFields: CustomField[]
-
-  tagSelectionModel = new FilterableDropdownSelectionModel()
+  tagSelectionModel = new FilterableDropdownSelectionModel(true)
   correspondentSelectionModel = new FilterableDropdownSelectionModel()
   documentTypeSelectionModel = new FilterableDropdownSelectionModel()
   storagePathsSelectionModel = new FilterableDropdownSelectionModel()
-  customFieldsSelectionModel = new FilterableDropdownSelectionModel()
+  customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
   tagDocumentCounts: SelectionDataItem[]
   correspondentDocumentCounts: SelectionDataItem[]
   documentTypeDocumentCounts: SelectionDataItem[]
@@ -176,7 +166,7 @@ export class BulkEditorComponent
       this.tagService
         .listAll()
         .pipe(first())
-        .subscribe((result) => (this.tags = result.results))
+        .subscribe((result) => (this.tagSelectionModel.items = result.results))
     }
     if (
       this.permissionService.currentUserCan(
@@ -187,7 +177,9 @@ export class BulkEditorComponent
       this.correspondentService
         .listAll()
         .pipe(first())
-        .subscribe((result) => (this.correspondents = result.results))
+        .subscribe(
+          (result) => (this.correspondentSelectionModel.items = result.results)
+        )
     }
     if (
       this.permissionService.currentUserCan(
@@ -198,7 +190,9 @@ export class BulkEditorComponent
       this.documentTypeService
         .listAll()
         .pipe(first())
-        .subscribe((result) => (this.documentTypes = result.results))
+        .subscribe(
+          (result) => (this.documentTypeSelectionModel.items = result.results)
+        )
     }
     if (
       this.permissionService.currentUserCan(
@@ -209,7 +203,9 @@ export class BulkEditorComponent
       this.storagePathService
         .listAll()
         .pipe(first())
-        .subscribe((result) => (this.storagePaths = result.results))
+        .subscribe(
+          (result) => (this.storagePathsSelectionModel.items = result.results)
+        )
     }
     if (
       this.permissionService.currentUserCan(
@@ -220,7 +216,9 @@ export class BulkEditorComponent
       this.customFieldService
         .listAll()
         .pipe(first())
-        .subscribe((result) => (this.customFields = result.results))
+        .subscribe(
+          (result) => (this.customFieldsSelectionModel.items = result.results)
+        )
     }
 
     this.downloadForm
@@ -651,7 +649,7 @@ export class BulkEditorComponent
       )
       .pipe(takeUntil(this.unsubscribeNotifier))
       .subscribe(({ newTag, tags }) => {
-        this.tags = tags.results
+        this.tagSelectionModel.items = tags.results
         this.tagSelectionModel.toggle(newTag.id)
       })
   }
@@ -674,7 +672,7 @@ export class BulkEditorComponent
       )
       .pipe(takeUntil(this.unsubscribeNotifier))
       .subscribe(({ newCorrespondent, correspondents }) => {
-        this.correspondents = correspondents.results
+        this.correspondentSelectionModel.items = correspondents.results
         this.correspondentSelectionModel.toggle(newCorrespondent.id)
       })
   }
@@ -695,7 +693,7 @@ export class BulkEditorComponent
       )
       .pipe(takeUntil(this.unsubscribeNotifier))
       .subscribe(({ newDocumentType, documentTypes }) => {
-        this.documentTypes = documentTypes.results
+        this.documentTypeSelectionModel.items = documentTypes.results
         this.documentTypeSelectionModel.toggle(newDocumentType.id)
       })
   }
@@ -716,7 +714,7 @@ export class BulkEditorComponent
       )
       .pipe(takeUntil(this.unsubscribeNotifier))
       .subscribe(({ newStoragePath, storagePaths }) => {
-        this.storagePaths = storagePaths.results
+        this.storagePathsSelectionModel.items = storagePaths.results
         this.storagePathsSelectionModel.toggle(newStoragePath.id)
       })
   }
@@ -737,7 +735,7 @@ export class BulkEditorComponent
       )
       .pipe(takeUntil(this.unsubscribeNotifier))
       .subscribe(({ newCustomField, customFields }) => {
-        this.customFields = customFields.results
+        this.customFieldsSelectionModel.items = customFields.results
         this.customFieldsSelectionModel.toggle(newCustomField.id)
       })
   }
@@ -875,7 +873,9 @@ export class BulkEditorComponent
     })
     const dialog =
       modal.componentInstance as CustomFieldsBulkEditDialogComponent
-    dialog.customFields = this.customFields
+    dialog.customFields = (
+      this.customFieldsSelectionModel.items as CustomField[]
+    ).filter((f) => f.id !== null)
     dialog.fieldsToAddIds = changedCustomFields.itemsToAdd.map(
       (item) => item.id
     )
index f8d346cba67f54e0a703c585d6cae90a97e9b881..f4a7938b71c2b7437192fd035758cd384fd670ec 100644 (file)
   <div class="col-auto">
     <div class="d-flex flex-wrap gap-3">
       <div class="d-flex flex-wrap gap-2">
-        @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tags.length > 0) {
+        @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tagSelectionModel.items.length > 0) {
           <pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Tags" icon="tag-fill" i18n-title
           filterPlaceholder="Filter tags" i18n-filterPlaceholder
-          [items]="tags"
-          [manyToOne]="true"
           [(selectionModel)]="tagSelectionModel"
           (selectionModelChange)="updateRules()"
           (opened)="onTagsDropdownOpen()"
           [disabled]="disabled"
           shortcutKey="t"></pngx-filterable-dropdown>
         }
-        @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondents.length > 0) {
+        @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) {
           <pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Correspondent" icon="person-fill" i18n-title
           filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
-          [items]="correspondents"
           [(selectionModel)]="correspondentSelectionModel"
           (selectionModelChange)="updateRules()"
           (opened)="onCorrespondentDropdownOpen()"
           [disabled]="disabled"
           shortcutKey="y"></pngx-filterable-dropdown>
         }
-        @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypes.length > 0) {
+        @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) {
             <pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Document type" icon="file-earmark-fill" i18n-title
             filterPlaceholder="Filter document types" i18n-filterPlaceholder
-            [items]="documentTypes"
             [(selectionModel)]="documentTypeSelectionModel"
             (selectionModelChange)="updateRules()"
             (opened)="onDocumentTypeDropdownOpen()"
             [disabled]="disabled"
             shortcutKey="u"></pngx-filterable-dropdown>
         }
-        @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.length > 0) {
+        @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) {
           <pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Storage path" icon="folder-fill" i18n-title
           filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
-          [items]="storagePaths"
           [(selectionModel)]="storagePathSelectionModel"
           (selectionModelChange)="updateRules()"
           (opened)="onStoragePathDropdownOpen()"
index 58524fa99712f22847f92beb2ecf3af09eb1988f..4e8a797cc3e58144feb0afe47f8b579447b6dc96 100644 (file)
@@ -69,6 +69,7 @@ import {
   FILTER_STORAGE_PATH,
   FILTER_TITLE,
   FILTER_TITLE_CONTENT,
+  NEGATIVE_NULL_FILTER_VALUE,
 } from 'src/app/data/filter-rule-type'
 import { StoragePath } from 'src/app/data/storage-path'
 import { Tag } from 'src/app/data/tag'
@@ -671,9 +672,6 @@ describe('FilterEditorComponent', () => {
         value: '12',
       },
     ]
-    expect(component.correspondentSelectionModel.logicalOperator).toEqual(
-      LogicalOperator.Or
-    )
     expect(component.correspondentSelectionModel.intersection).toEqual(
       Intersection.Include
     )
@@ -681,6 +679,19 @@ describe('FilterEditorComponent', () => {
       correspondents[0],
     ])
     component.toggleCorrespondent(12) // coverage
+
+    component.filterRules = [
+      {
+        rule_type: FILTER_CORRESPONDENT,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ]
+    expect(component.correspondentSelectionModel.intersection).toEqual(
+      Intersection.Exclude
+    )
+    expect(component.correspondentSelectionModel.getExcludedItems()).toEqual([
+      { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+    ])
   }))
 
   it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
@@ -754,9 +765,6 @@ describe('FilterEditorComponent', () => {
         value: '22',
       },
     ]
-    expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
-      LogicalOperator.Or
-    )
     expect(component.documentTypeSelectionModel.intersection).toEqual(
       Intersection.Include
     )
@@ -764,6 +772,19 @@ describe('FilterEditorComponent', () => {
       document_types[0],
     ])
     component.toggleDocumentType(22) // coverage
+
+    component.filterRules = [
+      {
+        rule_type: FILTER_DOCUMENT_TYPE,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ]
+    expect(component.documentTypeSelectionModel.intersection).toEqual(
+      Intersection.Exclude
+    )
+    expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual([
+      { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+    ])
   }))
 
   it('should ingest filter rules for has any of document types', fakeAsync(() => {
@@ -780,9 +801,6 @@ describe('FilterEditorComponent', () => {
         value: '23',
       },
     ]
-    expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
-      LogicalOperator.Or
-    )
     expect(component.documentTypeSelectionModel.intersection).toEqual(
       Intersection.Include
     )
@@ -837,9 +855,6 @@ describe('FilterEditorComponent', () => {
         value: '32',
       },
     ]
-    expect(component.storagePathSelectionModel.logicalOperator).toEqual(
-      LogicalOperator.Or
-    )
     expect(component.storagePathSelectionModel.intersection).toEqual(
       Intersection.Include
     )
@@ -847,6 +862,19 @@ describe('FilterEditorComponent', () => {
       storage_paths[0],
     ])
     component.toggleStoragePath(32) // coverage
+
+    component.filterRules = [
+      {
+        rule_type: FILTER_STORAGE_PATH,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ]
+    expect(component.storagePathSelectionModel.intersection).toEqual(
+      Intersection.Exclude
+    )
+    expect(component.storagePathSelectionModel.getExcludedItems()).toEqual([
+      { id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
+    ])
   }))
 
   it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
@@ -1398,6 +1426,19 @@ describe('FilterEditorComponent', () => {
         value: null,
       },
     ])
+
+    const excludeButton = correspondentsFilterableDropdown.queryAll(
+      By.css('input[value=exclude]')
+    )[0]
+    excludeButton.nativeElement.checked = true
+    excludeButton.triggerEventHandler('change')
+    fixture.detectChanges()
+    expect(component.filterRules).toEqual([
+      {
+        rule_type: FILTER_CORRESPONDENT,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ])
   }))
 
   it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
@@ -1455,6 +1496,19 @@ describe('FilterEditorComponent', () => {
         value: null,
       },
     ])
+
+    const excludeButton = docTypesFilterableDropdown.queryAll(
+      By.css('input[value=exclude]')
+    )[0]
+    excludeButton.nativeElement.checked = true
+    excludeButton.triggerEventHandler('change')
+    fixture.detectChanges()
+    expect(component.filterRules).toEqual([
+      {
+        rule_type: FILTER_DOCUMENT_TYPE,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ])
   }))
 
   it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
@@ -1512,6 +1566,19 @@ describe('FilterEditorComponent', () => {
         value: null,
       },
     ])
+
+    const excludeButton = storagePathsFilterableDropdown.queryAll(
+      By.css('input[value=exclude]')
+    )[0]
+    excludeButton.nativeElement.checked = true
+    excludeButton.triggerEventHandler('change')
+    fixture.detectChanges()
+    expect(component.filterRules).toEqual([
+      {
+        rule_type: FILTER_STORAGE_PATH,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ])
   }))
 
   it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
index debd7b4b302619ce0bf578597a96f8461a2e1f91..88f1be48ba7c6e65d555dcd3b3b754fda20fb0bb 100644 (file)
@@ -26,14 +26,12 @@ import {
   switchMap,
   takeUntil,
 } from 'rxjs/operators'
-import { Correspondent } from 'src/app/data/correspondent'
 import { CustomField } from 'src/app/data/custom-field'
 import {
   CustomFieldQueryLogicalOperator,
   CustomFieldQueryOperator,
 } from 'src/app/data/custom-field-query'
 import { Document } from 'src/app/data/document'
-import { DocumentType } from 'src/app/data/document-type'
 import { FilterRule } from 'src/app/data/filter-rule'
 import {
   FILTER_ADDED_AFTER,
@@ -75,9 +73,8 @@ import {
   FILTER_STORAGE_PATH,
   FILTER_TITLE,
   FILTER_TITLE_CONTENT,
+  NEGATIVE_NULL_FILTER_VALUE,
 } from 'src/app/data/filter-rule-type'
-import { StoragePath } from 'src/app/data/storage-path'
-import { Tag } from 'src/app/data/tag'
 import {
   PermissionAction,
   PermissionType,
@@ -251,7 +248,9 @@ export class FilterEditorComponent
         case FILTER_HAS_CORRESPONDENT_ANY:
           if (rule.value) {
             return $localize`Correspondent: ${
-              this.correspondents.find((c) => c.id == +rule.value)?.name
+              this.correspondentSelectionModel.items.find(
+                (c) => c.id == +rule.value
+              )?.name
             }`
           } else {
             return $localize`Without correspondent`
@@ -261,7 +260,9 @@ export class FilterEditorComponent
         case FILTER_HAS_DOCUMENT_TYPE_ANY:
           if (rule.value) {
             return $localize`Document type: ${
-              this.documentTypes.find((dt) => dt.id == +rule.value)?.name
+              this.documentTypeSelectionModel.items.find(
+                (dt) => dt.id == +rule.value
+              )?.name
             }`
           } else {
             return $localize`Without document type`
@@ -271,7 +272,9 @@ export class FilterEditorComponent
         case FILTER_HAS_STORAGE_PATH_ANY:
           if (rule.value) {
             return $localize`Storage path: ${
-              this.storagePaths.find((sp) => sp.id == +rule.value)?.name
+              this.storagePathSelectionModel.items.find(
+                (sp) => sp.id == +rule.value
+              )?.name
             }`
           } else {
             return $localize`Without storage path`
@@ -279,7 +282,7 @@ export class FilterEditorComponent
 
         case FILTER_HAS_TAGS_ALL:
           return $localize`Tag: ${
-            this.tags.find((t) => t.id == +rule.value)?.name
+            this.tagSelectionModel.items.find((t) => t.id == +rule.value)?.name
           }`
 
         case FILTER_HAS_ANY_TAG:
@@ -326,10 +329,6 @@ export class FilterEditorComponent
   @ViewChild('textFilterInput')
   textFilterInput: ElementRef
 
-  tags: Tag[] = []
-  correspondents: Correspondent[] = []
-  documentTypes: DocumentType[] = []
-  storagePaths: StoragePath[] = []
   customFields: CustomField[] = []
 
   tagDocumentCounts: SelectionDataItem[]
@@ -370,7 +369,7 @@ export class FilterEditorComponent
     )
   }
 
-  tagSelectionModel = new FilterableDropdownSelectionModel()
+  tagSelectionModel = new FilterableDropdownSelectionModel(true)
   correspondentSelectionModel = new FilterableDropdownSelectionModel()
   documentTypeSelectionModel = new FilterableDropdownSelectionModel()
   storagePathSelectionModel = new FilterableDropdownSelectionModel()
@@ -551,6 +550,19 @@ export class FilterEditorComponent
           )
           break
         case FILTER_CORRESPONDENT:
+          this.correspondentSelectionModel.intersection =
+            rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
+              ? Intersection.Exclude
+              : Intersection.Include
+          this.correspondentSelectionModel.set(
+            rule.value ? +rule.value : null,
+            this.correspondentSelectionModel.intersection ==
+              Intersection.Include
+              ? ToggleableItemState.Selected
+              : ToggleableItemState.Excluded,
+            false
+          )
+          break
         case FILTER_HAS_CORRESPONDENT_ANY:
           this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
           this.correspondentSelectionModel.intersection = Intersection.Include
@@ -569,6 +581,18 @@ export class FilterEditorComponent
           )
           break
         case FILTER_DOCUMENT_TYPE:
+          this.documentTypeSelectionModel.intersection =
+            rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
+              ? Intersection.Exclude
+              : Intersection.Include
+          this.documentTypeSelectionModel.set(
+            rule.value ? +rule.value : null,
+            this.documentTypeSelectionModel.intersection == Intersection.Include
+              ? ToggleableItemState.Selected
+              : ToggleableItemState.Excluded,
+            false
+          )
+          break
         case FILTER_HAS_DOCUMENT_TYPE_ANY:
           this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
           this.documentTypeSelectionModel.intersection = Intersection.Include
@@ -587,6 +611,18 @@ export class FilterEditorComponent
           )
           break
         case FILTER_STORAGE_PATH:
+          this.storagePathSelectionModel.intersection =
+            rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
+              ? Intersection.Exclude
+              : Intersection.Include
+          this.storagePathSelectionModel.set(
+            rule.value ? +rule.value : null,
+            this.storagePathSelectionModel.intersection == Intersection.Include
+              ? ToggleableItemState.Selected
+              : ToggleableItemState.Excluded,
+            false
+          )
+          break
         case FILTER_HAS_STORAGE_PATH_ANY:
           this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
           this.storagePathSelectionModel.intersection = Intersection.Include
@@ -809,9 +845,21 @@ export class FilterEditorComponent
           })
         })
     }
-    if (this.correspondentSelectionModel.isNoneSelected()) {
+    if (
+      this.correspondentSelectionModel.isNoneSelected() &&
+      this.correspondentSelectionModel.intersection == Intersection.Include
+    ) {
       filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
     } else {
+      if (
+        this.correspondentSelectionModel.isNoneSelected() &&
+        this.correspondentSelectionModel.intersection == Intersection.Exclude
+      ) {
+        filterRules.push({
+          rule_type: FILTER_CORRESPONDENT,
+          value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+        })
+      }
       this.correspondentSelectionModel
         .getSelectedItems()
         .forEach((correspondent) => {
@@ -822,6 +870,7 @@ export class FilterEditorComponent
         })
       this.correspondentSelectionModel
         .getExcludedItems()
+        .filter((correspondent) => correspondent.id > 0)
         .forEach((correspondent) => {
           filterRules.push({
             rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
@@ -829,9 +878,21 @@ export class FilterEditorComponent
           })
         })
     }
-    if (this.documentTypeSelectionModel.isNoneSelected()) {
+    if (
+      this.documentTypeSelectionModel.isNoneSelected() &&
+      this.documentTypeSelectionModel.intersection === Intersection.Include
+    ) {
       filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
     } else {
+      if (
+        this.documentTypeSelectionModel.isNoneSelected() &&
+        this.documentTypeSelectionModel.intersection == Intersection.Exclude
+      ) {
+        filterRules.push({
+          rule_type: FILTER_DOCUMENT_TYPE,
+          value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+        })
+      }
       this.documentTypeSelectionModel
         .getSelectedItems()
         .forEach((documentType) => {
@@ -842,6 +903,7 @@ export class FilterEditorComponent
         })
       this.documentTypeSelectionModel
         .getExcludedItems()
+        .filter((documentType) => documentType.id > 0)
         .forEach((documentType) => {
           filterRules.push({
             rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
@@ -849,9 +911,21 @@ export class FilterEditorComponent
           })
         })
     }
-    if (this.storagePathSelectionModel.isNoneSelected()) {
+    if (
+      this.storagePathSelectionModel.isNoneSelected() &&
+      this.storagePathSelectionModel.intersection == Intersection.Include
+    ) {
       filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
     } else {
+      if (
+        this.storagePathSelectionModel.isNoneSelected() &&
+        this.storagePathSelectionModel.intersection == Intersection.Exclude
+      ) {
+        filterRules.push({
+          rule_type: FILTER_STORAGE_PATH,
+          value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+        })
+      }
       this.storagePathSelectionModel
         .getSelectedItems()
         .forEach((storagePath) => {
@@ -862,6 +936,7 @@ export class FilterEditorComponent
         })
       this.storagePathSelectionModel
         .getExcludedItems()
+        .filter((storagePath) => storagePath.id > 0)
         .forEach((storagePath) => {
           filterRules.push({
             rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
@@ -1062,7 +1137,7 @@ export class FilterEditorComponent
     ) {
       this.loadingCountTotal++
       this.tagService.listAll().subscribe((result) => {
-        this.tags = result.results
+        this.tagSelectionModel.items = result.results
         this.maybeCompleteLoading()
       })
     }
@@ -1074,7 +1149,7 @@ export class FilterEditorComponent
     ) {
       this.loadingCountTotal++
       this.correspondentService.listAll().subscribe((result) => {
-        this.correspondents = result.results
+        this.correspondentSelectionModel.items = result.results
         this.maybeCompleteLoading()
       })
     }
@@ -1086,7 +1161,7 @@ export class FilterEditorComponent
     ) {
       this.loadingCountTotal++
       this.documentTypeService.listAll().subscribe((result) => {
-        this.documentTypes = result.results
+        this.documentTypeSelectionModel.items = result.results
         this.maybeCompleteLoading()
       })
     }
@@ -1098,7 +1173,7 @@ export class FilterEditorComponent
     ) {
       this.loadingCountTotal++
       this.storagePathService.listAll().subscribe((result) => {
-        this.storagePaths = result.results
+        this.storagePathSelectionModel.items = result.results
         this.maybeCompleteLoading()
       })
     }
index bb2bf762ca54da3adab2226dd2d5d454c79a757a..7f0f0d56d131a135355aa92677033726bc0a2f70 100644 (file)
@@ -1,5 +1,7 @@
 import { DataType } from './datatype'
 
+export const NEGATIVE_NULL_FILTER_VALUE = -1
+
 // These correspond to src/documents/models.py and changes here require a DB migration (and vice versa)
 export const FILTER_TITLE = 0
 export const FILTER_CONTENT = 1
index cc91f3f6cfb05aa5dce5206fe3768cab70c367bc..c22c90d1156513d27e7df1d1ffb3067ef374c864 100644 (file)
@@ -8,6 +8,7 @@ import {
   FILTER_HAS_CUSTOM_FIELDS_ALL,
   FILTER_HAS_CUSTOM_FIELDS_ANY,
   FILTER_HAS_TAGS_ALL,
+  NEGATIVE_NULL_FILTER_VALUE,
 } from '../data/filter-rule-type'
 import {
   filterRulesFromQueryParams,
@@ -97,6 +98,16 @@ describe('QueryParams Utils', () => {
       correspondent__isnull: 1,
     })
 
+    params = queryParamsFromFilterRules([
+      {
+        rule_type: FILTER_CORRESPONDENT,
+        value: NEGATIVE_NULL_FILTER_VALUE.toString(),
+      },
+    ])
+    expect(params).toEqual({
+      correspondent__isnull: 0,
+    })
+
     params = queryParamsFromFilterRules([
       {
         rule_type: FILTER_HAS_ANY_TAG,
index d90167c5b50cf6c04dd2d77cc5f7aeb64e7dd935..27716cc2d6b4a29d5b7b93b349bcc745a96341e1 100644 (file)
@@ -10,6 +10,7 @@ import {
   FILTER_HAS_CUSTOM_FIELDS_ANY,
   FILTER_RULE_TYPES,
   FilterRuleType,
+  NEGATIVE_NULL_FILTER_VALUE,
 } from '../data/filter-rule-type'
 import { ListViewState } from '../services/document-list-view.service'
 
@@ -113,6 +114,10 @@ export function filterRulesFromQueryParams(
           rt.isnull_filtervar == filterQueryParamName
       )
       const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName
+      const nullRuleValue =
+        queryParams.get(filterQueryParamName) == '1'
+          ? null
+          : NEGATIVE_NULL_FILTER_VALUE.toString()
       const valueURIComponent: string = queryParams.get(filterQueryParamName)
       const filterQueryParamValues: string[] = rule_type.multi
         ? valueURIComponent.split(',')
@@ -125,7 +130,7 @@ export function filterRulesFromQueryParams(
             val = val.replace('1', 'true').replace('0', 'false')
           return {
             rule_type: rule_type.id,
-            value: isNullRuleType ? null : val,
+            value: isNullRuleType ? nullRuleValue : val,
           }
         })
       )
@@ -143,6 +148,11 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
       let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
       if (ruleType.isnull_filtervar && rule.value == null) {
         params[ruleType.isnull_filtervar] = 1
+      } else if (
+        ruleType.isnull_filtervar &&
+        rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
+      ) {
+        params[ruleType.isnull_filtervar] = 0
       } else if (ruleType.multi) {
         params[ruleType.filtervar] = params[ruleType.filtervar]
           ? params[ruleType.filtervar] + ',' + rule.value