Merge pull request #3309 from paperless-ngx/feature-owner-filtering

Feature: owner filtering
This commit is contained in:
shamoon 2023-05-11 10:05:51 -07:00 committed by GitHub
commit 09086e574d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1018 additions and 179 deletions

View File

@ -150,7 +150,7 @@ describe('documents-list', () => {
cy.contains('button', 'Corresp 11').click() cy.contains('button', 'Corresp 11').click()
cy.contains('label', 'Exclude').click() cy.contains('label', 'Exclude').click()
}) })
cy.contains('One document') cy.contains('3 documents')
}) })
it('should apply tags', () => { it('should apply tags', () => {

View File

@ -190,6 +190,36 @@ describe('documents query params', () => {
response.count = response.results.length response.count = response.results.length
} }
if (req.query.hasOwnProperty('owner__id')) {
response.results = (
documentsJson.results as Array<PaperlessDocument>
).filter((d) => d.owner == req.query['owner__id'])
response.count = response.results.length
} else if (req.query.hasOwnProperty('owner__id__in')) {
const owners = req.query['owner__id__in']
.toString()
.split(',')
.map((o) => parseInt(o))
response.results = (
documentsJson.results as Array<PaperlessDocument>
).filter((d) => owners.includes(d.owner))
response.count = response.results.length
} else if (req.query.hasOwnProperty('owner__id__none')) {
const owners = req.query['owner__id__none']
.toString()
.split(',')
.map((o) => parseInt(o))
response.results = (
documentsJson.results as Array<PaperlessDocument>
).filter((d) => !owners.includes(d.owner))
response.count = response.results.length
} else if (req.query.hasOwnProperty('owner__isnull')) {
response.results = (
documentsJson.results as Array<PaperlessDocument>
).filter((d) => d.owner === null)
response.count = response.results.length
}
req.reply(response) req.reply(response)
}) })
}) })
@ -202,7 +232,7 @@ describe('documents query params', () => {
it('should show a list of documents reverse sorted by created', () => { it('should show a list of documents reverse sorted by created', () => {
cy.visit('/documents?sort=created&reverse=true') cy.visit('/documents?sort=created&reverse=true')
cy.get('app-document-card-small').first().contains('sit amet') cy.get('app-document-card-small').first().contains('Doc 6')
}) })
it('should show a list of documents sorted by added', () => { it('should show a list of documents sorted by added', () => {
@ -212,7 +242,7 @@ describe('documents query params', () => {
it('should show a list of documents reverse sorted by added', () => { it('should show a list of documents reverse sorted by added', () => {
cy.visit('/documents?sort=added&reverse=true') cy.visit('/documents?sort=added&reverse=true')
cy.get('app-document-card-small').first().contains('sit amet') cy.get('app-document-card-small').first().contains('Doc 6')
}) })
it('should show a list of documents filtered by any tags', () => { it('should show a list of documents filtered by any tags', () => {
@ -222,12 +252,12 @@ describe('documents query params', () => {
it('should show a list of documents filtered by excluded tags', () => { it('should show a list of documents filtered by excluded tags', () => {
cy.visit('/documents?sort=created&reverse=true&tags__id__none=2,4') cy.visit('/documents?sort=created&reverse=true&tags__id__none=2,4')
cy.contains('One document') cy.contains('3 documents')
}) })
it('should show a list of documents filtered by no tags', () => { it('should show a list of documents filtered by no tags', () => {
cy.visit('/documents?sort=created&reverse=true&is_tagged=0') cy.visit('/documents?sort=created&reverse=true&is_tagged=0')
cy.contains('One document') cy.contains('3 documents')
}) })
it('should show a list of documents filtered by document type', () => { it('should show a list of documents filtered by document type', () => {
@ -242,7 +272,7 @@ describe('documents query params', () => {
it('should show a list of documents filtered by no document type', () => { it('should show a list of documents filtered by no document type', () => {
cy.visit('/documents?sort=created&reverse=true&document_type__isnull=1') cy.visit('/documents?sort=created&reverse=true&document_type__isnull=1')
cy.contains('One document') cy.contains('3 documents')
}) })
it('should show a list of documents filtered by correspondent', () => { it('should show a list of documents filtered by correspondent', () => {
@ -257,7 +287,7 @@ describe('documents query params', () => {
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('One document') cy.contains('3 documents')
}) })
it('should show a list of documents filtered by storage path', () => { it('should show a list of documents filtered by storage path', () => {
@ -267,7 +297,7 @@ describe('documents query params', () => {
it('should show a list of documents filtered by no storage path', () => { it('should show a list of documents filtered by no storage path', () => {
cy.visit('/documents?sort=created&reverse=true&storage_path__isnull=1') cy.visit('/documents?sort=created&reverse=true&storage_path__isnull=1')
cy.contains('3 documents') cy.contains('5 documents')
}) })
it('should show a list of documents filtered by title or content', () => { it('should show a list of documents filtered by title or content', () => {
@ -312,7 +342,7 @@ describe('documents query params', () => {
cy.visit( cy.visit(
'/documents?sort=created&reverse=true&created__date__gt=2022-03-23' '/documents?sort=created&reverse=true&created__date__gt=2022-03-23'
) )
cy.contains('3 documents') cy.contains('5 documents')
}) })
it('should show a list of documents filtered by created date less than', () => { it('should show a list of documents filtered by created date less than', () => {
@ -324,7 +354,7 @@ describe('documents query params', () => {
it('should show a list of documents filtered by added date greater than', () => { it('should show a list of documents filtered by added date greater than', () => {
cy.visit('/documents?sort=created&reverse=true&added__date__gt=2022-03-24') cy.visit('/documents?sort=created&reverse=true&added__date__gt=2022-03-24')
cy.contains('2 documents') cy.contains('4 documents')
}) })
it('should show a list of documents filtered by added date less than', () => { it('should show a list of documents filtered by added date less than', () => {
@ -338,4 +368,24 @@ describe('documents query params', () => {
) )
cy.contains('2 documents') cy.contains('2 documents')
}) })
it('should show a list of documents filtered by owner', () => {
cy.visit('/documents?owner__id=15')
cy.contains('One document')
})
it('should show a list of documents filtered by multiple owners', () => {
cy.visit('/documents?owner__id__in=6,15')
cy.contains('2 documents')
})
it('should show a list of documents filtered by excluded owners', () => {
cy.visit('/documents?owner__id__none=6')
cy.contains('5 documents')
})
it('should show a list of documents filtered by null owner', () => {
cy.visit('/documents?owner__isnull=true')
cy.contains('4 documents')
})
}) })

View File

@ -143,6 +143,64 @@
} }
}, },
"notes": [] "notes": []
},
{
"id": 5,
"correspondent": null,
"document_type": null,
"storage_path": null,
"title": "Doc 5",
"content": "Test document 5",
"tags": [],
"created": "2023-05-01T07:24:18Z",
"created_date": "2023-05-02",
"modified": "2023-05-02T07:24:23.264859Z",
"added": "2023-05-02T07:24:22.922631Z",
"archive_serial_number": null,
"original_file_name": "doc5.pdf",
"archived_file_name": "doc5.pdf",
"owner": 15,
"user_can_change": true,
"permissions": {
"view": {
"users": [1],
"groups": []
},
"change": {
"users": [],
"groups": []
}
},
"notes": []
},
{
"id": 6,
"correspondent": null,
"document_type": null,
"storage_path": null,
"title": "Doc 6",
"content": "Test document 6",
"tags": [],
"created": "2023-05-01T10:24:18Z",
"created_date": "2023-05-02",
"modified": "2023-05-02T10:24:23.264859Z",
"added": "2023-05-02T10:24:22.922631Z",
"archive_serial_number": null,
"original_file_name": "doc6.pdf",
"archived_file_name": "doc6.pdf",
"owner": 6,
"user_can_change": true,
"permissions": {
"view": {
"users": [1],
"groups": []
},
"change": {
"users": [],
"groups": []
}
},
"notes": []
} }
] ]
} }

View File

@ -574,7 +574,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">28</context> <context context-type="linenumber">26</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -1146,7 +1146,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">81</context> <context context-type="linenumber">78</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">77</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7878445132438733225" datatype="html"> <trans-unit id="7878445132438733225" datatype="html">
@ -1547,7 +1551,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">141</context> <context context-type="linenumber">138</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -1852,6 +1856,10 @@
<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">16</context> <context context-type="linenumber">16</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">6</context>
@ -2000,7 +2008,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">78</context> <context context-type="linenumber">83</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -2102,6 +2110,45 @@
<context context-type="linenumber">43</context> <context context-type="linenumber">43</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5947558132119506443" datatype="html">
<source>My documents</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">28</context>
</context-group>
</trans-unit>
<trans-unit id="231920238966427751" datatype="html">
<source>Shared with me</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">38</context>
</context-group>
</trans-unit>
<trans-unit id="5151074932731293042" datatype="html">
<source>Unowned</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">48</context>
</context-group>
</trans-unit>
<trans-unit id="4555457172864212828" datatype="html">
<source>Users</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">68</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">332</context>
</context-group>
</trans-unit>
<trans-unit id="8999708063434507268" datatype="html">
<source>Hide unowned</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html</context>
<context context-type="linenumber">77</context>
</context-group>
</trans-unit>
<trans-unit id="8650499415827640724" datatype="html"> <trans-unit id="8650499415827640724" datatype="html">
<source>Type</source> <source>Type</source>
<context-group purpose="location"> <context-group purpose="location">
@ -2196,7 +2243,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">172</context> <context context-type="linenumber">184</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -2223,11 +2270,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">144</context> <context context-type="linenumber">149</context>
</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">172</context> <context context-type="linenumber">191</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>
@ -2421,7 +2468,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">105</context> <context context-type="linenumber">102</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
@ -2429,7 +2476,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">94</context> <context context-type="linenumber">99</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8659635229098859487" datatype="html"> <trans-unit id="8659635229098859487" datatype="html">
@ -2447,7 +2494,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">92</context> <context context-type="linenumber">89</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1418444397960583910" datatype="html"> <trans-unit id="1418444397960583910" datatype="html">
@ -2508,11 +2555,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">40</context> <context context-type="linenumber">38</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">137</context> <context context-type="linenumber">142</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -2531,11 +2578,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">51</context> <context context-type="linenumber">49</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">158</context> <context context-type="linenumber">170</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -2554,11 +2601,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">62</context> <context context-type="linenumber">60</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">165</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/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -2891,14 +2938,14 @@
<source>Edit:</source> <source>Edit:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">27</context> <context context-type="linenumber">25</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7001227209911602786" datatype="html"> <trans-unit id="7001227209911602786" datatype="html">
<source>Filter tags</source> <source>Filter tags</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">27</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -2909,7 +2956,7 @@
<source>Filter correspondents</source> <source>Filter correspondents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">41</context> <context context-type="linenumber">39</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -2920,7 +2967,7 @@
<source>Filter document types</source> <source>Filter document types</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">50</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -2931,7 +2978,7 @@
<source>Filter storage paths</source> <source>Filter storage paths</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">63</context> <context context-type="linenumber">61</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -2942,7 +2989,7 @@
<source>Actions</source> <source>Actions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">86</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -2989,28 +3036,28 @@
<source>Include:</source> <source>Include:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">111</context> <context context-type="linenumber">108</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1208547554603365604" datatype="html"> <trans-unit id="1208547554603365604" datatype="html">
<source> Archived files </source> <source> Archived files </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">115,117</context> <context context-type="linenumber">112,114</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6791570188945688785" datatype="html"> <trans-unit id="6791570188945688785" datatype="html">
<source> Original files </source> <source> Original files </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">121,123</context> <context context-type="linenumber">118,120</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3608345051493493574" datatype="html"> <trans-unit id="3608345051493493574" datatype="html">
<source> Use formatted filename </source> <source> Use formatted filename </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">128,130</context> <context context-type="linenumber">125,127</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7985804062689412812" datatype="html"> <trans-unit id="7985804062689412812" datatype="html">
@ -3198,7 +3245,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">194</context> <context context-type="linenumber">206</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2784168796433474565" datatype="html"> <trans-unit id="2784168796433474565" datatype="html">
@ -3209,7 +3256,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">199</context> <context context-type="linenumber">211</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="106713086593101376" datatype="html"> <trans-unit id="106713086593101376" datatype="html">
@ -3234,7 +3281,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">212</context> <context context-type="linenumber">227</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="157572966557284263" datatype="html"> <trans-unit id="157572966557284263" datatype="html">
@ -3245,7 +3292,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">217</context> <context context-type="linenumber">232</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3727324658595204357" datatype="html"> <trans-unit id="3727324658595204357" datatype="html">
@ -3285,7 +3332,7 @@
<source>Score:</source> <source>Score:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
<context context-type="linenumber">110</context> <context context-type="linenumber">116</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3661756380991326939" datatype="html"> <trans-unit id="3661756380991326939" datatype="html">
@ -3390,29 +3437,40 @@
<context context-type="linenumber">99</context> <context context-type="linenumber">99</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6849725902312323996" datatype="html">
<source>Reset filters</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">104</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit id="1559883523769732271" datatype="html"> <trans-unit id="1559883523769732271" datatype="html">
<source>Error while loading documents</source> <source>Error while loading documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">117</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="494022736054110363" datatype="html"> <trans-unit id="494022736054110363" datatype="html">
<source>Sort by ASN</source> <source>Sort by ASN</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">126</context> <context context-type="linenumber">131</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7517688192215738656" datatype="html"> <trans-unit id="7517688192215738656" datatype="html">
<source>ASN</source> <source>ASN</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">131,130</context> <context context-type="linenumber">136,135</context>
</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">177</context> <context context-type="linenumber">196</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>
@ -3423,28 +3481,46 @@
<source>Sort by correspondent</source> <source>Sort by correspondent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">133</context> <context context-type="linenumber">138</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2066713941761361709" datatype="html"> <trans-unit id="2066713941761361709" datatype="html">
<source>Sort by title</source> <source>Sort by title</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">140</context> <context context-type="linenumber">145</context>
</context-group>
</trans-unit>
<trans-unit id="6232673011753681091" datatype="html">
<source>Sort by owner</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="3715596725146409911" datatype="html">
<source>Owner</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">156</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/rest/document.service.ts</context>
<context context-type="linenumber">26</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3557446856808034218" datatype="html"> <trans-unit id="3557446856808034218" datatype="html">
<source>Sort by notes</source> <source>Sort by notes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">147</context> <context context-type="linenumber">159</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8104421162933956065" datatype="html"> <trans-unit id="8104421162933956065" datatype="html">
<source>Notes</source> <source>Notes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">151</context> <context context-type="linenumber">163</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
@ -3459,35 +3535,35 @@
<source>Sort by document type</source> <source>Sort by document type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">154</context> <context context-type="linenumber">166</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6213829731736042759" datatype="html"> <trans-unit id="6213829731736042759" datatype="html">
<source>Sort by storage path</source> <source>Sort by storage path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">161</context> <context context-type="linenumber">173</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3406167410329973166" datatype="html"> <trans-unit id="3406167410329973166" datatype="html">
<source>Sort by created date</source> <source>Sort by created date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">168</context> <context context-type="linenumber">180</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3769035778779263084" datatype="html"> <trans-unit id="3769035778779263084" datatype="html">
<source>Sort by added date</source> <source>Sort by added date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">175</context> <context context-type="linenumber">187</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="231679111972850796" datatype="html"> <trans-unit id="231679111972850796" datatype="html">
<source>Added</source> <source>Added</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">179</context> <context context-type="linenumber">191</context>
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@ -3502,7 +3578,7 @@
<source>Edit document</source> <source>Edit document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">198</context> <context context-type="linenumber">210</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2155249406916744630" datatype="html"> <trans-unit id="2155249406916744630" datatype="html">
@ -3519,123 +3595,137 @@
<context context-type="linenumber">246</context> <context context-type="linenumber">246</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6849725902312323996" datatype="html">
<source>Reset filters</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="5195932016807797291" datatype="html"> <trans-unit id="5195932016807797291" datatype="html">
<source>Correspondent: <x id="PH" equiv-text="this.correspondents.find((c) =&gt; c.id == +rule.value)?.name"/></source> <source>Correspondent: <x id="PH" equiv-text="this.correspondents.find((c) =&gt; 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">108,110</context> <context context-type="linenumber">117,119</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">112</context> <context context-type="linenumber">121</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) =&gt; dt.id == +rule.value)?.name"/></source> <source>Type: <x id="PH" equiv-text="this.documentTypes.find((dt) =&gt; 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">117,119</context> <context context-type="linenumber">126,128</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">121</context> <context context-type="linenumber">130</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) =&gt; t.id == +rule.value)?.name"/></source> <source>Tag: <x id="PH" equiv-text="this.tags.find((t) =&gt; 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">125,127</context> <context context-type="linenumber">134,136</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">131</context> <context context-type="linenumber">140</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">135</context> <context context-type="linenumber">144</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">138</context> <context context-type="linenumber">147</context>
</context-group>
</trans-unit>
<trans-unit id="102674688969746976" datatype="html">
<source>Owner: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">150</context>
</context-group>
</trans-unit>
<trans-unit id="3550877650686009106" datatype="html">
<source>Owner not in: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">153</context>
</context-group>
</trans-unit>
<trans-unit id="1082034558646673343" datatype="html">
<source>Without an owner</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">156</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3100631071441658964" datatype="html"> <trans-unit id="3100631071441658964" datatype="html">
<source>Title &amp; content</source> <source>Title &amp; 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">175</context> <context context-type="linenumber">194</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">180</context> <context context-type="linenumber">199</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">186</context> <context context-type="linenumber">205</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">205</context> <context context-type="linenumber">224</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">209</context> <context context-type="linenumber">228</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">213</context> <context context-type="linenumber">232</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">217</context> <context context-type="linenumber">236</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">221</context> <context context-type="linenumber">240</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7210076240260527720" datatype="html"> <trans-unit id="7210076240260527720" datatype="html">
@ -4338,13 +4428,6 @@
<context context-type="linenumber">327</context> <context context-type="linenumber">327</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4555457172864212828" datatype="html">
<source>Users</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
<context context-type="linenumber">332</context>
</context-group>
</trans-unit>
<trans-unit id="2941198503117307737" datatype="html"> <trans-unit id="2941198503117307737" datatype="html">
<source>Add User</source> <source>Add User</source>
<context-group purpose="location"> <context-group purpose="location">
@ -4917,6 +5000,13 @@
<context context-type="linenumber">11</context> <context context-type="linenumber">11</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5739581984228459958" datatype="html">
<source>Shared</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/username.pipe.ts</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="2807800733729323332" datatype="html"> <trans-unit id="2807800733729323332" datatype="html">
<source>Yes</source> <source>Yes</source>
<context-group purpose="location"> <context-group purpose="location">
@ -5079,7 +5169,7 @@
<source>Search score</source> <source>Search score</source>
<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>
<context context-type="linenumber">32</context> <context context-type="linenumber">33</context>
</context-group> </context-group>
<note priority="1" from="description">Score is a value returned by the full text search engine and specifies how well a result matches the given query</note> <note priority="1" from="description">Score is a value returned by the full text search engine and specifies how well a result matches the given query</note>
</trans-unit> </trans-unit>

View File

@ -88,6 +88,10 @@ import { PermissionsUserComponent } from './components/common/input/permissions/
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component' import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
import { IfOwnerDirective } from './directives/if-owner.directive' import { IfOwnerDirective } from './directives/if-owner.directive'
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive' import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
import { PermissionsFilterDropdownComponent } from './components/common/permissions-filter-dropdown/permissions-filter-dropdown.component'
import { UsernamePipe } from './pipes/username.pipe'
import localeAr from '@angular/common/locales/ar' import localeAr from '@angular/common/locales/ar'
import localeBe from '@angular/common/locales/be' import localeBe from '@angular/common/locales/be'
@ -111,8 +115,6 @@ import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv' import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr' import localeTr from '@angular/common/locales/tr'
import localeZh from '@angular/common/locales/zh' import localeZh from '@angular/common/locales/zh'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
registerLocaleData(localeAr) registerLocaleData(localeAr)
registerLocaleData(localeBe) registerLocaleData(localeBe)
@ -213,6 +215,8 @@ function initializeApp(settings: SettingsService) {
IfObjectPermissionsDirective, IfObjectPermissionsDirective,
PermissionsDialogComponent, PermissionsDialogComponent,
PermissionsFormComponent, PermissionsFormComponent,
PermissionsFilterDropdownComponent,
UsernamePipe,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -253,6 +257,7 @@ function initializeApp(settings: SettingsService) {
PermissionsGuard, PermissionsGuard,
DirtyDocGuard, DirtyDocGuard,
DirtySavedViewGuard, DirtySavedViewGuard,
UsernamePipe,
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })

View File

@ -18,8 +18,8 @@
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder> [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)" (selectItem)="itemSelected($event)" i18n-placeholder>
<button type="button" *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()"> <button type="button" *ngIf="!searchFieldEmpty" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg fill="currentColor" class="buttonicon-sm me-1">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
</button> </button>
</form> </form>
@ -107,7 +107,7 @@
<use xlink:href="assets/bootstrap-icons.svg#file-text"/> <use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg><span>&nbsp;{{d.title | documentTitle}}</span> </svg><span>&nbsp;{{d.title | documentTitle}}</span>
<span class="close" (click)="closeDocument(d); $event.preventDefault()"> <span class="close" (click)="closeDocument(d); $event.preventDefault()">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16"> <svg fill="currentColor" class="toolbaricon">
<use xlink:href="assets/bootstrap-icons.svg#x"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
</span> </span>

View File

@ -6,10 +6,10 @@
<div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> <div class="dropdown-menu date-dropdown shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<button *ngFor="let rd of relativeDates" class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.date)"> <button *ngFor="let rd of relativeDates" class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.date)">
<div _ngcontent-hga-c166="" class="selected-icon me-1"> <div class="selected-icon me-1">
<svg *ngIf="relativeDate === rd.date" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16"> <svg *ngIf="relativeDate === rd.date" fill="currentColor" class="buttonicon-sm">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
</div> </div>
{{rd.name}} {{rd.name}}
</button> </button>
@ -18,8 +18,8 @@
<div class="mb-2 d-flex flex-row w-100 justify-content-between small"> <div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>After</div> <div i18n>After</div>
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()"> <a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg fill="currentColor" class="buttonicon-sm">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" /> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
<small i18n>Clear</small> <small i18n>Clear</small>
</a> </a>
@ -29,8 +29,8 @@
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)" <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker"> maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button"> <button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16"> <svg fill="currentColor" class="buttonicon-sm">
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> <use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg> </svg>
</button> </button>
</div> </div>
@ -41,8 +41,8 @@
<div class="mb-2 d-flex flex-row w-100 justify-content-between small"> <div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div i18n>Before</div> <div i18n>Before</div>
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()"> <a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
<svg width="0.8em" height="0.8em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg fill="currentColor" class="buttonicon-sm">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" /> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
<small i18n>Clear</small> <small i18n>Clear</small>
</a> </a>
@ -52,8 +52,8 @@
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)" <input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker"> maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button"> <button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16"> <svg fill="currentColor" class="buttonicon-sm">
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> <use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg> </svg>
</button> </button>
</div> </div>

View File

@ -1,18 +1,18 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled"> <button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
<ng-container *ngIf="isChecked()"> <ng-container *ngIf="isChecked()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16"> <svg fill="currentColor" class="buttonicon-sm bi-check">
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/> <use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg> </svg>
</ng-container> </ng-container>
<ng-container *ngIf="isPartiallyChecked()"> <ng-container *ngIf="isPartiallyChecked()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-dash" viewBox="0 0 16 16"> <svg fill="currentColor" class="buttonicon-sm bi-dash">
<path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8z"/> <use xlink:href="assets/bootstrap-icons.svg#dash"/>
</svg> </svg>
</ng-container> </ng-container>
<ng-container *ngIf="isExcluded()"> <ng-container *ngIf="isExcluded()">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16"> <svg fill="currentColor" class="buttonicon-sm bi-x">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
</ng-container> </ng-container>
</div> </div>

View File

@ -0,0 +1,82 @@
<div class="btn-group w-100" ngbDropdown role="group">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
<app-clearable-badge [selected]="isActive" (cleared)="reset()"></app-clearable-badge><span class="visually-hidden">selected</span>
</button>
<div class="dropdown-menu permission-filter-dropdown shadow py-0 w-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush">
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>All</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SELF" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>My documents</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>Shared with me</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.UNOWNED" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>Unowned</small>
</div>
</button>
<button *appIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.OTHERS" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1 w-100">
<ng-select
name="user"
class="user-select small"
[(ngModel)]="selectionModel.includeUsers"
[disabled]="disabled"
[clearable]="false"
[items]="users"
bindLabel="username"
multiple="true"
bindValue="id"
placeholder="Users"
i18n-placeholder
(change)="onUserSelect()">
</ng-select>
</div>
</button>
<div *ngIf="selectionModel.ownerFilter === OwnerFilterType.NONE || selectionModel.ownerFilter === OwnerFilterType.NOT_SELF" class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0">
<div class="form-check form-switch w-100">
<input type="checkbox" class="form-check-input" id="hideUnowned" [(ngModel)]="this.selectionModel.hideUnowned" (change)="onChange()" [disabled]="disabled">
<label class="form-check-label w-100" for="hideUnowned"><small i18n>Hide unowned</small></label>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,8 @@
.user-select {
min-width: 15rem;
}
.selected-icon {
min-width: 1em;
min-height: 1em;
}

View File

@ -0,0 +1,132 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { first } from 'rxjs'
import { PaperlessUser } from 'src/app/data/paperless-user'
import {
PermissionAction,
PermissionType,
PermissionsService,
} from 'src/app/services/permissions.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
export class PermissionsSelectionModel {
ownerFilter: OwnerFilterType
hideUnowned: boolean
userID: number
includeUsers: number[]
excludeUsers: number[]
clear() {
this.ownerFilter = OwnerFilterType.NONE
this.userID = null
this.hideUnowned = false
this.includeUsers = []
this.excludeUsers = []
}
}
export enum OwnerFilterType {
NONE = 0,
SELF = 1,
NOT_SELF = 2,
OTHERS = 3,
UNOWNED = 4,
}
@Component({
selector: 'app-permissions-filter-dropdown',
templateUrl: './permissions-filter-dropdown.component.html',
styleUrls: ['./permissions-filter-dropdown.component.scss'],
})
export class PermissionsFilterDropdownComponent {
public PermissionAction = PermissionAction
public PermissionType = PermissionType
public OwnerFilterType = OwnerFilterType
@Input()
title: string
@Input()
disabled = false
@Input()
selectionModel: PermissionsSelectionModel
@Output()
ownerFilterSet = new EventEmitter<PermissionsSelectionModel>()
users: PaperlessUser[]
hideUnowned: boolean
get isActive(): boolean {
return (
this.selectionModel.ownerFilter !== OwnerFilterType.NONE ||
this.selectionModel.hideUnowned
)
}
constructor(
permissionsService: PermissionsService,
userService: UserService,
private settingsService: SettingsService
) {
if (
permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.User
)
) {
userService
.listAll()
.pipe(first())
.subscribe({
next: (result) => (this.users = result.results),
})
}
}
reset() {
this.selectionModel.clear()
this.onChange()
}
setFilter(type: OwnerFilterType) {
this.selectionModel.ownerFilter = type
if (this.selectionModel.ownerFilter === OwnerFilterType.SELF) {
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = []
this.selectionModel.userID = this.settingsService.currentUser.id
this.selectionModel.hideUnowned = false
} else if (this.selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
this.selectionModel.userID = null
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = [this.settingsService.currentUser.id]
this.selectionModel.hideUnowned = false
} else if (this.selectionModel.ownerFilter === OwnerFilterType.NONE) {
this.selectionModel.userID = null
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = []
this.selectionModel.hideUnowned = false
} else if (this.selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
this.selectionModel.userID = null
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = []
this.selectionModel.hideUnowned = false
}
this.onChange()
}
onChange() {
this.ownerFilterSet.emit(this.selectionModel)
}
onUserSelect() {
if (this.selectionModel.includeUsers?.length) {
this.selectionModel.ownerFilter = OwnerFilterType.OTHERS
} else {
this.selectionModel.ownerFilter = OwnerFilterType.NONE
}
this.onChange()
}
}

View File

@ -1,13 +1,13 @@
<div class="row"> <div class="d-flex flex-wrap gap-4">
<div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select"> <div class="d-flex align-items-center" role="group" aria-label="Select">
<button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()"> <button class="btn btn-sm btn-outline-secondary" (click)="list.selectNone()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" /> <use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
</svg>&nbsp;<ng-container i18n>Cancel</ng-container> </svg>&nbsp;<ng-container i18n>Cancel</ng-container>
</button> </button>
</div> </div>
<div class="col-auto mb-2 mb-xl-0 ms-auto ms-md-0" role="group" aria-label="Select"> <div class="d-flex align-items-center gap-2" role="group" aria-label="Select">
<label class="me-2 mb-0" i18n>Select:</label> <label class="me-2" i18n>Select:</label>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()"> <button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
@ -21,11 +21,9 @@
</button> </button>
</div> </div>
</div> </div>
<div class="w-100 d-xl-none"></div> <div class="d-flex align-items-center gap-2" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<div class="col-auto mb-2 mb-xl-0"> <label class="me-2" i18n>Edit:</label>
<div class="d-flex" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> <app-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
<label class="ms-auto mt-1 mb-0 me-2" i18n>Edit:</label>
<app-filterable-dropdown class="me-2 me-md-3" title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags" [items]="tags"
[disabled]="!userCanEditAll" [disabled]="!userCanEditAll"
@ -37,7 +35,7 @@
[documentCounts]="tagDocumentCounts" [documentCounts]="tagDocumentCounts"
(apply)="setTags($event)"> (apply)="setTags($event)">
</app-filterable-dropdown> </app-filterable-dropdown>
<app-filterable-dropdown class="me-2 me-md-3" title="Correspondent" icon="person-fill" i18n-title <app-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents" [items]="correspondents"
[disabled]="!userCanEditAll" [disabled]="!userCanEditAll"
@ -48,7 +46,7 @@
[documentCounts]="correspondentDocumentCounts" [documentCounts]="correspondentDocumentCounts"
(apply)="setCorrespondents($event)"> (apply)="setCorrespondents($event)">
</app-filterable-dropdown> </app-filterable-dropdown>
<app-filterable-dropdown class="me-2 me-md-3" title="Document type" icon="file-earmark-fill" i18n-title <app-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes" [items]="documentTypes"
[disabled]="!userCanEditAll" [disabled]="!userCanEditAll"
@ -59,7 +57,7 @@
[documentCounts]="documentTypeDocumentCounts" [documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)"> (apply)="setDocumentTypes($event)">
</app-filterable-dropdown> </app-filterable-dropdown>
<app-filterable-dropdown class="me-2 me-md-3" title="Storage path" icon="folder-fill" i18n-title <app-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths" [items]="storagePaths"
[disabled]="!userCanEditAll" [disabled]="!userCanEditAll"
@ -70,18 +68,17 @@
[documentCounts]="storagePathDocumentCounts" [documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)"> (apply)="setStoragePaths($event)">
</app-filterable-dropdown> </app-filterable-dropdown>
</div>
</div> </div>
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex"> <div class="d-flex align-items-center gap-2 ms-auto">
<div class="btn-toolbar me-2"> <div class="btn-toolbar">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll"> <button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" /> <use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>&nbsp;<ng-container i18n>Permissions</ng-container> </svg><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div>
</button> </button>
<div ngbDropdown class="me-2 d-flex"> <div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle> <button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor"> <svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" /> <use xlink:href="assets/bootstrap-icons.svg#three-dots" />
@ -94,7 +91,7 @@
</div> </div>
</div> </div>
<div class="btn-group btn-group-sm me-2"> <div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()"> <button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
<svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor"> <svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-down" /> <use xlink:href="assets/bootstrap-icons.svg#arrow-down" />
@ -134,7 +131,7 @@
</div> </div>
</div> </div>
<div class="btn-group btn-group-sm me-2"> <div class="btn-group btn-group-sm">
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll"> <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *appIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />

View File

@ -106,6 +106,12 @@
</svg> </svg>
<small>{{document.created_date | customDate:'mediumDate'}}</small> <small>{{document.created_date | customDate:'mediumDate'}}</small>
</div> </div>
<div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg>
<small>{{document.owner | username}}</small>
</div>
<div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score"> <div *ngIf="document.__search_hit__?.score" class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
<small class="text-muted" i18n>Score:</small> <small class="text-muted" i18n>Score:</small>
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar> <ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>

View File

@ -23,7 +23,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
export class DocumentCardLargeComponent extends ComponentWithPermissions { export class DocumentCardLargeComponent extends ComponentWithPermissions {
constructor( constructor(
private documentService: DocumentService, private documentService: DocumentService,
private settingsService: SettingsService public settingsService: SettingsService
) { ) {
super() super()
} }

View File

@ -38,15 +38,15 @@
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info"> <div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
<button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title <button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted bi bi-file-earmark" viewBox="0 0 16 16" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> <use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
</svg> </svg>
<small>{{(document.document_type$ | async)?.name}}</small> <small>{{(document.document_type$ | async)?.name}}</small>
</button> </button>
<button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title <button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()"> (click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
<svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/> <use xlink:href="assets/bootstrap-icons.svg#folder"/>
</svg> </svg>
<small>{{(document.storage_path$ | async)?.name}}</small> <small>{{(document.storage_path$ | async)?.name}}</small>
</button> </button>
@ -59,18 +59,23 @@
</div> </div>
</ng-template> </ng-template>
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip"> <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip">
<svg class="metadata-icon me-2 text-muted bi bi-calendar-event" viewBox="0 0 16 16" fill="currentColor"> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
<path d="M11 6.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-1z"/> <use xlink:href="assets/bootstrap-icons.svg#calendar-event"/>
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
</svg> </svg>
<small>{{document.created_date | customDate:'mediumDate'}}</small> <small>{{document.created_date | customDate:'mediumDate'}}</small>
</div> </div>
<div *ngIf="document.archive_serial_number" class="ps-0 p-1"> </div>
<svg class="metadata-icon me-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor"> <div *ngIf="document.archive_serial_number" class="ps-0 p-1">
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5zM3 4.5a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-7zm3 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7z"/> <svg class="metadata-icon me-2 text-muted" fill="currentColor">
</svg> <use xlink:href="assets/bootstrap-icons.svg#upc-scan"/>
<small>#{{document.archive_serial_number}}</small> </svg>
</div> <small>#{{document.archive_serial_number}}</small>
</div>
<div *ngIf="document.owner && document.owner !== settingsService.currentUser.id" class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock"/>
</svg>
<small>{{document.owner | username}}</small>
</div> </div>
</div> </div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">

View File

@ -24,7 +24,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
export class DocumentCardSmallComponent extends ComponentWithPermissions { export class DocumentCardSmallComponent extends ComponentWithPermissions {
constructor( constructor(
private documentService: DocumentService, private documentService: DocumentService,
private settingsService: SettingsService public settingsService: SettingsService
) { ) {
super() super()
} }

View File

@ -81,15 +81,15 @@
</app-page-header> </app-page-header>
<div class="row sticky-top pt-3 pt-sm-4 pb-2 pb-lg-4 bg-body"> <div class="row sticky-top pt-3 pt-sm-4 pb-3 pb-lg-4 bg-body">
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></app-filter-editor> <app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></app-filter-editor>
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor> <app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
</div> </div>
<ng-template #pagination> <ng-template #pagination>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
<p> <div class="d-flex align-items-center">
<ng-container *ngIf="list.isReloading"> <ng-container *ngIf="list.isReloading">
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container> <ng-container i18n>Loading...</ng-container>
@ -98,9 +98,14 @@
<ng-container *ngIf="!list.isReloading"> <ng-container *ngIf="!list.isReloading">
<span i18n *ngIf="list.selected.size === 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>&nbsp;<span i18n *ngIf="isFiltered">(filtered)</span> <span i18n *ngIf="list.selected.size === 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>&nbsp;<span i18n *ngIf="isFiltered">(filtered)</span>
</ng-container> </ng-container>
</p> <button *ngIf="!list.isReloading && isFiltered" class="btn btn-link py-0" (click)="resetFilters()">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><small i18n>Reset filters</small>
</button>
</div>
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" <ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" aria-label="Default pagination"></ngb-pagination> [rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
</div> </div>
</ng-template> </ng-template>
@ -142,6 +147,13 @@
[currentSortReverse]="list.sortReverse" [currentSortReverse]="list.sortReverse"
(sort)="onSort($event)" (sort)="onSort($event)"
i18n>Title</th> i18n>Title</th>
<th class="d-none d-xl-table-cell"
appSortable="owner"
title="Sort by owner" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Owner</th>
<th *ngIf="notesEnabled" class="d-none d-xl-table-cell" <th *ngIf="notesEnabled" class="d-none d-xl-table-cell"
appSortable="num_notes" appSortable="num_notes"
title="Sort by notes" i18n-title title="Sort by notes" i18n-title
@ -198,6 +210,9 @@
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag> <app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
</td> </td>
<td>
{{d.owner | username}}
</td>
<td *ngIf="notesEnabled" class="d-none d-xl-table-cell"> <td *ngIf="notesEnabled" class="d-none d-xl-table-cell">
<a *ngIf="d.notes.length" routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0"> <a *ngIf="d.notes.length" routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
<span class="badge rounded-pill bg-light border text-primary"> <span class="badge rounded-pill bg-light border text-primary">

View File

@ -300,4 +300,8 @@ export class DocumentListComponent
get notesEnabled(): boolean { get notesEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED) return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
} }
resetFilters() {
this.filterEditor.resetSelected()
}
} }

View File

@ -1,5 +1,5 @@
<div class="row flex-wrap" tourAnchor="tour.documents-filter-editor"> <div class="row flex-wrap" tourAnchor="tour.documents-filter-editor">
<div class="col mb-2 mb-xxl-0"> <div class="col mb-3 mb-xxl-0">
<div class="form-inline d-flex align-items-center"> <div class="form-inline d-flex align-items-center">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap"> <div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
<div ngbDropdown> <div ngbDropdown>
@ -12,8 +12,8 @@
<option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option> <option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option>
</select> </select>
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()"> <button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg fill="currentColor" class="buttonicon-sm me-1">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg> </svg>
</button> </button>
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'"> <input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'">
@ -22,8 +22,8 @@
</div> </div>
<div class="w-100 d-xxl-none"></div> <div class="w-100 d-xxl-none"></div>
<div class="col col-xl-auto"> <div class="col col-xl-auto">
<div class="d-flex flex-wrap"> <div class="d-flex flex-wrap gap-3">
<div class="d-flex flex-wrap mb-2 mb-xxl-0"> <div class="d-flex flex-wrap gap-2">
<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"
@ -49,7 +49,7 @@
(opened)="onDocumentTypeDropdownOpen()" (opened)="onDocumentTypeDropdownOpen()"
[documentCounts]="documentTypeDocumentCounts" [documentCounts]="documentTypeDocumentCounts"
[allowSelectNone]="true"></app-filterable-dropdown> [allowSelectNone]="true"></app-filterable-dropdown>
<app-filterable-dropdown class="me-2 flex-fill" title="Storage path" icon="folder-fill" i18n-title <app-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths" [items]="storagePaths"
[(selectionModel)]="storagePathSelectionModel" [(selectionModel)]="storagePathSelectionModel"
@ -58,28 +58,33 @@
[documentCounts]="storagePathDocumentCounts" [documentCounts]="storagePathDocumentCounts"
[allowSelectNone]="true"></app-filterable-dropdown> [allowSelectNone]="true"></app-filterable-dropdown>
</div> </div>
<div class="d-flex flex-wrap"> <div class="d-flex flex-wrap gap-2">
<app-date-dropdown class="mb-2 mb-xl-0" <app-date-dropdown
title="Created" i18n-title title="Created" i18n-title
(datesSet)="updateRules()" (datesSet)="updateRules()"
[(dateBefore)]="dateCreatedBefore" [(dateBefore)]="dateCreatedBefore"
[(dateAfter)]="dateCreatedAfter" [(dateAfter)]="dateCreatedAfter"
[(relativeDate)]="dateCreatedRelativeDate"></app-date-dropdown> [(relativeDate)]="dateCreatedRelativeDate"></app-date-dropdown>
<app-date-dropdown class="mb-2 mb-xl-0" <app-date-dropdown
title="Added" i18n-title title="Added" i18n-title
(datesSet)="updateRules()" (datesSet)="updateRules()"
[(dateBefore)]="dateAddedBefore" [(dateBefore)]="dateAddedBefore"
[(dateAfter)]="dateAddedAfter" [(dateAfter)]="dateAddedAfter"
[(relativeDate)]="dateAddedRelativeDate"></app-date-dropdown> [(relativeDate)]="dateAddedRelativeDate"></app-date-dropdown>
</div> </div>
<div class="d-flex flex-wrap">
<app-permissions-filter-dropdown
title="Permissions" i18n-title
(ownerFilterSet)="updateRules()"
[(selectionModel)]="permissionsSelectionModel"></app-permissions-filter-dropdown>
</div>
<div class="d-flex flex-wrap d-none d-sm-inline-block">
<button class="btn btn-outline-secondary btn-sm" [disabled]="!rulesModified" (click)="resetSelected()">
<svg class="toolbaricon ms-n1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"></use>
</svg><ng-container i18n>Reset filters</ng-container>
</button>
</div>
</div> </div>
</div> </div>
<div class="w-100 d-xxl-none"></div>
<div class="col col-xl-auto ps-xxl-0">
<button class="btn btn-link btn-sm px-0" [disabled]="!rulesModified" (click)="resetSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1 ms-n1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg><ng-container i18n>Reset filters</ng-container>
</button>
</div>
</div> </div>

View File

@ -43,6 +43,10 @@ import {
FILTER_DOCUMENT_TYPE, FILTER_DOCUMENT_TYPE,
FILTER_CORRESPONDENT, FILTER_CORRESPONDENT,
FILTER_STORAGE_PATH, FILTER_STORAGE_PATH,
FILTER_OWNER,
FILTER_OWNER_DOES_NOT_INCLUDE,
FILTER_OWNER_ISNULL,
FILTER_OWNER_ANY,
} from 'src/app/data/filter-rule-type' } from 'src/app/data/filter-rule-type'
import { import {
FilterableDropdownSelectionModel, FilterableDropdownSelectionModel,
@ -59,6 +63,11 @@ import { PaperlessDocument } from 'src/app/data/paperless-document'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { RelativeDate } from '../../common/date-dropdown/date-dropdown.component' import { RelativeDate } from '../../common/date-dropdown/date-dropdown.component'
import {
OwnerFilterType,
PermissionsSelectionModel,
} from '../../common/permissions-filter-dropdown/permissions-filter-dropdown.component'
import { SettingsService } from 'src/app/services/settings.service'
const TEXT_FILTER_TARGET_TITLE = 'title' const TEXT_FILTER_TARGET_TITLE = 'title'
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content' const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
@ -136,6 +145,15 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
case FILTER_ASN: case FILTER_ASN:
return $localize`ASN: ${rule.value}` return $localize`ASN: ${rule.value}`
case FILTER_OWNER:
return $localize`Owner: ${rule.value}`
case FILTER_OWNER_DOES_NOT_INCLUDE:
return $localize`Owner not in: ${rule.value}`
case FILTER_OWNER_ISNULL:
return $localize`Without an owner`
} }
} }
@ -147,7 +165,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
private tagService: TagService, private tagService: TagService,
private correspondentService: CorrespondentService, private correspondentService: CorrespondentService,
private documentService: DocumentService, private documentService: DocumentService,
private storagePathService: StoragePathService private storagePathService: StoragePathService,
private settingsService: SettingsService
) {} ) {}
@ViewChild('textFilterInput') @ViewChild('textFilterInput')
@ -241,6 +260,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
dateCreatedRelativeDate: RelativeDate dateCreatedRelativeDate: RelativeDate
dateAddedRelativeDate: RelativeDate dateAddedRelativeDate: RelativeDate
permissionsSelectionModel = new PermissionsSelectionModel()
_unmodifiedFilterRules: FilterRule[] = [] _unmodifiedFilterRules: FilterRule[] = []
_filterRules: FilterRule[] = [] _filterRules: FilterRule[] = []
@ -274,6 +295,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
this.dateCreatedRelativeDate = null this.dateCreatedRelativeDate = null
this.dateAddedRelativeDate = null this.dateAddedRelativeDate = null
this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS
this.permissionsSelectionModel.clear()
value.forEach((rule) => { value.forEach((rule) => {
switch (rule.rule_type) { switch (rule.rule_type) {
@ -441,6 +463,35 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
this.textFilterModifier = TEXT_FILTER_MODIFIER_LT this.textFilterModifier = TEXT_FILTER_MODIFIER_LT
this._textFilter = rule.value this._textFilter = rule.value
break break
case FILTER_OWNER:
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.SELF
this.permissionsSelectionModel.hideUnowned = false
if (rule.value)
this.permissionsSelectionModel.userID = parseInt(rule.value, 10)
break
case FILTER_OWNER_ANY:
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.OTHERS
if (rule.value)
this.permissionsSelectionModel.includeUsers.push(
parseInt(rule.value, 10)
)
break
case FILTER_OWNER_DOES_NOT_INCLUDE:
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.NOT_SELF
if (rule.value)
this.permissionsSelectionModel.excludeUsers.push(
parseInt(rule.value, 10)
)
break
case FILTER_OWNER_ISNULL:
if (rule.value === 'true' || rule.value === '1') {
this.permissionsSelectionModel.hideUnowned = false
this.permissionsSelectionModel.ownerFilter = OwnerFilterType.UNOWNED
} else {
this.permissionsSelectionModel.hideUnowned =
rule.value === 'false' || rule.value === '0'
break
}
} }
}) })
this.rulesModified = filterRulesDiffer( this.rulesModified = filterRulesDiffer(
@ -702,6 +753,40 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
} }
} }
} }
if (this.permissionsSelectionModel.ownerFilter == OwnerFilterType.SELF) {
filterRules.push({
rule_type: FILTER_OWNER,
value: this.permissionsSelectionModel.userID.toString(),
})
} else if (
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.NOT_SELF
) {
filterRules.push({
rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
value: this.permissionsSelectionModel.excludeUsers?.join(','),
})
} else if (
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.OTHERS
) {
filterRules.push({
rule_type: FILTER_OWNER_ANY,
value: this.permissionsSelectionModel.includeUsers?.join(','),
})
} else if (
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.UNOWNED
) {
filterRules.push({
rule_type: FILTER_OWNER_ISNULL,
value: 'true',
})
}
if (this.permissionsSelectionModel.hideUnowned) {
filterRules.push({
rule_type: FILTER_OWNER_ISNULL,
value: 'false',
})
}
return filterRules return filterRules
} }

View File

@ -2,7 +2,7 @@
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *appIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>Create</button> <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *appIfPermissions="{ action: PermissionAction.Add, type: permissionType }" i18n>Create</button>
</app-page-header> </app-page-header>
<div class="row"> <div class="row mb-3">
<div class="col-md mb-2 mb-xl-0"> <div class="col-md mb-2 mb-xl-0">
<div class="form-inline d-flex align-items-center"> <div class="form-inline d-flex align-items-center">
<label class="text-muted me-2 mb-0" i18n>Filter by:</label> <label class="text-muted me-2 mb-0" i18n>Filter by:</label>
@ -10,7 +10,7 @@
</div> </div>
</div> </div>
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination> <ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
</div> </div>
<table class="table table-striped align-middle border shadow-sm"> <table class="table table-striped align-middle border shadow-sm">
@ -72,5 +72,5 @@
<div class="d-flex"> <div class="d-flex">
<div i18n *ngIf="collectionSize > 0">{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</div> <div i18n *ngIf="collectionSize > 0">{collectionSize, plural, =1 {One {{typeName}}} other {{{collectionSize || 0}} total {{typeNamePlural}}}}</div>
<ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination> <ngb-pagination *ngIf="collectionSize > 20" class="ms-auto" [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
</div> </div>

View File

@ -125,8 +125,8 @@
</div> </div>
<div class="col-2"> <div class="col-2">
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()"> <button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg fill="currentColor" class="buttonicon-sm me-1">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/> <use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><ng-container i18n>Reset</ng-container> </svg><ng-container i18n>Reset</ng-container>
</button> </button>
</div> </div>

View File

@ -41,6 +41,11 @@ export const FILTER_TITLE_CONTENT = 19
export const FILTER_FULLTEXT_QUERY = 20 export const FILTER_FULLTEXT_QUERY = 20
export const FILTER_FULLTEXT_MORELIKE = 21 export const FILTER_FULLTEXT_MORELIKE = 21
export const FILTER_OWNER = 32
export const FILTER_OWNER_ANY = 33
export const FILTER_OWNER_ISNULL = 34
export const FILTER_OWNER_DOES_NOT_INCLUDE = 35
export const FILTER_RULE_TYPES: FilterRuleType[] = [ export const FILTER_RULE_TYPES: FilterRuleType[] = [
{ {
id: FILTER_TITLE, id: FILTER_TITLE,
@ -242,6 +247,30 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
datatype: 'number', datatype: 'number',
multi: false, multi: false,
}, },
{
id: FILTER_OWNER,
filtervar: 'owner__id',
datatype: 'number',
multi: false,
},
{
id: FILTER_OWNER_ANY,
filtervar: 'owner__id__in',
datatype: 'number',
multi: true,
},
{
id: FILTER_OWNER_ISNULL,
filtervar: 'owner__isnull',
datatype: 'boolean',
multi: false,
},
{
id: FILTER_OWNER_DOES_NOT_INCLUDE,
filtervar: 'owner__id__none',
datatype: 'number',
multi: true,
},
] ]
export interface FilterRuleType { export interface FilterRuleType {

View File

@ -0,0 +1,42 @@
import { Pipe, PipeTransform } from '@angular/core'
import { UserService } from '../services/rest/user.service'
import {
PermissionAction,
PermissionType,
PermissionsService,
} from '../services/permissions.service'
import { PaperlessUser } from '../data/paperless-user'
@Pipe({
name: 'username',
})
export class UsernamePipe implements PipeTransform {
users: PaperlessUser[]
constructor(
permissionsService: PermissionsService,
userService: UserService
) {
if (
permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.User
)
) {
userService.listAll().subscribe((r) => (this.users = r.results))
}
}
transform(userID: number): string {
return this.users
? this.getName(this.users.find((u) => u.id === userID)) ?? ''
: $localize`Shared`
}
getName(user: PaperlessUser): string {
if (!user) return ''
const name = [user.first_name, user.last_name].join(' ')
if (name.length > 1) return name.trim()
return user.username
}
}

View File

@ -23,6 +23,7 @@ export const DOCUMENT_SORT_FIELDS = [
{ field: 'added', name: $localize`Added` }, { field: 'added', name: $localize`Added` },
{ field: 'modified', name: $localize`Modified` }, { field: 'modified', name: $localize`Modified` },
{ field: 'num_notes', name: $localize`Notes` }, { field: 'num_notes', name: $localize`Notes` },
{ field: 'owner', name: $localize`Owner` },
] ]
export const DOCUMENT_SORT_FIELDS_FULLTEXT = [ export const DOCUMENT_SORT_FIELDS_FULLTEXT = [

View File

@ -304,6 +304,10 @@ textarea,
cursor: not-allowed; cursor: not-allowed;
} }
ul.pagination {
margin-bottom: 0;
}
.page-link { .page-link {
color: var(--bs-secondary); color: var(--bs-secondary);
background-color: var(--bs-body-bg); background-color: var(--bs-body-bg);
@ -317,7 +321,6 @@ textarea,
.page-item.active .page-link { .page-item.active .page-link {
background-color: var(--bs-primary); background-color: var(--bs-primary);
border-color: var(--bs-primary) !important;
color: var(--bs-light); color: var(--bs-light);
} }
@ -425,6 +428,11 @@ textarea,
height: 1.2em; height: 1.2em;
} }
.buttonicon-sm {
width: 1em;
height: 1em;
}
.sidebaricon { .sidebaricon {
width: 16px; width: 16px;
height: 16px; height: 16px;

View File

@ -75,6 +75,10 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
color: var(--bs-body-color) !important; color: var(--bs-body-color) !important;
} }
.btn {
--bs-btn-disabled-opacity: 0.35;
}
.btn-primary { .btn-primary {
&:hover, &:focus, &.active, &:active { &:hover, &:focus, &.active, &:active {
color: var(--bs-body-color) !important; color: var(--bs-body-color) !important;

View File

@ -105,6 +105,8 @@ class DocumentFilterSet(FilterSet):
title_content = TitleContentFilter() title_content = TitleContentFilter()
owner__id__none = ObjectFilter(field_name="owner", exclude=True)
class Meta: class Meta:
model = Document model = Document
fields = { fields = {
@ -125,6 +127,8 @@ class DocumentFilterSet(FilterSet):
"storage_path": ["isnull"], "storage_path": ["isnull"],
"storage_path__id": ID_KWARGS, "storage_path__id": ID_KWARGS,
"storage_path__name": CHAR_KWARGS, "storage_path__name": CHAR_KWARGS,
"owner": ["isnull"],
"owner__id": ID_KWARGS,
} }

View File

@ -208,11 +208,13 @@ class DelayedQuery:
for document_type_id in v.split(","): for document_type_id in v.split(","):
criterias.append(query.Not(query.Term("type_id", document_type_id))) criterias.append(query.Not(query.Term("type_id", document_type_id)))
elif k == "correspondent__isnull": elif k == "correspondent__isnull":
criterias.append(query.Term("has_correspondent", v == "false")) criterias.append(
query.Term("has_correspondent", self.evalBoolean(v) is False),
)
elif k == "is_tagged": elif k == "is_tagged":
criterias.append(query.Term("has_tag", v == "true")) criterias.append(query.Term("has_tag", self.evalBoolean(v)))
elif k == "document_type__isnull": elif k == "document_type__isnull":
criterias.append(query.Term("has_type", v == "false")) criterias.append(query.Term("has_type", self.evalBoolean(v) is False))
elif k == "created__date__lt": elif k == "created__date__lt":
criterias.append( criterias.append(
query.DateRange("created", start=None, end=isoparse(v)), query.DateRange("created", start=None, end=isoparse(v)),
@ -236,7 +238,19 @@ class DelayedQuery:
for storage_path_id in v.split(","): for storage_path_id in v.split(","):
criterias.append(query.Not(query.Term("path_id", storage_path_id))) criterias.append(query.Not(query.Term("path_id", storage_path_id)))
elif k == "storage_path__isnull": elif k == "storage_path__isnull":
criterias.append(query.Term("has_path", v == "false")) criterias.append(query.Term("has_path", self.evalBoolean(v) is False))
elif k == "owner__isnull":
criterias.append(query.Term("has_owner", self.evalBoolean(v) is False))
elif k == "owner__id":
criterias.append(query.Term("owner_id", v))
elif k == "owner__id__in":
owners_in = []
for owner_id in v.split(","):
owners_in.append(query.Term("owner_id", owner_id))
criterias.append(query.Or(owners_in))
elif k == "owner__id__none":
for owner_id in v.split(","):
criterias.append(query.Not(query.Term("owner_id", owner_id)))
user_criterias = [query.Term("has_owner", False)] user_criterias = [query.Term("has_owner", False)]
if "user" in self.query_params: if "user" in self.query_params:
@ -254,6 +268,9 @@ class DelayedQuery:
else: else:
return query.Or(user_criterias) if len(user_criterias) > 0 else None return query.Or(user_criterias) if len(user_criterias) > 0 else None
def evalBoolean(self, val):
return val.lower() in {"true", "1"}
def _get_query_sortedby(self): def _get_query_sortedby(self):
if "ordering" not in self.query_params: if "ordering" not in self.query_params:
return None, False return None, False
@ -269,6 +286,7 @@ class DelayedQuery:
"document_type__name": "type", "document_type__name": "type",
"archive_serial_number": "asn", "archive_serial_number": "asn",
"num_notes": "num_notes", "num_notes": "num_notes",
"owner": "owner",
} }
if field.startswith("-"): if field.startswith("-"):

View File

@ -0,0 +1,58 @@
# Generated by Django 4.1.7 on 2023-05-04 04:11
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1035_rename_comment_note"),
]
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"),
(32, "owner is"),
(33, "has owner in"),
(34, "does not have owner"),
(35, "does not have owner in"),
],
verbose_name="rule type",
),
),
]

View File

@ -448,6 +448,10 @@ class SavedViewFilterRule(models.Model):
(29, _("does not have document type in")), (29, _("does not have document type in")),
(30, _("has storage path in")), (30, _("has storage path in")),
(31, _("does not have storage path in")), (31, _("does not have storage path in")),
(32, _("owner is")),
(33, _("has owner in")),
(34, _("does not have owner")),
(35, _("does not have owner in")),
] ]
saved_view = models.ForeignKey( saved_view = models.ForeignKey(

View File

@ -469,6 +469,98 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
results = response.data["results"] results = response.data["results"]
self.assertEqual(len(results), 0) self.assertEqual(len(results), 0)
def test_document_owner_filters(self):
"""
GIVEN:
- Documents with owners, with and without granted permissions
WHEN:
- User filters by owner
THEN:
- Owner filters work correctly but still respect permissions
"""
u1 = User.objects.create_user("user1")
u2 = User.objects.create_user("user2")
u1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
u2.user_permissions.add(*Permission.objects.filter(codename="view_document"))
u1_doc1 = Document.objects.create(
title="none1",
checksum="A",
mime_type="application/pdf",
owner=u1,
)
Document.objects.create(
title="none2",
checksum="B",
mime_type="application/pdf",
owner=u2,
)
u0_doc1 = Document.objects.create(
title="none3",
checksum="C",
mime_type="application/pdf",
)
u1_doc2 = Document.objects.create(
title="none4",
checksum="D",
mime_type="application/pdf",
owner=u1,
)
u2_doc2 = Document.objects.create(
title="none5",
checksum="E",
mime_type="application/pdf",
owner=u2,
)
self.client.force_authenticate(user=u1)
assign_perm("view_document", u1, u2_doc2)
# Will not show any u1 docs or u2_doc1 which isn't shared
response = self.client.get(f"/api/documents/?owner__id__none={u1.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data["results"]
self.assertEqual(len(results), 2)
self.assertCountEqual(
[results[0]["id"], results[1]["id"]],
[u0_doc1.id, u2_doc2.id],
)
# Will not show any u1 docs, u0_doc1 which has no owner or u2_doc1 which isn't shared
response = self.client.get(
f"/api/documents/?owner__id__none={u1.id}&owner__isnull=false",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data["results"]
self.assertEqual(len(results), 1)
self.assertCountEqual([results[0]["id"]], [u2_doc2.id])
# Will not show any u1 docs, u2_doc2 which is shared but has owner
response = self.client.get(
f"/api/documents/?owner__id__none={u1.id}&owner__isnull=true",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data["results"]
self.assertEqual(len(results), 1)
self.assertCountEqual([results[0]["id"]], [u0_doc1.id])
# Will not show any u1 docs or u2_doc1 which is not shared
response = self.client.get(f"/api/documents/?owner__id={u2.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data["results"]
self.assertEqual(len(results), 1)
self.assertCountEqual([results[0]["id"]], [u2_doc2.id])
# Will not show u2_doc1 which is not shared
response = self.client.get(f"/api/documents/?owner__id__in={u1.id},{u2.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data["results"]
self.assertEqual(len(results), 3)
self.assertCountEqual(
[results[0]["id"], results[1]["id"], results[2]["id"]],
[u1_doc1.id, u1_doc2.id, u2_doc2.id],
)
def test_search(self): def test_search(self):
d1 = Document.objects.create( d1 = Document.objects.create(
title="invoice", title="invoice",
@ -996,15 +1088,15 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
) )
self.assertCountEqual( self.assertCountEqual(
search_query("&storage_path__isnull"), search_query("&storage_path__isnull=true"),
[d1.id, d2.id, d3.id, d4.id, d5.id], [d1.id, d2.id, d3.id, d4.id, d5.id],
) )
self.assertCountEqual( self.assertCountEqual(
search_query("&correspondent__isnull"), search_query("&correspondent__isnull=true"),
[d2.id, d3.id, d4.id, d5.id, d7.id], [d2.id, d3.id, d4.id, d5.id, d7.id],
) )
self.assertCountEqual( self.assertCountEqual(
search_query("&document_type__isnull"), search_query("&document_type__isnull=true"),
[d1.id, d3.id, d4.id, d5.id, d7.id], [d1.id, d3.id, d4.id, d5.id, d7.id],
) )
self.assertCountEqual( self.assertCountEqual(
@ -1116,18 +1208,30 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertEqual(r.data["count"], 2) self.assertEqual(r.data["count"], 2)
r = self.client.get("/api/documents/?query=test&document_type__id__none=1") r = self.client.get("/api/documents/?query=test&document_type__id__none=1")
self.assertEqual(r.data["count"], 2) self.assertEqual(r.data["count"], 2)
r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}")
self.assertEqual(r.data["count"], 1)
r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}")
self.assertEqual(r.data["count"], 1)
r = self.client.get(
f"/api/documents/?query=test&owner__id__none={u1.id}&owner__isnull=true",
)
self.assertEqual(r.data["count"], 1)
self.client.force_authenticate(user=u2) self.client.force_authenticate(user=u2)
r = self.client.get("/api/documents/?query=test") r = self.client.get("/api/documents/?query=test")
self.assertEqual(r.data["count"], 3) self.assertEqual(r.data["count"], 3)
r = self.client.get("/api/documents/?query=test&document_type__id__none=1") r = self.client.get("/api/documents/?query=test&document_type__id__none=1")
self.assertEqual(r.data["count"], 3) self.assertEqual(r.data["count"], 3)
r = self.client.get(f"/api/documents/?query=test&owner__id__none={u2.id}")
self.assertEqual(r.data["count"], 1)
self.client.force_authenticate(user=superuser) self.client.force_authenticate(user=superuser)
r = self.client.get("/api/documents/?query=test") r = self.client.get("/api/documents/?query=test")
self.assertEqual(r.data["count"], 4) self.assertEqual(r.data["count"], 4)
r = self.client.get("/api/documents/?query=test&document_type__id__none=1") r = self.client.get("/api/documents/?query=test&document_type__id__none=1")
self.assertEqual(r.data["count"], 4) self.assertEqual(r.data["count"], 4)
r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}")
self.assertEqual(r.data["count"], 3)
def test_search_filtering_with_object_perms(self): def test_search_filtering_with_object_perms(self):
""" """
@ -1157,6 +1261,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertEqual(r.data["count"], 2) self.assertEqual(r.data["count"], 2)
r = self.client.get("/api/documents/?query=test&document_type__id__none=1") r = self.client.get("/api/documents/?query=test&document_type__id__none=1")
self.assertEqual(r.data["count"], 2) self.assertEqual(r.data["count"], 2)
r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}")
self.assertEqual(r.data["count"], 1)
r = self.client.get(f"/api/documents/?query=test&owner__id={u1.id}")
self.assertEqual(r.data["count"], 1)
r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}")
self.assertEqual(r.data["count"], 1)
r = self.client.get("/api/documents/?query=test&owner__isnull=true")
self.assertEqual(r.data["count"], 1)
assign_perm("view_document", u1, d2) assign_perm("view_document", u1, d2)
assign_perm("view_document", u1, d3) assign_perm("view_document", u1, d3)
@ -1170,6 +1282,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertEqual(r.data["count"], 4) self.assertEqual(r.data["count"], 4)
r = self.client.get("/api/documents/?query=test&document_type__id__none=1") r = self.client.get("/api/documents/?query=test&document_type__id__none=1")
self.assertEqual(r.data["count"], 4) self.assertEqual(r.data["count"], 4)
r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}")
self.assertEqual(r.data["count"], 3)
r = self.client.get(f"/api/documents/?query=test&owner__id={u1.id}")
self.assertEqual(r.data["count"], 1)
r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}")
self.assertEqual(r.data["count"], 1)
r = self.client.get("/api/documents/?query=test&owner__isnull=true")
self.assertEqual(r.data["count"], 1)
def test_search_sorting(self): def test_search_sorting(self):
u1 = User.objects.create_user("user1") u1 = User.objects.create_user("user1")
@ -1251,6 +1371,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
search_query("&ordering=-num_notes"), search_query("&ordering=-num_notes"),
[d1.id, d3.id, d2.id], [d1.id, d3.id, d2.id],
) )
self.assertListEqual(
search_query("&ordering=owner"),
[d1.id, d2.id, d3.id],
)
self.assertListEqual(
search_query("&ordering=-owner"),
[d3.id, d2.id, d1.id],
)
def test_pagination_all(self): def test_pagination_all(self):
""" """

View File

@ -264,6 +264,7 @@ class DocumentViewSet(
"added", "added",
"archive_serial_number", "archive_serial_number",
"num_notes", "num_notes",
"owner",
) )
def get_queryset(self): def get_queryset(self):