diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 67e8be0fc..f609139d8 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -5,14 +5,14 @@ Close - node_modules/src/alert/alert.ts + 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 51 Slide of - node_modules/src/carousel/carousel.ts + 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 132,136 Currently selected slide number read by screen reader @@ -20,212 +20,212 @@ Previous - node_modules/src/carousel/carousel.ts - 148,149 + 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 + 156,160 Next - node_modules/src/carousel/carousel.ts - 167,170 + 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 + 196,199 Previous month - node_modules/src/datepicker/datepicker-navigation.ts + 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 77,79 - node_modules/src/datepicker/datepicker-navigation.ts - 97,98 + 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 + 102 Next month - node_modules/src/datepicker/datepicker-navigation.ts + 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 102 - node_modules/src/datepicker/datepicker-navigation.ts + 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 102 HH - node_modules/src/ngb-config.ts + 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 13 Close - node_modules/src/ngb-config.ts + 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 13 Select month - node_modules/src/ngb-config.ts + 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 13 - node_modules/src/ngb-config.ts + 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 13 «« - node_modules/src/ngb-config.ts + 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 13 Hours - node_modules/src/ngb-config.ts + 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 13 « - node_modules/src/ngb-config.ts + 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 13 MM - node_modules/src/ngb-config.ts + 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 13 » - node_modules/src/ngb-config.ts + 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 13 Select year - node_modules/src/ngb-config.ts + 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 13 - node_modules/src/ngb-config.ts + 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 13 Minutes - node_modules/src/ngb-config.ts + 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 13 »» - node_modules/src/ngb-config.ts + 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 13 First - node_modules/src/ngb-config.ts + 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 13 Increment hours - node_modules/src/ngb-config.ts + 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 13 Previous - node_modules/src/ngb-config.ts + 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 13 Decrement hours - node_modules/src/ngb-config.ts + 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 13 Next - node_modules/src/ngb-config.ts + 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 13 Increment minutes - node_modules/src/ngb-config.ts + 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 13 Last - node_modules/src/ngb-config.ts + 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 13 Decrement minutes - node_modules/src/ngb-config.ts + 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 13 SS - node_modules/src/ngb-config.ts + 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 13 Seconds - node_modules/src/ngb-config.ts + 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 13 Increment seconds - node_modules/src/ngb-config.ts + 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 13 Decrement seconds - node_modules/src/ngb-config.ts + 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 13 - node_modules/src/ngb-config.ts + 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 13 @@ -233,7 +233,7 @@ - node_modules/src/progressbar/progressbar.ts + 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 41,42 @@ -1120,7 +1120,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 193 + 190 @@ -1194,11 +1194,11 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 104 + 97 src/app/components/document-list/filter-editor/filter-editor.component.html - 106 + 101 src/app/components/manage/mail/mail.component.html @@ -1793,7 +1793,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 110 + 103 src/app/components/manage/custom-fields/custom-fields.component.html @@ -2086,7 +2086,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 164 + 157 src/app/components/manage/custom-fields/custom-fields.component.html @@ -2553,15 +2553,15 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 796 + 794 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 829 + 827 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 848 + 846 src/app/components/manage/custom-fields/custom-fields.component.ts @@ -2986,7 +2986,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 136 + 129 src/app/components/document-list/document-card-large/document-card-large.component.html @@ -3161,27 +3161,27 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 439 + 437 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 479 + 477 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 517 + 515 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 555 + 553 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 617 + 615 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 750 + 748 @@ -5246,7 +5246,7 @@ Not assigned src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts - 392 + 81 Filter drop down element to filter for documents with no correspondent/type/tag assigned @@ -5254,7 +5254,7 @@ Open filter src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts - 513 + 554 @@ -6415,7 +6415,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 384 + 382 this string is used to separate processing, failed and added on the file upload widget @@ -6490,7 +6490,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 114 + 107 @@ -6519,7 +6519,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 117 + 110 @@ -6562,7 +6562,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 180 + 177 src/app/data/document.ts @@ -6595,7 +6595,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 37 + 35 src/app/components/document-list/document-list.component.html @@ -6603,7 +6603,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.html - 52 + 50 src/app/data/document.ts @@ -6622,7 +6622,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 52 + 49 src/app/components/document-list/document-list.component.html @@ -6630,7 +6630,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.html - 64 + 61 src/app/data/document.ts @@ -6649,7 +6649,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html - 67 + 63 src/app/components/document-list/document-list.component.html @@ -6657,7 +6657,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.html - 76 + 72 src/app/data/document.ts @@ -6940,7 +6940,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 752 + 750 @@ -6951,7 +6951,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 754 + 752 @@ -6969,7 +6969,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 792 + 790 @@ -7050,7 +7050,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 826 + 824 @@ -7149,122 +7149,122 @@ Filter correspondents src/app/components/document-list/bulk-editor/bulk-editor.component.html - 38 + 36 src/app/components/document-list/filter-editor/filter-editor.component.html - 53 + 51 Filter document types src/app/components/document-list/bulk-editor/bulk-editor.component.html - 53 + 50 src/app/components/document-list/filter-editor/filter-editor.component.html - 65 + 62 Filter storage paths src/app/components/document-list/bulk-editor/bulk-editor.component.html - 68 + 64 src/app/components/document-list/filter-editor/filter-editor.component.html - 77 + 73 Custom fields src/app/components/document-list/bulk-editor/bulk-editor.component.html - 82 + 77 src/app/components/document-list/filter-editor/filter-editor.component.html - 89 + 84 src/app/components/document-list/filter-editor/filter-editor.component.ts - 188 + 185 Filter custom fields src/app/components/document-list/bulk-editor/bulk-editor.component.html - 83 + 78 Set values src/app/components/document-list/bulk-editor/bulk-editor.component.html - 93 + 86 Merge src/app/components/document-list/bulk-editor/bulk-editor.component.html - 120 + 113 Include: src/app/components/document-list/bulk-editor/bulk-editor.component.html - 142 + 135 Archived files src/app/components/document-list/bulk-editor/bulk-editor.component.html - 146 + 139 Original files src/app/components/document-list/bulk-editor/bulk-editor.component.html - 150 + 143 Use formatted filename src/app/components/document-list/bulk-editor/bulk-editor.component.html - 155 + 148 Error executing bulk operation src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 288 + 286 "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 376 + 374 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 382 + 380 "" and "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 378 + 376 This is for messages like 'modify "tag1" and "tag2"' @@ -7272,7 +7272,7 @@ and "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 386,388 + 384,386 this is for messages like 'modify "tag1", "tag2" and "tag3"' @@ -7280,14 +7280,14 @@ Confirm tags assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 403 + 401 This operation will add the tag "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 409 + 407 @@ -7296,14 +7296,14 @@ )"/> to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 414,416 + 412,414 This operation will remove the tag "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 422 + 420 @@ -7312,7 +7312,7 @@ )"/> from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 427,429 + 425,427 @@ -7323,84 +7323,84 @@ )"/> on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 431,435 + 429,433 Confirm correspondent assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 472 + 470 This operation will assign the correspondent "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 474 + 472 This operation will remove the correspondent from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 476 + 474 Confirm document type assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 510 + 508 This operation will assign the document type "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 512 + 510 This operation will remove the document type from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 514 + 512 Confirm storage path assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 548 + 546 This operation will assign the storage path "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 550 + 548 This operation will remove the storage path from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 552 + 550 Confirm custom field assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 581 + 579 This operation will assign the custom field "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 587 + 585 @@ -7409,14 +7409,14 @@ )"/> to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 592,594 + 590,592 This operation will remove the custom field "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 600 + 598 @@ -7425,7 +7425,7 @@ )"/> from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 605,607 + 603,605 @@ -7436,56 +7436,56 @@ )"/> on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 609,613 + 607,611 Move selected document(s) to the trash? src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 751 + 749 This operation will permanently recreate the archive files for selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 793 + 791 The archive files will be re-generated with the current settings. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 794 + 792 This operation will permanently rotate the original version of document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 827 + 825 Merge confirm src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 846 + 844 This operation will merge selected documents into a new document. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 847 + 845 Merged document will be queued for consumption. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 866 + 864 @@ -7767,7 +7767,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.html - 112 + 107 @@ -7792,7 +7792,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 185 + 182 src/app/data/document.ts @@ -7981,161 +7981,167 @@ Dates src/app/components/document-list/filter-editor/filter-editor.component.html - 95 + 90 Title & content src/app/components/document-list/filter-editor/filter-editor.component.ts - 183 + 180 File type src/app/components/document-list/filter-editor/filter-editor.component.ts - 190 + 187 More like src/app/components/document-list/filter-editor/filter-editor.component.ts - 199 + 196 equals src/app/components/document-list/filter-editor/filter-editor.component.ts - 205 + 202 is empty src/app/components/document-list/filter-editor/filter-editor.component.ts - 209 + 206 is not empty src/app/components/document-list/filter-editor/filter-editor.component.ts - 213 + 210 greater than src/app/components/document-list/filter-editor/filter-editor.component.ts - 217 + 214 less than src/app/components/document-list/filter-editor/filter-editor.component.ts - 221 + 218 - Correspondent: + Correspondent: src/app/components/document-list/filter-editor/filter-editor.component.ts - 253,255 + 250,254 Without correspondent src/app/components/document-list/filter-editor/filter-editor.component.ts - 257 + 256 - Document type: + Document type: src/app/components/document-list/filter-editor/filter-editor.component.ts - 263,265 + 262,266 Without document type src/app/components/document-list/filter-editor/filter-editor.component.ts - 267 + 268 - Storage path: + Storage path: src/app/components/document-list/filter-editor/filter-editor.component.ts - 273,275 + 274,278 Without storage path src/app/components/document-list/filter-editor/filter-editor.component.ts - 277 + 280 - Tag: + Tag: src/app/components/document-list/filter-editor/filter-editor.component.ts - 281,283 + 284,286 Without any tag src/app/components/document-list/filter-editor/filter-editor.component.ts - 287 + 290 Custom fields query src/app/components/document-list/filter-editor/filter-editor.component.ts - 291 + 294 Title: src/app/components/document-list/filter-editor/filter-editor.component.ts - 294 + 297 ASN: src/app/components/document-list/filter-editor/filter-editor.component.ts - 297 + 300 Owner: src/app/components/document-list/filter-editor/filter-editor.component.ts - 300 + 303 Owner not in: src/app/components/document-list/filter-editor/filter-editor.component.ts - 303 + 306 Without an owner src/app/components/document-list/filter-editor/filter-editor.component.ts - 306 + 309 diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts index 6ba15eacd..cd279b1b5 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts @@ -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('selection model should sort items by state', () => { - component.items = items.concat([{ id: null, name: 'Null B' }]) + 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.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 diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index 34320003e..45c776df6 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -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() - @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 { diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index ac8f476c7..0eb655a21 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -20,10 +20,8 @@ @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) { { 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]) diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index aa32497f3..bf6d06cd4 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -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 ) diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html index f8d346cba..f4a7938b7 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html @@ -35,11 +35,9 @@
- @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tags.length > 0) { + @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tagSelectionModel.items.length > 0) { } - @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondents.length > 0) { + @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) { } - @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypes.length > 0) { + @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) { } - @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.length > 0) { + @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) { { 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(() => { diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts index debd7b4b3..88f1be48b 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -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() }) } diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts index bb2bf762c..7f0f0d56d 100644 --- a/src-ui/src/app/data/filter-rule-type.ts +++ b/src-ui/src/app/data/filter-rule-type.ts @@ -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 diff --git a/src-ui/src/app/utils/query-params.spec.ts b/src-ui/src/app/utils/query-params.spec.ts index cc91f3f6c..c22c90d11 100644 --- a/src-ui/src/app/utils/query-params.spec.ts +++ b/src-ui/src/app/utils/query-params.spec.ts @@ -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, diff --git a/src-ui/src/app/utils/query-params.ts b/src-ui/src/app/utils/query-params.ts index d90167c5b..27716cc2d 100644 --- a/src-ui/src/app/utils/query-params.ts +++ b/src-ui/src/app/utils/query-params.ts @@ -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