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) |             (d.tags as Array<number>).includes(tag_id) | ||||||
|           ) |           ) | ||||||
|           response.count = response.results.length |           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) |         req.reply(response) | ||||||
| @@ -112,6 +132,27 @@ describe('documents-list', () => { | |||||||
|     cy.contains('One document') |     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', () => { |   it('should apply tags', () => { | ||||||
|     cy.get('app-document-card-small:first-of-type').click() |     cy.get('app-document-card-small:first-of-type').click() | ||||||
|     cy.get('app-bulk-editor app-filterable-dropdown[title="Tags"]').within( |     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', () => { |   it('should show a list of documents filtered by document type', () => { | ||||||
|     cy.visit('/documents?sort=created&reverse=true&document_type__id=1') |     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') |     cy.contains('3 documents') | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
| @@ -245,9 +250,14 @@ describe('documents query params', () => { | |||||||
|     cy.contains('2 documents') |     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', () => { |   it('should show a list of documents filtered by no correspondent', () => { | ||||||
|     cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1') |     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', () => { |   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, |             "id": 3, | ||||||
|             "correspondent": null, |             "correspondent": 14, | ||||||
|             "document_type": 1, |             "document_type": 1, | ||||||
|             "storage_path": null, |             "storage_path": null, | ||||||
|             "title": "dolor", |             "title": "dolor", | ||||||
| @@ -64,7 +64,7 @@ | |||||||
|         { |         { | ||||||
|             "id": 4, |             "id": 4, | ||||||
|             "correspondent": 9, |             "correspondent": 9, | ||||||
|             "document_type": 1, |             "document_type": 2, | ||||||
|             "storage_path": null, |             "storage_path": null, | ||||||
|             "title": "sit amet", |             "title": "sit amet", | ||||||
|             "content": "Test document PDF", |             "content": "Test document PDF", | ||||||
|   | |||||||
| @@ -1794,25 +1794,39 @@ | |||||||
|           <context context-type="linenumber">18</context> |           <context context-type="linenumber">18</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </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"> |       <trans-unit id="4391289919356861627" datatype="html"> | ||||||
|         <source>Apply</source> |         <source>Apply</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7780041345210191160" datatype="html"> |       <trans-unit id="7780041345210191160" datatype="html"> | ||||||
|         <source>Click again to exclude items.</source> |         <source>Click again to exclude items.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7593728289020204896" datatype="html"> |       <trans-unit id="7593728289020204896" datatype="html"> | ||||||
|         <source>Not assigned</source> |         <source>Not assigned</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context> |           <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> |         </context-group> | ||||||
|         <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note> |         <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
| @@ -2174,7 +2188,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> | ||||||
| @@ -3331,7 +3345,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/services/rest/document.service.ts</context> |           <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> |         <source>Correspondent: <x id="PH" equiv-text="this.correspondents.find((c) => c.id == +rule.value)?.name"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8170755470576301659" datatype="html"> |       <trans-unit id="8170755470576301659" datatype="html"> | ||||||
|         <source>Without correspondent</source> |         <source>Without correspondent</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8705701325879965907" datatype="html"> |       <trans-unit id="8705701325879965907" datatype="html"> | ||||||
|         <source>Type: <x id="PH" equiv-text="this.documentTypes.find((dt) => dt.id == +rule.value)?.name"/></source> |         <source>Type: <x id="PH" equiv-text="this.documentTypes.find((dt) => dt.id == +rule.value)?.name"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4362173610367509215" datatype="html"> |       <trans-unit id="4362173610367509215" datatype="html"> | ||||||
|         <source>Without document type</source> |         <source>Without document type</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8180755793012580465" datatype="html"> |       <trans-unit id="8180755793012580465" datatype="html"> | ||||||
|         <source>Tag: <x id="PH" equiv-text="this.tags.find((t) => t.id == +rule.value)?.name"/></source> |         <source>Tag: <x id="PH" equiv-text="this.tags.find((t) => t.id == +rule.value)?.name"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6494566478302448576" datatype="html"> |       <trans-unit id="6494566478302448576" datatype="html"> | ||||||
|         <source>Without any tag</source> |         <source>Without any tag</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6523384805359286307" datatype="html"> |       <trans-unit id="6523384805359286307" datatype="html"> | ||||||
|         <source>Title: <x id="PH" equiv-text="rule.value"/></source> |         <source>Title: <x id="PH" equiv-text="rule.value"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1872523635812236432" datatype="html"> |       <trans-unit id="1872523635812236432" datatype="html"> | ||||||
|         <source>ASN: <x id="PH" equiv-text="rule.value"/></source> |         <source>ASN: <x id="PH" equiv-text="rule.value"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3100631071441658964" datatype="html"> |       <trans-unit id="3100631071441658964" datatype="html"> | ||||||
|         <source>Title & content</source> |         <source>Title & content</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1010505078885609376" datatype="html"> |       <trans-unit id="1010505078885609376" datatype="html"> | ||||||
|         <source>Advanced search</source> |         <source>Advanced search</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2649431021108393503" datatype="html"> |       <trans-unit id="2649431021108393503" datatype="html"> | ||||||
|         <source>More like</source> |         <source>More like</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3697582909018473071" datatype="html"> |       <trans-unit id="3697582909018473071" datatype="html"> | ||||||
|         <source>equals</source> |         <source>equals</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5325481293405718739" datatype="html"> |       <trans-unit id="5325481293405718739" datatype="html"> | ||||||
|         <source>is empty</source> |         <source>is empty</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6166785695326182482" datatype="html"> |       <trans-unit id="6166785695326182482" datatype="html"> | ||||||
|         <source>is not empty</source> |         <source>is not empty</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4686622206659266699" datatype="html"> |       <trans-unit id="4686622206659266699" datatype="html"> | ||||||
|         <source>greater than</source> |         <source>greater than</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8014012170270529279" datatype="html"> |       <trans-unit id="8014012170270529279" datatype="html"> | ||||||
|         <source>less than</source> |         <source>less than</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> |           <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> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7210076240260527720" datatype="html"> |       <trans-unit id="7210076240260527720" datatype="html"> | ||||||
|   | |||||||
| @@ -1,21 +1,29 @@ | |||||||
| <div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown"> | <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"> |     <svg class="toolbaricon" fill="currentColor"> | ||||||
|       <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> |       <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> | ||||||
|     </svg> |     </svg> | ||||||
|     <div class="d-none d-sm-inline"> {{title}}</div> |     <div class="d-none d-sm-inline"> {{title}}</div> | ||||||
|     <ng-container *ngIf="!editing && selectionModel.totalCount > 0"> |     <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> |     </ng-container> | ||||||
|   </button> |   </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 class="list-group list-group-flush"> | ||||||
|       <div *ngIf="!editing && multiple" class="list-group-item d-flex"> |       <div *ngIf="!editing && manyToOne" class="list-group-item d-flex"> | ||||||
|         <div class="btn-group btn-group-xs flex-fill"> |         <div class="btn-group btn-group-xs flex-fill" role="group"> | ||||||
|           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorAnd" value="and"> |           <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" i18n>All</label> |           <label class="btn btn-outline-primary" for="logicalOperatorAnd_{{name}}" i18n>All</label> | ||||||
|           <input [(ngModel)]="selectionModel.logicalOperator" [disabled]="!operatorToggleEnabled" (ngModelChange)="selectionModel.toggleOperator()" type="radio" class="btn-check" id="logicalOperatorOr" value="or"> |           <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" i18n>Any</label> |           <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> |       </div> | ||||||
|       <div class="list-group-item"> |       <div class="list-group-item"> | ||||||
| @@ -34,7 +42,7 @@ | |||||||
|           <use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> |           <use xlink:href="assets/bootstrap-icons.svg#arrow-right" /> | ||||||
|         </svg> |         </svg> | ||||||
|       </button> |       </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> |         <small i18n>Click again to exclude items.</small> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   | |||||||
| @@ -18,12 +18,25 @@ export interface ChangedItems { | |||||||
|   itemsToRemove: MatchingModel[] |   itemsToRemove: MatchingModel[] | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export enum LogicalOperator { | ||||||
|  |   And = 'and', | ||||||
|  |   Or = 'or', | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export enum Intersection { | ||||||
|  |   Include = 'include', | ||||||
|  |   Exclude = 'exclude', | ||||||
|  | } | ||||||
|  |  | ||||||
| export class FilterableDropdownSelectionModel { | export class FilterableDropdownSelectionModel { | ||||||
|   changed = new Subject<FilterableDropdownSelectionModel>() |   changed = new Subject<FilterableDropdownSelectionModel>() | ||||||
|  |  | ||||||
|   multiple = false |   manyToOne = false | ||||||
|   private _logicalOperator = 'and' |   singleSelect = false | ||||||
|   temporaryLogicalOperator = this._logicalOperator |   private _logicalOperator: LogicalOperator = LogicalOperator.And | ||||||
|  |   temporaryLogicalOperator: LogicalOperator = this._logicalOperator | ||||||
|  |   private _intersection: Intersection = Intersection.Include | ||||||
|  |   temporaryIntersection: Intersection = this._intersection | ||||||
|  |  | ||||||
|   items: MatchingModel[] = [] |   items: MatchingModel[] = [] | ||||||
|  |  | ||||||
| @@ -86,7 +99,30 @@ export class FilterableDropdownSelectionModel { | |||||||
|       (state != ToggleableItemState.Selected && |       (state != ToggleableItemState.Selected && | ||||||
|         state != ToggleableItemState.Excluded) |         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 ( |     } else if ( | ||||||
|       state == ToggleableItemState.Selected || |       state == ToggleableItemState.Selected || | ||||||
|       state == ToggleableItemState.Excluded |       state == ToggleableItemState.Excluded | ||||||
| @@ -94,14 +130,6 @@ export class FilterableDropdownSelectionModel { | |||||||
|       this.temporarySelectionStates.delete(id) |       this.temporarySelectionStates.delete(id) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!this.multiple) { |  | ||||||
|       for (let key of this.temporarySelectionStates.keys()) { |  | ||||||
|         if (key != id) { |  | ||||||
|           this.temporarySelectionStates.delete(key) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!id) { |     if (!id) { | ||||||
|       for (let key of this.temporarySelectionStates.keys()) { |       for (let key of this.temporarySelectionStates.keys()) { | ||||||
|         if (key) { |         if (key) { | ||||||
| @@ -119,19 +147,36 @@ export class FilterableDropdownSelectionModel { | |||||||
|  |  | ||||||
|   exclude(id: number, fireEvent: boolean = true) { |   exclude(id: number, fireEvent: boolean = true) { | ||||||
|     let state = this.temporarySelectionStates.get(id) |     let state = this.temporarySelectionStates.get(id) | ||||||
|     if (state == null || state != ToggleableItemState.Excluded) { |     if (id && (state == null || state != ToggleableItemState.Excluded)) { | ||||||
|       this.temporarySelectionStates.set(id, ToggleableItemState.Excluded) |       this.temporaryLogicalOperator = this._logicalOperator = this.manyToOne | ||||||
|       this.temporaryLogicalOperator = this._logicalOperator = 'and' |         ? LogicalOperator.And | ||||||
|     } else if (state == ToggleableItemState.Excluded) { |         : LogicalOperator.Or | ||||||
|       this.temporarySelectionStates.delete(id) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!this.multiple) { |       if (this.manyToOne || this.singleSelect) { | ||||||
|       for (let key of this.temporarySelectionStates.keys()) { |         this.temporarySelectionStates.set(id, ToggleableItemState.Excluded) | ||||||
|         if (key != id) { |  | ||||||
|           this.temporarySelectionStates.delete(key) |         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) { |     if (fireEvent) { | ||||||
| @@ -143,11 +188,11 @@ export class FilterableDropdownSelectionModel { | |||||||
|     return this.selectionStates.get(id) || ToggleableItemState.NotSelected |     return this.selectionStates.get(id) || ToggleableItemState.NotSelected | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get logicalOperator(): string { |   get logicalOperator(): LogicalOperator { | ||||||
|     return this.temporaryLogicalOperator |     return this.temporaryLogicalOperator | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   set logicalOperator(operator: string) { |   set logicalOperator(operator: LogicalOperator) { | ||||||
|     this.temporaryLogicalOperator = operator |     this.temporaryLogicalOperator = operator | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -155,6 +200,26 @@ export class FilterableDropdownSelectionModel { | |||||||
|     this.changed.next(this) |     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) { |   get(id: number) { | ||||||
|     return ( |     return ( | ||||||
|       this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected |       this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected | ||||||
| @@ -171,7 +236,8 @@ export class FilterableDropdownSelectionModel { | |||||||
|  |  | ||||||
|   clear(fireEvent = true) { |   clear(fireEvent = true) { | ||||||
|     this.temporarySelectionStates.clear() |     this.temporarySelectionStates.clear() | ||||||
|     this.temporaryLogicalOperator = this._logicalOperator = 'and' |     this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And | ||||||
|  |     this.temporaryIntersection = this._intersection = Intersection.Include | ||||||
|     if (fireEvent) { |     if (fireEvent) { | ||||||
|       this.changed.next(this) |       this.changed.next(this) | ||||||
|     } |     } | ||||||
| @@ -194,6 +260,8 @@ export class FilterableDropdownSelectionModel { | |||||||
|       return true |       return true | ||||||
|     } else if (this.temporaryLogicalOperator !== this._logicalOperator) { |     } else if (this.temporaryLogicalOperator !== this._logicalOperator) { | ||||||
|       return true |       return true | ||||||
|  |     } else if (this.temporaryIntersection !== this._intersection) { | ||||||
|  |       return true | ||||||
|     } else { |     } else { | ||||||
|       return false |       return false | ||||||
|     } |     } | ||||||
| @@ -217,13 +285,18 @@ export class FilterableDropdownSelectionModel { | |||||||
|       this.selectionStates.set(key, value) |       this.selectionStates.set(key, value) | ||||||
|     }) |     }) | ||||||
|     this._logicalOperator = this.temporaryLogicalOperator |     this._logicalOperator = this.temporaryLogicalOperator | ||||||
|  |     this._intersection = this.temporaryIntersection | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   reset() { |   reset(complete: boolean = false) { | ||||||
|     this.temporarySelectionStates.clear() |     this.temporarySelectionStates.clear() | ||||||
|     this.selectionStates.forEach((value, key) => { |     if (complete) { | ||||||
|       this.temporarySelectionStates.set(key, value) |       this.selectionStates.clear() | ||||||
|     }) |     } else { | ||||||
|  |       this.selectionStates.forEach((value, key) => { | ||||||
|  |         this.temporarySelectionStates.set(key, value) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   diff(): ChangedItems { |   diff(): ChangedItems { | ||||||
| @@ -269,14 +342,16 @@ export class FilterableDropdownComponent { | |||||||
|     return this._selectionModel.items |     return this._selectionModel.items | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   _selectionModel = new FilterableDropdownSelectionModel() |   _selectionModel: FilterableDropdownSelectionModel = | ||||||
|  |     new FilterableDropdownSelectionModel() | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   set selectionModel(model: FilterableDropdownSelectionModel) { |   set selectionModel(model: FilterableDropdownSelectionModel) { | ||||||
|     if (this.selectionModel) { |     if (this.selectionModel) { | ||||||
|       this.selectionModel.changed.complete() |       this.selectionModel.changed.complete() | ||||||
|       model.items = this.selectionModel.items |       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) => { |     model.changed.subscribe((updatedModel) => { | ||||||
|       this.selectionModelChange.next(updatedModel) |       this.selectionModelChange.next(updatedModel) | ||||||
| @@ -292,12 +367,12 @@ export class FilterableDropdownComponent { | |||||||
|   selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>() |   selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>() | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   set multiple(value: boolean) { |   set manyToOne(manyToOne: boolean) { | ||||||
|     this.selectionModel.multiple = value |     this.selectionModel.manyToOne = manyToOne | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get multiple() { |   get manyToOne() { | ||||||
|     return this.selectionModel.multiple |     return this.selectionModel.manyToOne | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
| @@ -327,16 +402,20 @@ export class FilterableDropdownComponent { | |||||||
|   @Output() |   @Output() | ||||||
|   opened = new EventEmitter() |   opened = new EventEmitter() | ||||||
|  |  | ||||||
|   get operatorToggleEnabled(): boolean { |   get modifierToggleEnabled(): boolean { | ||||||
|     return ( |     return this.manyToOne | ||||||
|       this.selectionModel.selectionSize() > 1 && |       ? this.selectionModel.selectionSize() > 1 && | ||||||
|       this.selectionModel.getExcludedItems().length == 0 |           this.selectionModel.getExcludedItems().length == 0 | ||||||
|     ) |       : !this.selectionModel.isNoneSelected() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   documentCounts: SelectionDataItem[] |   documentCounts: SelectionDataItem[] | ||||||
|  |  | ||||||
|  |   get name(): string { | ||||||
|  |     return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null | ||||||
|  |   } | ||||||
|  |  | ||||||
|   getUpdatedDocumentCount(id: number) { |   getUpdatedDocumentCount(id: number) { | ||||||
|     if (this.documentCounts) { |     if (this.documentCounts) { | ||||||
|       return this.documentCounts.find((c) => c.id === id)?.document_count |       return this.documentCounts.find((c) => c.id === id)?.document_count | ||||||
| @@ -346,7 +425,6 @@ export class FilterableDropdownComponent { | |||||||
|   modelIsDirty: boolean = false |   modelIsDirty: boolean = false | ||||||
|  |  | ||||||
|   constructor(private filterPipe: FilterPipe) { |   constructor(private filterPipe: FilterPipe) { | ||||||
|     this.selectionModel = new FilterableDropdownSelectionModel() |  | ||||||
|     this.selectionModelChange.subscribe((updatedModel) => { |     this.selectionModelChange.subscribe((updatedModel) => { | ||||||
|       this.modelIsDirty = updatedModel.isDirty() |       this.modelIsDirty = updatedModel.isDirty() | ||||||
|     }) |     }) | ||||||
| @@ -400,7 +478,7 @@ export class FilterableDropdownComponent { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   reset() { |   reset() { | ||||||
|     this.selectionModel.reset() |     this.selectionModel.reset(true) | ||||||
|     this.selectionModelChange.emit(this.selectionModel) |     this.selectionModelChange.emit(this.selectionModel) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ | |||||||
|         [items]="tags" |         [items]="tags" | ||||||
|         [disabled]="!userCanEditAll" |         [disabled]="!userCanEditAll" | ||||||
|         [editing]="true" |         [editing]="true" | ||||||
|         [multiple]="true" |         [manyToOne]="true" | ||||||
|         [applyOnClose]="applyOnClose" |         [applyOnClose]="applyOnClose" | ||||||
|         (opened)="openTagsDropdown()" |         (opened)="openTagsDropdown()" | ||||||
|         [(selectionModel)]="tagSelectionModel" |         [(selectionModel)]="tagSelectionModel" | ||||||
|   | |||||||
| @@ -14,19 +14,19 @@ | |||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   <div class="btn-group flex-fill" role="group"> |   <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"> |     <label for="displayModeDetails" class="btn btn-outline-primary btn-sm"> | ||||||
|       <svg class="toolbaricon" fill="currentColor"> |       <svg class="toolbaricon" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#list-ul" /> |         <use xlink:href="assets/bootstrap-icons.svg#list-ul" /> | ||||||
|       </svg> |       </svg> | ||||||
|     </label> |     </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"> |     <label for="displayModeSmall" class="btn btn-outline-primary btn-sm"> | ||||||
|       <svg class="toolbaricon" fill="currentColor"> |       <svg class="toolbaricon" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#grid" /> |         <use xlink:href="assets/bootstrap-icons.svg#grid" /> | ||||||
|       </svg> |       </svg> | ||||||
|     </label> |     </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"> |     <label for="displayModeLarge" class="btn btn-outline-primary btn-sm"> | ||||||
|       <svg class="toolbaricon" fill="currentColor"> |       <svg class="toolbaricon" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#hdd-stack" /> |         <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 |           <app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title | ||||||
|             filterPlaceholder="Filter tags" i18n-filterPlaceholder |             filterPlaceholder="Filter tags" i18n-filterPlaceholder | ||||||
|             [items]="tags" |             [items]="tags" | ||||||
|             [multiple]="true" |             [manyToOne]="true" | ||||||
|             [(selectionModel)]="tagSelectionModel" |             [(selectionModel)]="tagSelectionModel" | ||||||
|             (selectionModelChange)="updateRules()" |             (selectionModelChange)="updateRules()" | ||||||
|             (opened)="onTagsDropdownOpen()" |             (opened)="onTagsDropdownOpen()" | ||||||
|   | |||||||
| @@ -21,10 +21,10 @@ import { | |||||||
|   FILTER_ADDED_AFTER, |   FILTER_ADDED_AFTER, | ||||||
|   FILTER_ADDED_BEFORE, |   FILTER_ADDED_BEFORE, | ||||||
|   FILTER_ASN, |   FILTER_ASN, | ||||||
|   FILTER_CORRESPONDENT, |   FILTER_HAS_CORRESPONDENT_ANY, | ||||||
|   FILTER_CREATED_AFTER, |   FILTER_CREATED_AFTER, | ||||||
|   FILTER_CREATED_BEFORE, |   FILTER_CREATED_BEFORE, | ||||||
|   FILTER_DOCUMENT_TYPE, |   FILTER_HAS_DOCUMENT_TYPE_ANY, | ||||||
|   FILTER_FULLTEXT_MORELIKE, |   FILTER_FULLTEXT_MORELIKE, | ||||||
|   FILTER_FULLTEXT_QUERY, |   FILTER_FULLTEXT_QUERY, | ||||||
|   FILTER_HAS_ANY_TAG, |   FILTER_HAS_ANY_TAG, | ||||||
| @@ -33,12 +33,22 @@ import { | |||||||
|   FILTER_DOES_NOT_HAVE_TAG, |   FILTER_DOES_NOT_HAVE_TAG, | ||||||
|   FILTER_TITLE, |   FILTER_TITLE, | ||||||
|   FILTER_TITLE_CONTENT, |   FILTER_TITLE_CONTENT, | ||||||
|   FILTER_STORAGE_PATH, |   FILTER_HAS_STORAGE_PATH_ANY, | ||||||
|   FILTER_ASN_ISNULL, |   FILTER_ASN_ISNULL, | ||||||
|   FILTER_ASN_GT, |   FILTER_ASN_GT, | ||||||
|   FILTER_ASN_LT, |   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' | } 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 { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component' | ||||||
| import { | import { | ||||||
|   DocumentService, |   DocumentService, | ||||||
| @@ -93,7 +103,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|     if (this.filterRules.length == 1) { |     if (this.filterRules.length == 1) { | ||||||
|       let rule = this.filterRules[0] |       let rule = this.filterRules[0] | ||||||
|       switch (this.filterRules[0].rule_type) { |       switch (this.filterRules[0].rule_type) { | ||||||
|         case FILTER_CORRESPONDENT: |         case FILTER_HAS_CORRESPONDENT_ANY: | ||||||
|           if (rule.value) { |           if (rule.value) { | ||||||
|             return $localize`Correspondent: ${ |             return $localize`Correspondent: ${ | ||||||
|               this.correspondents.find((c) => c.id == +rule.value)?.name |               this.correspondents.find((c) => c.id == +rule.value)?.name | ||||||
| @@ -102,7 +112,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|             return $localize`Without correspondent` |             return $localize`Without correspondent` | ||||||
|           } |           } | ||||||
|  |  | ||||||
|         case FILTER_DOCUMENT_TYPE: |         case FILTER_HAS_DOCUMENT_TYPE_ANY: | ||||||
|           if (rule.value) { |           if (rule.value) { | ||||||
|             return $localize`Type: ${ |             return $localize`Type: ${ | ||||||
|               this.documentTypes.find((dt) => dt.id == +rule.value)?.name |               this.documentTypes.find((dt) => dt.id == +rule.value)?.name | ||||||
| @@ -335,6 +345,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|           this.dateAddedBefore = rule.value |           this.dateAddedBefore = rule.value | ||||||
|           break |           break | ||||||
|         case FILTER_HAS_TAGS_ALL: |         case FILTER_HAS_TAGS_ALL: | ||||||
|  |           this.tagSelectionModel.logicalOperator = LogicalOperator.And | ||||||
|           this.tagSelectionModel.set( |           this.tagSelectionModel.set( | ||||||
|             rule.value ? +rule.value : null, |             rule.value ? +rule.value : null, | ||||||
|             ToggleableItemState.Selected, |             ToggleableItemState.Selected, | ||||||
| @@ -342,7 +353,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|           ) |           ) | ||||||
|           break |           break | ||||||
|         case FILTER_HAS_TAGS_ANY: |         case FILTER_HAS_TAGS_ANY: | ||||||
|           this.tagSelectionModel.logicalOperator = 'or' |           this.tagSelectionModel.logicalOperator = LogicalOperator.Or | ||||||
|           this.tagSelectionModel.set( |           this.tagSelectionModel.set( | ||||||
|             rule.value ? +rule.value : null, |             rule.value ? +rule.value : null, | ||||||
|             ToggleableItemState.Selected, |             ToggleableItemState.Selected, | ||||||
| @@ -360,26 +371,59 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|           ) |           ) | ||||||
|           break |           break | ||||||
|         case FILTER_CORRESPONDENT: |         case FILTER_CORRESPONDENT: | ||||||
|  |         case FILTER_HAS_CORRESPONDENT_ANY: | ||||||
|  |           this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or | ||||||
|  |           this.correspondentSelectionModel.intersection = Intersection.Include | ||||||
|           this.correspondentSelectionModel.set( |           this.correspondentSelectionModel.set( | ||||||
|             rule.value ? +rule.value : null, |             rule.value ? +rule.value : null, | ||||||
|             ToggleableItemState.Selected, |             ToggleableItemState.Selected, | ||||||
|             false |             false | ||||||
|           ) |           ) | ||||||
|           break |           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_DOCUMENT_TYPE: | ||||||
|  |         case FILTER_HAS_DOCUMENT_TYPE_ANY: | ||||||
|  |           this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or | ||||||
|  |           this.documentTypeSelectionModel.intersection = Intersection.Include | ||||||
|           this.documentTypeSelectionModel.set( |           this.documentTypeSelectionModel.set( | ||||||
|             rule.value ? +rule.value : null, |             rule.value ? +rule.value : null, | ||||||
|             ToggleableItemState.Selected, |             ToggleableItemState.Selected, | ||||||
|             false |             false | ||||||
|           ) |           ) | ||||||
|           break |           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_STORAGE_PATH: | ||||||
|  |         case FILTER_HAS_STORAGE_PATH_ANY: | ||||||
|  |           this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or | ||||||
|  |           this.storagePathSelectionModel.intersection = Intersection.Include | ||||||
|           this.storagePathSelectionModel.set( |           this.storagePathSelectionModel.set( | ||||||
|             rule.value ? +rule.value : null, |             rule.value ? +rule.value : null, | ||||||
|             ToggleableItemState.Selected, |             ToggleableItemState.Selected, | ||||||
|             false |             false | ||||||
|           ) |           ) | ||||||
|           break |           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: |         case FILTER_ASN_ISNULL: | ||||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_ASN |           this.textFilterTarget = TEXT_FILTER_TARGET_ASN | ||||||
|           this.textFilterModifier = |           this.textFilterModifier = | ||||||
| @@ -469,7 +513,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|       filterRules.push({ rule_type: FILTER_HAS_ANY_TAG, value: 'false' }) |       filterRules.push({ rule_type: FILTER_HAS_ANY_TAG, value: 'false' }) | ||||||
|     } else { |     } else { | ||||||
|       const tagFilterType = |       const tagFilterType = | ||||||
|         this.tagSelectionModel.logicalOperator == 'and' |         this.tagSelectionModel.logicalOperator == LogicalOperator.And | ||||||
|           ? FILTER_HAS_TAGS_ALL |           ? FILTER_HAS_TAGS_ALL | ||||||
|           : FILTER_HAS_TAGS_ANY |           : FILTER_HAS_TAGS_ANY | ||||||
|       this.tagSelectionModel |       this.tagSelectionModel | ||||||
| @@ -491,28 +535,66 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | |||||||
|           }) |           }) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|     this.correspondentSelectionModel |     if (this.correspondentSelectionModel.isNoneSelected()) { | ||||||
|       .getSelectedItems() |       filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null }) | ||||||
|       .forEach((correspondent) => { |     } else { | ||||||
|         filterRules.push({ |       this.correspondentSelectionModel | ||||||
|           rule_type: FILTER_CORRESPONDENT, |         .getSelectedItems() | ||||||
|           value: correspondent.id?.toString(), |         .forEach((correspondent) => { | ||||||
|  |           filterRules.push({ | ||||||
|  |             rule_type: FILTER_HAS_CORRESPONDENT_ANY, | ||||||
|  |             value: correspondent.id?.toString(), | ||||||
|  |           }) | ||||||
|         }) |         }) | ||||||
|       }) |       this.correspondentSelectionModel | ||||||
|     this.documentTypeSelectionModel |         .getExcludedItems() | ||||||
|       .getSelectedItems() |         .forEach((correspondent) => { | ||||||
|       .forEach((documentType) => { |           filterRules.push({ | ||||||
|         filterRules.push({ |             rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT, | ||||||
|           rule_type: FILTER_DOCUMENT_TYPE, |             value: correspondent.id?.toString(), | ||||||
|           value: documentType.id?.toString(), |           }) | ||||||
|         }) |         }) | ||||||
|       }) |     } | ||||||
|     this.storagePathSelectionModel.getSelectedItems().forEach((storagePath) => { |     if (this.documentTypeSelectionModel.isNoneSelected()) { | ||||||
|       filterRules.push({ |       filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null }) | ||||||
|         rule_type: FILTER_STORAGE_PATH, |     } else { | ||||||
|         value: storagePath.id?.toString(), |       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) { |     if (this.dateCreatedBefore) { | ||||||
|       filterRules.push({ |       filterRules.push({ | ||||||
|         rule_type: FILTER_CREATED_BEFORE, |         rule_type: FILTER_CREATED_BEFORE, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Component } from '@angular/core' | import { Component } from '@angular/core' | ||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 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 { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||||
| import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| @@ -35,7 +35,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Paperles | |||||||
|       toastService, |       toastService, | ||||||
|       documentListViewService, |       documentListViewService, | ||||||
|       permissionsService, |       permissionsService, | ||||||
|       FILTER_CORRESPONDENT, |       FILTER_HAS_CORRESPONDENT_ANY, | ||||||
|       $localize`correspondent`, |       $localize`correspondent`, | ||||||
|       $localize`correspondents`, |       $localize`correspondents`, | ||||||
|       PermissionType.Correspondent, |       PermissionType.Correspondent, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Component } from '@angular/core' | import { Component } from '@angular/core' | ||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 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 { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { | import { | ||||||
| @@ -32,7 +32,7 @@ export class DocumentTypeListComponent extends ManagementListComponent<Paperless | |||||||
|       toastService, |       toastService, | ||||||
|       documentListViewService, |       documentListViewService, | ||||||
|       permissionsService, |       permissionsService, | ||||||
|       FILTER_DOCUMENT_TYPE, |       FILTER_HAS_DOCUMENT_TYPE_ANY, | ||||||
|       $localize`document type`, |       $localize`document type`, | ||||||
|       $localize`document types`, |       $localize`document types`, | ||||||
|       PermissionType.DocumentType, |       PermissionType.DocumentType, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { Component } from '@angular/core' | import { Component } from '@angular/core' | ||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 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 { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { | import { | ||||||
| @@ -32,7 +32,7 @@ export class StoragePathListComponent extends ManagementListComponent<PaperlessS | |||||||
|       toastService, |       toastService, | ||||||
|       documentListViewService, |       documentListViewService, | ||||||
|       permissionsService, |       permissionsService, | ||||||
|       FILTER_STORAGE_PATH, |       FILTER_HAS_STORAGE_PATH_ANY, | ||||||
|       $localize`storage path`, |       $localize`storage path`, | ||||||
|       $localize`storage paths`, |       $localize`storage paths`, | ||||||
|       PermissionType.StoragePath, |       PermissionType.StoragePath, | ||||||
|   | |||||||
| @@ -8,8 +8,12 @@ export const FILTER_ASN_GT = 23 | |||||||
| export const FILTER_ASN_LT = 24 | export const FILTER_ASN_LT = 24 | ||||||
|  |  | ||||||
| export const FILTER_CORRESPONDENT = 3 | 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_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_IS_IN_INBOX = 5 | ||||||
| export const FILTER_HAS_TAGS_ALL = 6 | 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_HAS_TAGS_ANY = 22 | ||||||
|  |  | ||||||
| export const FILTER_STORAGE_PATH = 25 | 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_BEFORE = 8 | ||||||
| export const FILTER_CREATED_AFTER = 9 | export const FILTER_CREATED_AFTER = 9 | ||||||
| @@ -63,6 +69,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | |||||||
|     datatype: 'correspondent', |     datatype: 'correspondent', | ||||||
|     multi: false, |     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, |     id: FILTER_STORAGE_PATH, | ||||||
|     filtervar: 'storage_path__id', |     filtervar: 'storage_path__id', | ||||||
| @@ -70,6 +88,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | |||||||
|     datatype: 'storage_path', |     datatype: 'storage_path', | ||||||
|     multi: false, |     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, |     id: FILTER_DOCUMENT_TYPE, | ||||||
|     filtervar: 'document_type__id', |     filtervar: 'document_type__id', | ||||||
| @@ -77,6 +107,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | |||||||
|     datatype: 'document_type', |     datatype: 'document_type', | ||||||
|     multi: false, |     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, |     id: FILTER_IS_IN_INBOX, | ||||||
|     filtervar: 'is_in_inbox', |     filtervar: 'is_in_inbox', | ||||||
|   | |||||||
| @@ -86,12 +86,12 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params { | |||||||
|     let params = {} |     let params = {} | ||||||
|     for (let rule of filterRules) { |     for (let rule of filterRules) { | ||||||
|       let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type) |       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] = params[ruleType.filtervar] | ||||||
|           ? params[ruleType.filtervar] + ',' + rule.value |           ? params[ruleType.filtervar] + ',' + rule.value | ||||||
|           : rule.value |           : rule.value | ||||||
|       } else if (ruleType.isnull_filtervar && rule.value == null) { |  | ||||||
|         params[ruleType.isnull_filtervar] = 1 |  | ||||||
|       } else { |       } else { | ||||||
|         params[ruleType.filtervar] = rule.value |         params[ruleType.filtervar] = rule.value | ||||||
|         if (ruleType.datatype == 'boolean') |         if (ruleType.datatype == 'boolean') | ||||||
|   | |||||||
| @@ -36,29 +36,30 @@ class DocumentTypeFilterSet(FilterSet): | |||||||
|         fields = {"name": CHAR_KWARGS} |         fields = {"name": CHAR_KWARGS} | ||||||
|  |  | ||||||
|  |  | ||||||
| class TagsFilter(Filter): | class ObjectFilter(Filter): | ||||||
|     def __init__(self, exclude=False, in_list=False): |     def __init__(self, exclude=False, in_list=False, field_name=""): | ||||||
|         super().__init__() |         super().__init__() | ||||||
|         self.exclude = exclude |         self.exclude = exclude | ||||||
|         self.in_list = in_list |         self.in_list = in_list | ||||||
|  |         self.field_name = field_name | ||||||
|  |  | ||||||
|     def filter(self, qs, value): |     def filter(self, qs, value): | ||||||
|         if not value: |         if not value: | ||||||
|             return qs |             return qs | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             tag_ids = [int(x) for x in value.split(",")] |             object_ids = [int(x) for x in value.split(",")] | ||||||
|         except ValueError: |         except ValueError: | ||||||
|             return qs |             return qs | ||||||
|  |  | ||||||
|         if self.in_list: |         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: |         else: | ||||||
|             for tag_id in tag_ids: |             for obj_id in object_ids: | ||||||
|                 if self.exclude: |                 if self.exclude: | ||||||
|                     qs = qs.exclude(tags__id=tag_id) |                     qs = qs.exclude(**{f"{self.field_name}__id": obj_id}) | ||||||
|                 else: |                 else: | ||||||
|                     qs = qs.filter(tags__id=tag_id) |                     qs = qs.filter(**{f"{self.field_name}__id": obj_id}) | ||||||
|  |  | ||||||
|         return qs |         return qs | ||||||
|  |  | ||||||
| @@ -90,11 +91,17 @@ class DocumentFilterSet(FilterSet): | |||||||
|         exclude=True, |         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() |     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")), |         (23, _("ASN greater than")), | ||||||
|         (24, _("ASN less than")), |         (24, _("ASN less than")), | ||||||
|         (25, _("storage path is")), |         (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( |     saved_view = models.ForeignKey( | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon