mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge pull request #2893 from paperless-ngx/feature-enhanced-object-filtering
Enhancement: support filtering multiple correspondents, doctypes & storage paths
This commit is contained in:
		| @@ -48,6 +48,26 @@ describe('documents-list', () => { | ||||
|             (d.tags as Array<number>).includes(tag_id) | ||||
|           ) | ||||
|           response.count = response.results.length | ||||
|         } else if (req.query.hasOwnProperty('correspondent__id__in')) { | ||||
|           // filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&correspondent__id__in=9,14 | ||||
|           const correspondent_ids = req.query['correspondent__id__in'] | ||||
|             .toString() | ||||
|             .split(',') | ||||
|             .map((c) => +c) | ||||
|           response.results = (documentsJson.results as Array<any>).filter((d) => | ||||
|             correspondent_ids.includes(d.correspondent) | ||||
|           ) | ||||
|           response.count = response.results.length | ||||
|         } else if (req.query.hasOwnProperty('correspondent__id__none')) { | ||||
|           // filtering e.g. http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&correspondent__id__none=9,14 | ||||
|           const correspondent_ids = req.query['correspondent__id__none'] | ||||
|             .toString() | ||||
|             .split(',') | ||||
|             .map((c) => +c) | ||||
|           response.results = (documentsJson.results as Array<any>).filter( | ||||
|             (d) => !correspondent_ids.includes(d.correspondent) | ||||
|           ) | ||||
|           response.count = response.results.length | ||||
|         } | ||||
|  | ||||
|         req.reply(response) | ||||
| @@ -112,6 +132,27 @@ describe('documents-list', () => { | ||||
|     cy.contains('One document') | ||||
|   }) | ||||
|  | ||||
|   it('should filter including multiple correspondents', () => { | ||||
|     cy.get('app-filter-editor app-filterable-dropdown[title="Correspondent"]') | ||||
|       .click() | ||||
|       .within(() => { | ||||
|         cy.contains('button', 'ABC Test Correspondent').click() | ||||
|         cy.contains('button', 'Corresp 11').click() | ||||
|       }) | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should filter excluding multiple correspondents', () => { | ||||
|     cy.get('app-filter-editor app-filterable-dropdown[title="Correspondent"]') | ||||
|       .click() | ||||
|       .within(() => { | ||||
|         cy.contains('button', 'ABC Test Correspondent').click() | ||||
|         cy.contains('button', 'Corresp 11').click() | ||||
|         cy.contains('label', 'Exclude').click() | ||||
|       }) | ||||
|     cy.contains('One document') | ||||
|   }) | ||||
|  | ||||
|   it('should apply tags', () => { | ||||
|     cy.get('app-document-card-small:first-of-type').click() | ||||
|     cy.get('app-bulk-editor app-filterable-dropdown[title="Tags"]').within( | ||||
|   | ||||
| @@ -232,6 +232,11 @@ describe('documents query params', () => { | ||||
|  | ||||
|   it('should show a list of documents filtered by document type', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&document_type__id=1') | ||||
|     cy.contains('2 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by multiple correspondents', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&document_type__id__in=1,2') | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
|  | ||||
| @@ -245,9 +250,14 @@ describe('documents query params', () => { | ||||
|     cy.contains('2 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by multiple correspondents', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&correspondent__id__in=9,14') | ||||
|     cy.contains('3 documents') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by no correspondent', () => { | ||||
|     cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1') | ||||
|     cy.contains('2 documents') | ||||
|     cy.contains('One document') | ||||
|   }) | ||||
|  | ||||
|   it('should show a list of documents filtered by storage path', () => { | ||||
|   | ||||
| @@ -1 +1,257 @@ | ||||
| {"count":27,"next":"http://localhost:8000/api/correspondents/?page=2","previous":null,"results":[{"id":9,"slug":"abc-test-correspondent","name":"ABC Test Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":13,"slug":"corresp-10","name":"Corresp 10","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":14,"slug":"corresp-11","name":"Corresp 11","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":15,"slug":"corresp-12","name":"Corresp 12","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":16,"slug":"corresp-13","name":"Corresp 13","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":18,"slug":"corresp-15","name":"Corresp 15","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":19,"slug":"corresp-16","name":"Corresp 16","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":20,"slug":"corresp-17","name":"Corresp 17","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":21,"slug":"corresp-18","name":"Corresp 18","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":22,"slug":"corresp-19","name":"Corresp 19","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":23,"slug":"corresp-20","name":"Corresp 20","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":24,"slug":"corresp-21","name":"Corresp 21","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":25,"slug":"corresp-22","name":"Corresp 22","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":26,"slug":"corresp-23","name":"Corresp 23","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":5,"slug":"corresp-3","name":"Corresp 3","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":6,"slug":"corresp-4","name":"Corresp 4","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":7,"slug":"corresp-5","name":"Corresp 5","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":8,"slug":"corresp-6","name":"Corresp 6","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":10,"slug":"corresp-7","name":"Corresp 7","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":11,"slug":"corresp-8","name":"Corresp 8","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":12,"slug":"corresp-9","name":"Corresp 9","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":17,"slug":"correspondent-14","name":"Correspondent 14","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0,"last_correspondence":null},{"id":2,"slug":"correspondent-2","name":"Correspondent 2","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":7,"last_correspondence":"2021-01-20T23:37:58.204614Z"},{"id":27,"slug":"michael-shamoon","name":"Michael Shamoon","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2022-03-16T03:48:50.089624Z"},{"id":4,"slug":"newest-correspondent","name":"Newest Correspondent","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":1,"last_correspondence":"2021-02-07T08:00:00Z"}]} | ||||
| { | ||||
|     "count": 27, | ||||
|     "next": "http://localhost:8000/api/correspondents/?page=2", | ||||
|     "previous": null, | ||||
|     "results": [ | ||||
|         { | ||||
|             "id": 9, | ||||
|             "slug": "abc-test-correspondent", | ||||
|             "name": "ABC Test Correspondent", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 13, | ||||
|             "slug": "corresp-10", | ||||
|             "name": "Corresp 10", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 14, | ||||
|             "slug": "corresp-11", | ||||
|             "name": "Corresp 11", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 15, | ||||
|             "slug": "corresp-12", | ||||
|             "name": "Corresp 12", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 16, | ||||
|             "slug": "corresp-13", | ||||
|             "name": "Corresp 13", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 18, | ||||
|             "slug": "corresp-15", | ||||
|             "name": "Corresp 15", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 19, | ||||
|             "slug": "corresp-16", | ||||
|             "name": "Corresp 16", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 20, | ||||
|             "slug": "corresp-17", | ||||
|             "name": "Corresp 17", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 21, | ||||
|             "slug": "corresp-18", | ||||
|             "name": "Corresp 18", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 22, | ||||
|             "slug": "corresp-19", | ||||
|             "name": "Corresp 19", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 23, | ||||
|             "slug": "corresp-20", | ||||
|             "name": "Corresp 20", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 24, | ||||
|             "slug": "corresp-21", | ||||
|             "name": "Corresp 21", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 25, | ||||
|             "slug": "corresp-22", | ||||
|             "name": "Corresp 22", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 26, | ||||
|             "slug": "corresp-23", | ||||
|             "name": "Corresp 23", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 5, | ||||
|             "slug": "corresp-3", | ||||
|             "name": "Corresp 3", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 6, | ||||
|             "slug": "corresp-4", | ||||
|             "name": "Corresp 4", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 7, | ||||
|             "slug": "corresp-5", | ||||
|             "name": "Corresp 5", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 8, | ||||
|             "slug": "corresp-6", | ||||
|             "name": "Corresp 6", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 10, | ||||
|             "slug": "corresp-7", | ||||
|             "name": "Corresp 7", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 11, | ||||
|             "slug": "corresp-8", | ||||
|             "name": "Corresp 8", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 12, | ||||
|             "slug": "corresp-9", | ||||
|             "name": "Corresp 9", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 17, | ||||
|             "slug": "correspondent-14", | ||||
|             "name": "Correspondent 14", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 0, | ||||
|             "last_correspondence": null | ||||
|         }, | ||||
|         { | ||||
|             "id": 2, | ||||
|             "slug": "correspondent-2", | ||||
|             "name": "Correspondent 2", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 7, | ||||
|             "last_correspondence": "2021-01-20T23:37:58.204614Z" | ||||
|         }, | ||||
|         { | ||||
|             "id": 27, | ||||
|             "slug": "correspondent-slug", | ||||
|             "name": "Correspondent Slug", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 1, | ||||
|             "last_correspondence": "2022-03-16T03:48:50.089624Z" | ||||
|         }, | ||||
|         { | ||||
|             "id": 4, | ||||
|             "slug": "newest-correspondent", | ||||
|             "name": "Newest Correspondent", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 1, | ||||
|             "last_correspondence": "2021-02-07T08:00:00Z" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|   | ||||
| @@ -1 +1,25 @@ | ||||
| {"count":1,"next":null,"previous":null,"results":[{"id":1,"slug":"test","name":"Test Doc Type","match":"","matching_algorithm":1,"is_insensitive":true,"document_count":0}]} | ||||
| { | ||||
|     "count": 2, | ||||
|     "next": null, | ||||
|     "previous": null, | ||||
|     "results": [ | ||||
|         { | ||||
|             "id": 1, | ||||
|             "slug": "test", | ||||
|             "name": "Test Doc Type", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 1 | ||||
|         }, | ||||
|         { | ||||
|             "id": 2, | ||||
|             "slug": "test2", | ||||
|             "name": "Test Doc Type 2", | ||||
|             "match": "", | ||||
|             "matching_algorithm": 1, | ||||
|             "is_insensitive": true, | ||||
|             "document_count": 1 | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|   | ||||
| @@ -43,7 +43,7 @@ | ||||
|         }, | ||||
|         { | ||||
|             "id": 3, | ||||
|             "correspondent": null, | ||||
|             "correspondent": 14, | ||||
|             "document_type": 1, | ||||
|             "storage_path": null, | ||||
|             "title": "dolor", | ||||
| @@ -64,7 +64,7 @@ | ||||
|         { | ||||
|             "id": 4, | ||||
|             "correspondent": 9, | ||||
|             "document_type": 1, | ||||
|             "document_type": 2, | ||||
|             "storage_path": null, | ||||
|             "title": "sit amet", | ||||
|             "content": "Test document PDF", | ||||
|   | ||||
| @@ -1794,25 +1794,39 @@ | ||||
|           <context context-type="linenumber">18</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6381578200008167206" datatype="html"> | ||||
|         <source>Include</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context> | ||||
|           <context context-type="linenumber">24</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5668077948386857930" datatype="html"> | ||||
|         <source>Exclude</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context> | ||||
|           <context context-type="linenumber">26</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4391289919356861627" datatype="html"> | ||||
|         <source>Apply</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context> | ||||
|           <context context-type="linenumber">32</context> | ||||
|           <context context-type="linenumber">40</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="7780041345210191160" datatype="html"> | ||||
|         <source>Click again to exclude items.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context> | ||||
|           <context context-type="linenumber">38</context> | ||||
|           <context context-type="linenumber">46</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="7593728289020204896" datatype="html"> | ||||
|         <source>Not assigned</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context> | ||||
|           <context context-type="linenumber">262</context> | ||||
|           <context context-type="linenumber">321</context> | ||||
|         </context-group> | ||||
|         <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note> | ||||
|       </trans-unit> | ||||
| @@ -2174,7 +2188,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">162</context> | ||||
|           <context context-type="linenumber">172</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||
| @@ -3331,7 +3345,7 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">167</context> | ||||
|           <context context-type="linenumber">177</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||
| @@ -3385,112 +3399,112 @@ | ||||
|         <source>Correspondent: <x id="PH" equiv-text="this.correspondents.find((c) => c.id == +rule.value)?.name"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">98,100</context> | ||||
|           <context context-type="linenumber">108,110</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8170755470576301659" datatype="html"> | ||||
|         <source>Without correspondent</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">102</context> | ||||
|           <context context-type="linenumber">112</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8705701325879965907" datatype="html"> | ||||
|         <source>Type: <x id="PH" equiv-text="this.documentTypes.find((dt) => dt.id == +rule.value)?.name"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">107,109</context> | ||||
|           <context context-type="linenumber">117,119</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4362173610367509215" datatype="html"> | ||||
|         <source>Without document type</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">111</context> | ||||
|           <context context-type="linenumber">121</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8180755793012580465" datatype="html"> | ||||
|         <source>Tag: <x id="PH" equiv-text="this.tags.find((t) => t.id == +rule.value)?.name"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">115,117</context> | ||||
|           <context context-type="linenumber">125,127</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6494566478302448576" datatype="html"> | ||||
|         <source>Without any tag</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">121</context> | ||||
|           <context context-type="linenumber">131</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6523384805359286307" datatype="html"> | ||||
|         <source>Title: <x id="PH" equiv-text="rule.value"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">125</context> | ||||
|           <context context-type="linenumber">135</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1872523635812236432" datatype="html"> | ||||
|         <source>ASN: <x id="PH" equiv-text="rule.value"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">128</context> | ||||
|           <context context-type="linenumber">138</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3100631071441658964" datatype="html"> | ||||
|         <source>Title & content</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">165</context> | ||||
|           <context context-type="linenumber">175</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1010505078885609376" datatype="html"> | ||||
|         <source>Advanced search</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">170</context> | ||||
|           <context context-type="linenumber">180</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2649431021108393503" datatype="html"> | ||||
|         <source>More like</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">176</context> | ||||
|           <context context-type="linenumber">186</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3697582909018473071" datatype="html"> | ||||
|         <source>equals</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">195</context> | ||||
|           <context context-type="linenumber">205</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5325481293405718739" datatype="html"> | ||||
|         <source>is empty</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">199</context> | ||||
|           <context context-type="linenumber">209</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6166785695326182482" datatype="html"> | ||||
|         <source>is not empty</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">203</context> | ||||
|           <context context-type="linenumber">213</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4686622206659266699" datatype="html"> | ||||
|         <source>greater than</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">207</context> | ||||
|           <context context-type="linenumber">217</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8014012170270529279" datatype="html"> | ||||
|         <source>less than</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> | ||||
|           <context context-type="linenumber">211</context> | ||||
|           <context context-type="linenumber">221</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="7210076240260527720" datatype="html"> | ||||
|   | ||||
| @@ -1,21 +1,29 @@ | ||||
| <div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown"> | ||||
|   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled"> | ||||
|   <button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled"> | ||||
|     <svg class="toolbaricon" fill="currentColor"> | ||||
|       <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> | ||||
|     </svg> | ||||
|     <div class="d-none d-sm-inline"> {{title}}</div> | ||||
|     <ng-container *ngIf="!editing && selectionModel.totalCount > 0"> | ||||
|       <app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge> | ||||
|       <app-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge> | ||||
|     </ng-container> | ||||
|   </button> | ||||
|   <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> | ||||
|   <div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}"> | ||||
|     <div class="list-group list-group-flush"> | ||||
|       <div *ngIf="!editing && multiple" class="list-group-item d-flex"> | ||||
|         <div class="btn-group btn-group-xs flex-fill"> | ||||
|           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd" value="and"> | ||||
|           <label class="btn btn-outline-primary" for="logicalOperatorAnd" i18n>All</label> | ||||
|           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr" value="or"> | ||||
|           <label class="btn btn-outline-primary" for="logicalOperatorOr" i18n>Any</label> | ||||
|       <div *ngIf="!editing && manyToOne" class="list-group-item d-flex"> | ||||
|         <div class="btn-group btn-group-xs flex-fill" role="group"> | ||||
|           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd_{{name}}" name="logicalOperatorAnd_{{name}}" value="and"> | ||||
|           <label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label> | ||||
|           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr_{{name}}" name="logicalOperatorOr_{{name}}" value="or"> | ||||
|           <label class="btn btn-outline-primary" for="logicalOperatorOr_{{name}}" i18n>Any</label> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div *ngIf="!editing && !manyToOne" class="list-group-item d-flex"> | ||||
|         <div class="btn-group btn-group-xs flex-fill" role="group"> | ||||
|           <input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionInclude_{{name}}" name="intersectionInclude_{{name}}" value="include"> | ||||
|           <label class="btn btn-outline-primary" for="intersectionInclude_{{name}}" i18n>Include</label> | ||||
|           <input [(ngModel)]="selectionModel.intersection" [disabled]="!modifierToggleEnabled" (ngModelChange)="selectionModel.toggleIntersection()" type="radio" class="btn-check" id="intersectionExclude_{{name}}" name="intersectionExclude_{{name}}" value="exclude"> | ||||
|           <label class="btn btn-outline-primary" for="intersectionExclude_{{name}}" i18n>Exclude</label> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="list-group-item"> | ||||
| @@ -34,7 +42,7 @@ | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> | ||||
|         </svg> | ||||
|       </button> | ||||
|       <div *ngIf="!editing && multiple" class="list-group-item list-group-item-note pt-1 pb-2"> | ||||
|       <div *ngIf="!editing && manyToOne" class="list-group-item list-group-item-note pt-1 pb-2"> | ||||
|         <small i18n>Click again to exclude items.</small> | ||||
|       </div> | ||||
|     </div> | ||||
|   | ||||
| @@ -18,12 +18,25 @@ export interface ChangedItems { | ||||
|   itemsToRemove: MatchingModel[] | ||||
| } | ||||
|  | ||||
| export enum LogicalOperator { | ||||
|   And = 'and', | ||||
|   Or = 'or', | ||||
| } | ||||
|  | ||||
| export enum Intersection { | ||||
|   Include = 'include', | ||||
|   Exclude = 'exclude', | ||||
| } | ||||
|  | ||||
| export class FilterableDropdownSelectionModel { | ||||
|   changed = new Subject<FilterableDropdownSelectionModel>() | ||||
|  | ||||
|   multiple = false | ||||
|   private _logicalOperator = 'and' | ||||
|   temporaryLogicalOperator = this._logicalOperator | ||||
|   manyToOne = false | ||||
|   singleSelect = false | ||||
|   private _logicalOperator: LogicalOperator = LogicalOperator.And | ||||
|   temporaryLogicalOperator: LogicalOperator = this._logicalOperator | ||||
|   private _intersection: Intersection = Intersection.Include | ||||
|   temporaryIntersection: Intersection = this._intersection | ||||
|  | ||||
|   items: MatchingModel[] = [] | ||||
|  | ||||
| @@ -86,7 +99,30 @@ export class FilterableDropdownSelectionModel { | ||||
|       (state != ToggleableItemState.Selected && | ||||
|         state != ToggleableItemState.Excluded) | ||||
|     ) { | ||||
|       this.temporarySelectionStates.set(id, ToggleableItemState.Selected) | ||||
|       if (this.manyToOne || this.singleSelect) { | ||||
|         this.temporarySelectionStates.set(id, ToggleableItemState.Selected) | ||||
|  | ||||
|         if (this.singleSelect) { | ||||
|           for (let key of this.temporarySelectionStates.keys()) { | ||||
|             if (key != id) { | ||||
|               this.temporarySelectionStates.delete(key) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         let newState = | ||||
|           this.intersection == Intersection.Include | ||||
|             ? ToggleableItemState.Selected | ||||
|             : ToggleableItemState.Excluded | ||||
|         if (!id) newState = ToggleableItemState.Selected | ||||
|         if ( | ||||
|           state == ToggleableItemState.Excluded && | ||||
|           this.intersection == Intersection.Exclude | ||||
|         ) { | ||||
|           newState = ToggleableItemState.NotSelected | ||||
|         } | ||||
|         this.temporarySelectionStates.set(id, newState) | ||||
|       } | ||||
|     } else if ( | ||||
|       state == ToggleableItemState.Selected || | ||||
|       state == ToggleableItemState.Excluded | ||||
| @@ -94,14 +130,6 @@ export class FilterableDropdownSelectionModel { | ||||
|       this.temporarySelectionStates.delete(id) | ||||
|     } | ||||
|  | ||||
|     if (!this.multiple) { | ||||
|       for (let key of this.temporarySelectionStates.keys()) { | ||||
|         if (key != id) { | ||||
|           this.temporarySelectionStates.delete(key) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (!id) { | ||||
|       for (let key of this.temporarySelectionStates.keys()) { | ||||
|         if (key) { | ||||
| @@ -119,19 +147,36 @@ export class FilterableDropdownSelectionModel { | ||||
|  | ||||
|   exclude(id: number, fireEvent: boolean = true) { | ||||
|     let state = this.temporarySelectionStates.get(id) | ||||
|     if (state == null || state != ToggleableItemState.Excluded) { | ||||
|       this.temporarySelectionStates.set(id, ToggleableItemState.Excluded) | ||||
|       this.temporaryLogicalOperator = this._logicalOperator = 'and' | ||||
|     } else if (state == ToggleableItemState.Excluded) { | ||||
|       this.temporarySelectionStates.delete(id) | ||||
|     } | ||||
|     if (id && (state == null || state != ToggleableItemState.Excluded)) { | ||||
|       this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne | ||||
|         ? LogicalOperator.And | ||||
|         : LogicalOperator.Or | ||||
|  | ||||
|     if (!this.multiple) { | ||||
|       for (let key of this.temporarySelectionStates.keys()) { | ||||
|         if (key != id) { | ||||
|           this.temporarySelectionStates.delete(key) | ||||
|       if (this.manyToOne || this.singleSelect) { | ||||
|         this.temporarySelectionStates.set(id, ToggleableItemState.Excluded) | ||||
|  | ||||
|         if (this.singleSelect) { | ||||
|           for (let key of this.temporarySelectionStates.keys()) { | ||||
|             if (key != id) { | ||||
|               this.temporarySelectionStates.delete(key) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         let newState = | ||||
|           this.intersection == Intersection.Include | ||||
|             ? ToggleableItemState.Selected | ||||
|             : ToggleableItemState.Excluded | ||||
|         if ( | ||||
|           state == ToggleableItemState.Selected && | ||||
|           this.intersection == Intersection.Include | ||||
|         ) { | ||||
|           newState = ToggleableItemState.NotSelected | ||||
|         } | ||||
|         this.temporarySelectionStates.set(id, newState) | ||||
|       } | ||||
|     } else if (!id || state == ToggleableItemState.Excluded) { | ||||
|       this.temporarySelectionStates.delete(id) | ||||
|     } | ||||
|  | ||||
|     if (fireEvent) { | ||||
| @@ -143,11 +188,11 @@ export class FilterableDropdownSelectionModel { | ||||
|     return this.selectionStates.get(id) || ToggleableItemState.NotSelected | ||||
|   } | ||||
|  | ||||
|   get logicalOperator(): string { | ||||
|   get logicalOperator(): LogicalOperator { | ||||
|     return this.temporaryLogicalOperator | ||||
|   } | ||||
|  | ||||
|   set logicalOperator(operator: string) { | ||||
|   set logicalOperator(operator: LogicalOperator) { | ||||
|     this.temporaryLogicalOperator = operator | ||||
|   } | ||||
|  | ||||
| @@ -155,6 +200,26 @@ export class FilterableDropdownSelectionModel { | ||||
|     this.changed.next(this) | ||||
|   } | ||||
|  | ||||
|   get intersection(): Intersection { | ||||
|     return this.temporaryIntersection | ||||
|   } | ||||
|  | ||||
|   set intersection(intersection: Intersection) { | ||||
|     this.temporaryIntersection = intersection | ||||
|   } | ||||
|  | ||||
|   toggleIntersection() { | ||||
|     if (this.temporarySelectionStates.size === 0) return | ||||
|     let newState = | ||||
|       this.intersection == Intersection.Include | ||||
|         ? ToggleableItemState.Selected | ||||
|         : ToggleableItemState.Excluded | ||||
|     this.temporarySelectionStates.forEach((state, key) => { | ||||
|       this.temporarySelectionStates.set(key, newState) | ||||
|     }) | ||||
|     this.changed.next(this) | ||||
|   } | ||||
|  | ||||
|   get(id: number) { | ||||
|     return ( | ||||
|       this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected | ||||
| @@ -171,7 +236,8 @@ export class FilterableDropdownSelectionModel { | ||||
|  | ||||
|   clear(fireEvent = true) { | ||||
|     this.temporarySelectionStates.clear() | ||||
|     this.temporaryLogicalOperator = this._logicalOperator = 'and' | ||||
|     this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And | ||||
|     this.temporaryIntersection = this._intersection = Intersection.Include | ||||
|     if (fireEvent) { | ||||
|       this.changed.next(this) | ||||
|     } | ||||
| @@ -194,6 +260,8 @@ export class FilterableDropdownSelectionModel { | ||||
|       return true | ||||
|     } else if (this.temporaryLogicalOperator !== this._logicalOperator) { | ||||
|       return true | ||||
|     } else if (this.temporaryIntersection !== this._intersection) { | ||||
|       return true | ||||
|     } else { | ||||
|       return false | ||||
|     } | ||||
| @@ -217,13 +285,18 @@ export class FilterableDropdownSelectionModel { | ||||
|       this.selectionStates.set(key, value) | ||||
|     }) | ||||
|     this._logicalOperator = this.temporaryLogicalOperator | ||||
|     this._intersection = this.temporaryIntersection | ||||
|   } | ||||
|  | ||||
|   reset() { | ||||
|   reset(complete: boolean = false) { | ||||
|     this.temporarySelectionStates.clear() | ||||
|     this.selectionStates.forEach((value, key) => { | ||||
|       this.temporarySelectionStates.set(key, value) | ||||
|     }) | ||||
|     if (complete) { | ||||
|       this.selectionStates.clear() | ||||
|     } else { | ||||
|       this.selectionStates.forEach((value, key) => { | ||||
|         this.temporarySelectionStates.set(key, value) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   diff(): ChangedItems { | ||||
| @@ -269,14 +342,16 @@ export class FilterableDropdownComponent { | ||||
|     return this._selectionModel.items | ||||
|   } | ||||
|  | ||||
|   _selectionModel = new FilterableDropdownSelectionModel() | ||||
|   _selectionModel: FilterableDropdownSelectionModel = | ||||
|     new FilterableDropdownSelectionModel() | ||||
|  | ||||
|   @Input() | ||||
|   set selectionModel(model: FilterableDropdownSelectionModel) { | ||||
|     if (this.selectionModel) { | ||||
|       this.selectionModel.changed.complete() | ||||
|       model.items = this.selectionModel.items | ||||
|       model.multiple = this.selectionModel.multiple | ||||
|       model.manyToOne = this.selectionModel.manyToOne | ||||
|       model.singleSelect = this.editing && !this.selectionModel.manyToOne | ||||
|     } | ||||
|     model.changed.subscribe((updatedModel) => { | ||||
|       this.selectionModelChange.next(updatedModel) | ||||
| @@ -292,12 +367,12 @@ export class FilterableDropdownComponent { | ||||
|   selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>() | ||||
|  | ||||
|   @Input() | ||||
|   set multiple(value: boolean) { | ||||
|     this.selectionModel.multiple = value | ||||
|   set manyToOne(manyToOne: boolean) { | ||||
|     this.selectionModel.manyToOne = manyToOne | ||||
|   } | ||||
|  | ||||
|   get multiple() { | ||||
|     return this.selectionModel.multiple | ||||
|   get manyToOne() { | ||||
|     return this.selectionModel.manyToOne | ||||
|   } | ||||
|  | ||||
|   @Input() | ||||
| @@ -327,16 +402,20 @@ export class FilterableDropdownComponent { | ||||
|   @Output() | ||||
|   opened = new EventEmitter() | ||||
|  | ||||
|   get operatorToggleEnabled(): boolean { | ||||
|     return ( | ||||
|       this.selectionModel.selectionSize() > 1 && | ||||
|       this.selectionModel.getExcludedItems().length == 0 | ||||
|     ) | ||||
|   get modifierToggleEnabled(): boolean { | ||||
|     return this.manyToOne | ||||
|       ? this.selectionModel.selectionSize() > 1 && | ||||
|           this.selectionModel.getExcludedItems().length == 0 | ||||
|       : !this.selectionModel.isNoneSelected() | ||||
|   } | ||||
|  | ||||
|   @Input() | ||||
|   documentCounts: SelectionDataItem[] | ||||
|  | ||||
|   get name(): string { | ||||
|     return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null | ||||
|   } | ||||
|  | ||||
|   getUpdatedDocumentCount(id: number) { | ||||
|     if (this.documentCounts) { | ||||
|       return this.documentCounts.find((c) => c.id === id)?.document_count | ||||
| @@ -346,7 +425,6 @@ export class FilterableDropdownComponent { | ||||
|   modelIsDirty: boolean = false | ||||
|  | ||||
|   constructor(private filterPipe: FilterPipe) { | ||||
|     this.selectionModel = new FilterableDropdownSelectionModel() | ||||
|     this.selectionModelChange.subscribe((updatedModel) => { | ||||
|       this.modelIsDirty = updatedModel.isDirty() | ||||
|     }) | ||||
| @@ -400,7 +478,7 @@ export class FilterableDropdownComponent { | ||||
|   } | ||||
|  | ||||
|   reset() { | ||||
|     this.selectionModel.reset() | ||||
|     this.selectionModel.reset(true) | ||||
|     this.selectionModelChange.emit(this.selectionModel) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,7 @@ | ||||
|         [items]="tags" | ||||
|         [disabled]="!userCanEditAll" | ||||
|         [editing]="true" | ||||
|         [multiple]="true" | ||||
|         [manyToOne]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (opened)="openTagsDropdown()" | ||||
|         [(selectionModel)]="tagSelectionModel" | ||||
|   | ||||
| @@ -14,19 +14,19 @@ | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="btn-group flex-fill" role="group"> | ||||
|     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails"> | ||||
|     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="details" (ngModelChange)="saveDisplayMode()" id="displayModeDetails" name="displayModeDetails"> | ||||
|     <label for="displayModeDetails" class="btn btn-outline-primary btn-sm"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#list-ul" /> | ||||
|       </svg> | ||||
|     </label> | ||||
|     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall"> | ||||
|     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="smallCards" (ngModelChange)="saveDisplayMode()" id="displayModeSmall" name="displayModeSmall"> | ||||
|     <label for="displayModeSmall" class="btn btn-outline-primary btn-sm"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#grid" /> | ||||
|       </svg> | ||||
|     </label> | ||||
|     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge"> | ||||
|     <input type="radio" class="btn-check" [(ngModel)]="displayMode" value="largeCards" (ngModelChange)="saveDisplayMode()" id="displayModeLarge" name="displayModeLarge"> | ||||
|     <label for="displayModeLarge" class="btn btn-outline-primary btn-sm"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#hdd-stack" /> | ||||
|   | ||||
| @@ -27,7 +27,7 @@ | ||||
|           <app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title | ||||
|             filterPlaceholder="Filter tags" i18n-filterPlaceholder | ||||
|             [items]="tags" | ||||
|             [multiple]="true" | ||||
|             [manyToOne]="true" | ||||
|             [(selectionModel)]="tagSelectionModel" | ||||
|             (selectionModelChange)="updateRules()" | ||||
|             (opened)="onTagsDropdownOpen()" | ||||
|   | ||||
| @@ -21,10 +21,10 @@ import { | ||||
|   FILTER_ADDED_AFTER, | ||||
|   FILTER_ADDED_BEFORE, | ||||
|   FILTER_ASN, | ||||
|   FILTER_CORRESPONDENT, | ||||
|   FILTER_HAS_CORRESPONDENT_ANY, | ||||
|   FILTER_CREATED_AFTER, | ||||
|   FILTER_CREATED_BEFORE, | ||||
|   FILTER_DOCUMENT_TYPE, | ||||
|   FILTER_HAS_DOCUMENT_TYPE_ANY, | ||||
|   FILTER_FULLTEXT_MORELIKE, | ||||
|   FILTER_FULLTEXT_QUERY, | ||||
|   FILTER_HAS_ANY_TAG, | ||||
| @@ -33,12 +33,22 @@ import { | ||||
|   FILTER_DOES_NOT_HAVE_TAG, | ||||
|   FILTER_TITLE, | ||||
|   FILTER_TITLE_CONTENT, | ||||
|   FILTER_STORAGE_PATH, | ||||
|   FILTER_HAS_STORAGE_PATH_ANY, | ||||
|   FILTER_ASN_ISNULL, | ||||
|   FILTER_ASN_GT, | ||||
|   FILTER_ASN_LT, | ||||
|   FILTER_DOES_NOT_HAVE_CORRESPONDENT, | ||||
|   FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE, | ||||
|   FILTER_DOES_NOT_HAVE_STORAGE_PATH, | ||||
|   FILTER_DOCUMENT_TYPE, | ||||
|   FILTER_CORRESPONDENT, | ||||
|   FILTER_STORAGE_PATH, | ||||
| } from 'src/app/data/filter-rule-type' | ||||
| import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component' | ||||
| import { | ||||
|   FilterableDropdownSelectionModel, | ||||
|   Intersection, | ||||
|   LogicalOperator, | ||||
| } from '../../common/filterable-dropdown/filterable-dropdown.component' | ||||
| import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component' | ||||
| import { | ||||
|   DocumentService, | ||||
| @@ -93,7 +103,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     if (this.filterRules.length == 1) { | ||||
|       let rule = this.filterRules[0] | ||||
|       switch (this.filterRules[0].rule_type) { | ||||
|         case FILTER_CORRESPONDENT: | ||||
|         case FILTER_HAS_CORRESPONDENT_ANY: | ||||
|           if (rule.value) { | ||||
|             return $localize`Correspondent: ${ | ||||
|               this.correspondents.find((c) => c.id == +rule.value)?.name | ||||
| @@ -102,7 +112,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|             return $localize`Without correspondent` | ||||
|           } | ||||
|  | ||||
|         case FILTER_DOCUMENT_TYPE: | ||||
|         case FILTER_HAS_DOCUMENT_TYPE_ANY: | ||||
|           if (rule.value) { | ||||
|             return $localize`Type: ${ | ||||
|               this.documentTypes.find((dt) => dt.id == +rule.value)?.name | ||||
| @@ -335,6 +345,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|           this.dateAddedBefore = rule.value | ||||
|           break | ||||
|         case FILTER_HAS_TAGS_ALL: | ||||
|           this.tagSelectionModel.logicalOperator = LogicalOperator.And | ||||
|           this.tagSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Selected, | ||||
| @@ -342,7 +353,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_HAS_TAGS_ANY: | ||||
|           this.tagSelectionModel.logicalOperator = 'or' | ||||
|           this.tagSelectionModel.logicalOperator = LogicalOperator.Or | ||||
|           this.tagSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Selected, | ||||
| @@ -360,26 +371,59 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_CORRESPONDENT: | ||||
|         case FILTER_HAS_CORRESPONDENT_ANY: | ||||
|           this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or | ||||
|           this.correspondentSelectionModel.intersection = Intersection.Include | ||||
|           this.correspondentSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Selected, | ||||
|             false | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_DOES_NOT_HAVE_CORRESPONDENT: | ||||
|           this.correspondentSelectionModel.intersection = Intersection.Exclude | ||||
|           this.correspondentSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Excluded, | ||||
|             false | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_DOCUMENT_TYPE: | ||||
|         case FILTER_HAS_DOCUMENT_TYPE_ANY: | ||||
|           this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or | ||||
|           this.documentTypeSelectionModel.intersection = Intersection.Include | ||||
|           this.documentTypeSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Selected, | ||||
|             false | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE: | ||||
|           this.documentTypeSelectionModel.intersection = Intersection.Exclude | ||||
|           this.documentTypeSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Excluded, | ||||
|             false | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_STORAGE_PATH: | ||||
|         case FILTER_HAS_STORAGE_PATH_ANY: | ||||
|           this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or | ||||
|           this.storagePathSelectionModel.intersection = Intersection.Include | ||||
|           this.storagePathSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Selected, | ||||
|             false | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_DOES_NOT_HAVE_STORAGE_PATH: | ||||
|           this.storagePathSelectionModel.intersection = Intersection.Exclude | ||||
|           this.storagePathSelectionModel.set( | ||||
|             rule.value ? +rule.value : null, | ||||
|             ToggleableItemState.Excluded, | ||||
|             false | ||||
|           ) | ||||
|           break | ||||
|         case FILTER_ASN_ISNULL: | ||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_ASN | ||||
|           this.textFilterModifier = | ||||
| @@ -469,7 +513,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|       filterRules.push({ rule_type: FILTER_HAS_ANY_TAG, value: 'false' }) | ||||
|     } else { | ||||
|       const tagFilterType = | ||||
|         this.tagSelectionModel.logicalOperator == 'and' | ||||
|         this.tagSelectionModel.logicalOperator == LogicalOperator.And | ||||
|           ? FILTER_HAS_TAGS_ALL | ||||
|           : FILTER_HAS_TAGS_ANY | ||||
|       this.tagSelectionModel | ||||
| @@ -491,28 +535,66 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|           }) | ||||
|         }) | ||||
|     } | ||||
|     this.correspondentSelectionModel | ||||
|       .getSelectedItems() | ||||
|       .forEach((correspondent) => { | ||||
|         filterRules.push({ | ||||
|           rule_type: FILTER_CORRESPONDENT, | ||||
|           value: correspondent.id?.toString(), | ||||
|     if (this.correspondentSelectionModel.isNoneSelected()) { | ||||
|       filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null }) | ||||
|     } else { | ||||
|       this.correspondentSelectionModel | ||||
|         .getSelectedItems() | ||||
|         .forEach((correspondent) => { | ||||
|           filterRules.push({ | ||||
|             rule_type: FILTER_HAS_CORRESPONDENT_ANY, | ||||
|             value: correspondent.id?.toString(), | ||||
|           }) | ||||
|         }) | ||||
|       }) | ||||
|     this.documentTypeSelectionModel | ||||
|       .getSelectedItems() | ||||
|       .forEach((documentType) => { | ||||
|         filterRules.push({ | ||||
|           rule_type: FILTER_DOCUMENT_TYPE, | ||||
|           value: documentType.id?.toString(), | ||||
|       this.correspondentSelectionModel | ||||
|         .getExcludedItems() | ||||
|         .forEach((correspondent) => { | ||||
|           filterRules.push({ | ||||
|             rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT, | ||||
|             value: correspondent.id?.toString(), | ||||
|           }) | ||||
|         }) | ||||
|       }) | ||||
|     this.storagePathSelectionModel.getSelectedItems().forEach((storagePath) => { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_STORAGE_PATH, | ||||
|         value: storagePath.id?.toString(), | ||||
|       }) | ||||
|     }) | ||||
|     } | ||||
|     if (this.documentTypeSelectionModel.isNoneSelected()) { | ||||
|       filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null }) | ||||
|     } else { | ||||
|       this.documentTypeSelectionModel | ||||
|         .getSelectedItems() | ||||
|         .forEach((documentType) => { | ||||
|           filterRules.push({ | ||||
|             rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY, | ||||
|             value: documentType.id?.toString(), | ||||
|           }) | ||||
|         }) | ||||
|       this.documentTypeSelectionModel | ||||
|         .getExcludedItems() | ||||
|         .forEach((documentType) => { | ||||
|           filterRules.push({ | ||||
|             rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE, | ||||
|             value: documentType.id?.toString(), | ||||
|           }) | ||||
|         }) | ||||
|     } | ||||
|     if (this.storagePathSelectionModel.isNoneSelected()) { | ||||
|       filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null }) | ||||
|     } else { | ||||
|       this.storagePathSelectionModel | ||||
|         .getSelectedItems() | ||||
|         .forEach((storagePath) => { | ||||
|           filterRules.push({ | ||||
|             rule_type: FILTER_HAS_STORAGE_PATH_ANY, | ||||
|             value: storagePath.id?.toString(), | ||||
|           }) | ||||
|         }) | ||||
|       this.storagePathSelectionModel | ||||
|         .getExcludedItems() | ||||
|         .forEach((storagePath) => { | ||||
|           filterRules.push({ | ||||
|             rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH, | ||||
|             value: storagePath.id?.toString(), | ||||
|           }) | ||||
|         }) | ||||
|     } | ||||
|     if (this.dateCreatedBefore) { | ||||
|       filterRules.push({ | ||||
|         rule_type: FILTER_CREATED_BEFORE, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type' | ||||
| import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||
| import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| @@ -35,7 +35,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles | ||||
|       toastService, | ||||
|       documentListViewService, | ||||
|       permissionsService, | ||||
|       FILTER_CORRESPONDENT, | ||||
|       FILTER_HAS_CORRESPONDENT_ANY, | ||||
|       $localize`correspondent`, | ||||
|       $localize`correspondents`, | ||||
|       PermissionType.Correspondent, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type' | ||||
| import { FILTER_HAS_DOCUMENT_TYPE_ANY } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { | ||||
| @@ -32,7 +32,7 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless | ||||
|       toastService, | ||||
|       documentListViewService, | ||||
|       permissionsService, | ||||
|       FILTER_DOCUMENT_TYPE, | ||||
|       FILTER_HAS_DOCUMENT_TYPE_ANY, | ||||
|       $localize`document type`, | ||||
|       $localize`document types`, | ||||
|       PermissionType.DocumentType, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type' | ||||
| import { FILTER_HAS_STORAGE_PATH_ANY } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||
| import { | ||||
| @@ -32,7 +32,7 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS | ||||
|       toastService, | ||||
|       documentListViewService, | ||||
|       permissionsService, | ||||
|       FILTER_STORAGE_PATH, | ||||
|       FILTER_HAS_STORAGE_PATH_ANY, | ||||
|       $localize`storage path`, | ||||
|       $localize`storage paths`, | ||||
|       PermissionType.StoragePath, | ||||
|   | ||||
| @@ -8,8 +8,12 @@ export const FILTER_ASN_GT = 23 | ||||
| export const FILTER_ASN_LT = 24 | ||||
|  | ||||
| export const FILTER_CORRESPONDENT = 3 | ||||
| export const FILTER_HAS_CORRESPONDENT_ANY = 26 | ||||
| export const FILTER_DOES_NOT_HAVE_CORRESPONDENT = 27 | ||||
|  | ||||
| export const FILTER_DOCUMENT_TYPE = 4 | ||||
| export const FILTER_HAS_DOCUMENT_TYPE_ANY = 28 | ||||
| export const FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE = 29 | ||||
|  | ||||
| export const FILTER_IS_IN_INBOX = 5 | ||||
| export const FILTER_HAS_TAGS_ALL = 6 | ||||
| @@ -18,6 +22,8 @@ export const FILTER_DOES_NOT_HAVE_TAG = 17 | ||||
| export const FILTER_HAS_TAGS_ANY = 22 | ||||
|  | ||||
| export const FILTER_STORAGE_PATH = 25 | ||||
| export const FILTER_HAS_STORAGE_PATH_ANY = 30 | ||||
| export const FILTER_DOES_NOT_HAVE_STORAGE_PATH = 31 | ||||
|  | ||||
| export const FILTER_CREATED_BEFORE = 8 | ||||
| export const FILTER_CREATED_AFTER = 9 | ||||
| @@ -63,6 +69,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'correspondent', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_HAS_CORRESPONDENT_ANY, | ||||
|     filtervar: 'correspondent__id__in', | ||||
|     datatype: 'correspondent', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_DOES_NOT_HAVE_CORRESPONDENT, | ||||
|     filtervar: 'correspondent__id__none', | ||||
|     datatype: 'correspondent', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_STORAGE_PATH, | ||||
|     filtervar: 'storage_path__id', | ||||
| @@ -70,6 +88,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'storage_path', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_HAS_STORAGE_PATH_ANY, | ||||
|     filtervar: 'storage_path__id__in', | ||||
|     datatype: 'storage_path', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_DOES_NOT_HAVE_STORAGE_PATH, | ||||
|     filtervar: 'storage_path__id__none', | ||||
|     datatype: 'storage_path', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_DOCUMENT_TYPE, | ||||
|     filtervar: 'document_type__id', | ||||
| @@ -77,6 +107,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|     datatype: 'document_type', | ||||
|     multi: false, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_HAS_DOCUMENT_TYPE_ANY, | ||||
|     filtervar: 'document_type__id__in', | ||||
|     datatype: 'document_type', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE, | ||||
|     filtervar: 'document_type__id__none', | ||||
|     datatype: 'document_type', | ||||
|     multi: true, | ||||
|   }, | ||||
|   { | ||||
|     id: FILTER_IS_IN_INBOX, | ||||
|     filtervar: 'is_in_inbox', | ||||
|   | ||||
| @@ -86,12 +86,12 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params { | ||||
|     let params = {} | ||||
|     for (let rule of filterRules) { | ||||
|       let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type) | ||||
|       if (ruleType.multi) { | ||||
|       if (ruleType.isnull_filtervar && rule.value == null) { | ||||
|         params[ruleType.isnull_filtervar] = 1 | ||||
|       } else if (ruleType.multi) { | ||||
|         params[ruleType.filtervar] = params[ruleType.filtervar] | ||||
|           ? params[ruleType.filtervar] + ',' + rule.value | ||||
|           : rule.value | ||||
|       } else if (ruleType.isnull_filtervar && rule.value == null) { | ||||
|         params[ruleType.isnull_filtervar] = 1 | ||||
|       } else { | ||||
|         params[ruleType.filtervar] = rule.value | ||||
|         if (ruleType.datatype == 'boolean') | ||||
|   | ||||
| @@ -36,29 +36,30 @@ class DocumentTypeFilterSet(FilterSet): | ||||
|         fields = {"name": CHAR_KWARGS} | ||||
|  | ||||
|  | ||||
| class TagsFilter(Filter): | ||||
|     def __init__(self, exclude=False, in_list=False): | ||||
| class ObjectFilter(Filter): | ||||
|     def __init__(self, exclude=False, in_list=False, field_name=""): | ||||
|         super().__init__() | ||||
|         self.exclude = exclude | ||||
|         self.in_list = in_list | ||||
|         self.field_name = field_name | ||||
|  | ||||
|     def filter(self, qs, value): | ||||
|         if not value: | ||||
|             return qs | ||||
|  | ||||
|         try: | ||||
|             tag_ids = [int(x) for x in value.split(",")] | ||||
|             object_ids = [int(x) for x in value.split(",")] | ||||
|         except ValueError: | ||||
|             return qs | ||||
|  | ||||
|         if self.in_list: | ||||
|             qs = qs.filter(tags__id__in=tag_ids).distinct() | ||||
|             qs = qs.filter(**{f"{self.field_name}__id__in": object_ids}).distinct() | ||||
|         else: | ||||
|             for tag_id in tag_ids: | ||||
|             for obj_id in object_ids: | ||||
|                 if self.exclude: | ||||
|                     qs = qs.exclude(tags__id=tag_id) | ||||
|                     qs = qs.exclude(**{f"{self.field_name}__id": obj_id}) | ||||
|                 else: | ||||
|                     qs = qs.filter(tags__id=tag_id) | ||||
|                     qs = qs.filter(**{f"{self.field_name}__id": obj_id}) | ||||
|  | ||||
|         return qs | ||||
|  | ||||
| @@ -90,11 +91,17 @@ class DocumentFilterSet(FilterSet): | ||||
|         exclude=True, | ||||
|     ) | ||||
|  | ||||
|     tags__id__all = TagsFilter() | ||||
|     tags__id__all = ObjectFilter(field_name="tags") | ||||
|  | ||||
|     tags__id__none = TagsFilter(exclude=True) | ||||
|     tags__id__none = ObjectFilter(field_name="tags", exclude=True) | ||||
|  | ||||
|     tags__id__in = TagsFilter(in_list=True) | ||||
|     tags__id__in = ObjectFilter(field_name="tags", in_list=True) | ||||
|  | ||||
|     correspondent__id__none = ObjectFilter(field_name="correspondent", exclude=True) | ||||
|  | ||||
|     document_type__id__none = ObjectFilter(field_name="document_type", exclude=True) | ||||
|  | ||||
|     storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True) | ||||
|  | ||||
|     is_in_inbox = InboxFilter() | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,54 @@ | ||||
| # Generated by Django 4.1.5 on 2023-03-15 07:10 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("documents", "1033_alter_documenttype_options_alter_tag_options_and_more"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="savedviewfilterrule", | ||||
|             name="rule_type", | ||||
|             field=models.PositiveIntegerField( | ||||
|                 choices=[ | ||||
|                     (0, "title contains"), | ||||
|                     (1, "content contains"), | ||||
|                     (2, "ASN is"), | ||||
|                     (3, "correspondent is"), | ||||
|                     (4, "document type is"), | ||||
|                     (5, "is in inbox"), | ||||
|                     (6, "has tag"), | ||||
|                     (7, "has any tag"), | ||||
|                     (8, "created before"), | ||||
|                     (9, "created after"), | ||||
|                     (10, "created year is"), | ||||
|                     (11, "created month is"), | ||||
|                     (12, "created day is"), | ||||
|                     (13, "added before"), | ||||
|                     (14, "added after"), | ||||
|                     (15, "modified before"), | ||||
|                     (16, "modified after"), | ||||
|                     (17, "does not have tag"), | ||||
|                     (18, "does not have ASN"), | ||||
|                     (19, "title or content contains"), | ||||
|                     (20, "fulltext query"), | ||||
|                     (21, "more like this"), | ||||
|                     (22, "has tags in"), | ||||
|                     (23, "ASN greater than"), | ||||
|                     (24, "ASN less than"), | ||||
|                     (25, "storage path is"), | ||||
|                     (26, "has correspondent in"), | ||||
|                     (27, "does not have correspondent in"), | ||||
|                     (28, "has document type in"), | ||||
|                     (29, "does not have document type in"), | ||||
|                     (30, "has storage path in"), | ||||
|                     (31, "does not have storage path in"), | ||||
|                 ], | ||||
|                 verbose_name="rule type", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -447,6 +447,12 @@ class SavedViewFilterRule(models.Model): | ||||
|         (23, _("ASN greater than")), | ||||
|         (24, _("ASN less than")), | ||||
|         (25, _("storage path is")), | ||||
|         (26, _("has correspondent in")), | ||||
|         (27, _("does not have correspondent in")), | ||||
|         (28, _("has document type in")), | ||||
|         (29, _("does not have document type in")), | ||||
|         (30, _("has storage path in")), | ||||
|         (31, _("does not have storage path in")), | ||||
|     ] | ||||
|  | ||||
|     saved_view = models.ForeignKey( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon