Merge branch 'dev' into fix-5782

This commit is contained in:
shamoon 2024-02-25 16:53:03 -08:00 committed by GitHub
commit 86768f822e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 337 additions and 166 deletions

View File

@ -28,7 +28,7 @@ jobs:
stale-issue-message: > stale-issue-message: >
This issue has been automatically marked as stale because it has not had This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you recent activity. It will be closed if no further activity occurs. Thank you
for your contributions. for your contributions. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.
lock-threads: lock-threads:
name: 'Lock Old Threads' name: 'Lock Old Threads'
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -43,14 +43,17 @@ jobs:
This issue has been automatically locked since there This issue has been automatically locked since there
has not been any recent activity after it was closed. has not been any recent activity after it was closed.
Please open a new discussion or issue for related concerns. Please open a new discussion or issue for related concerns.
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.
pr-comment: > pr-comment: >
This pull request has been automatically locked since there This pull request has been automatically locked since there
has not been any recent activity after it was closed. has not been any recent activity after it was closed.
Please open a new discussion or issue for related concerns. Please open a new discussion or issue for related concerns.
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.
discussion-comment: > discussion-comment: >
This discussion has been automatically locked since there This discussion has been automatically locked since there
has not been any recent activity after it was closed. has not been any recent activity after it was closed.
Please open a new discussion for related concerns. Please open a new discussion for related concerns.
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.
close-answered-discussions: close-answered-discussions:
name: 'Close Answered Discussions' name: 'Close Answered Discussions'
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -90,7 +93,7 @@ jobs:
}`; }`;
const commentVariables = { const commentVariables = {
discussion: discussion.id, discussion: discussion.id,
body: 'This discussion has been automatically closed because it was marked as answered.', body: 'This discussion has been automatically closed because it was marked as answered. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.',
} }
await github.graphql(addCommentMutation, commentVariables) await github.graphql(addCommentMutation, commentVariables)
@ -180,7 +183,7 @@ jobs:
}`; }`;
const commentVariables = { const commentVariables = {
discussion: discussion.id, discussion: discussion.id,
body: 'This discussion has been automatically closed due to inactivity.', body: 'This discussion has been automatically closed due to inactivity. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.',
} }
await github.graphql(addCommentMutation, commentVariables); await github.graphql(addCommentMutation, commentVariables);
@ -258,7 +261,7 @@ jobs:
}`; }`;
const commentVariables = { const commentVariables = {
discussion: discussion.id, discussion: discussion.id,
body: 'This discussion has been automatically closed due to lack of community interest.', body: 'This discussion has been automatically closed due to lack of community support. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-respoistory-maintenance) for more details.',
} }
await github.graphql(addCommentMutation, commentVariables); await github.graphql(addCommentMutation, commentVariables);

View File

@ -137,3 +137,19 @@ All team members are notified when mentioned or assigned to a relevant issue or
We are not overly strict with inviting people to the organization. If you have read the [team permissions](#permissions) and think having additional access would enhance your contributions, please reach out to an [admin](#structure) of the team. We are not overly strict with inviting people to the organization. If you have read the [team permissions](#permissions) and think having additional access would enhance your contributions, please reach out to an [admin](#structure) of the team.
The admins occasionally invite contributors directly if we believe having them on a team will accelerate their work. The admins occasionally invite contributors directly if we believe having them on a team will accelerate their work.
# Automatic Respoistory Maintenance
The Paperless-ngx team appreciates all effort and interest from the community in filing bug reports, creating feature requests, sharing ideas and helping other
community members. That said, in an effort to keep the repository organized and managebale the project uses automatic handling of certain areas:
- Issues that cannot be reproduced will be marked 'stale' after 7 days of inactivity and closed after 14 further days of inactivity.
- Issues, pull requests and discussions that are closed will be locked after 30 days of inactivity.
- Discussions with a marked answer will be automatically closed.
- Discussions in the 'General' or 'Support' categories will be closed after 180 days of inactivity.
- Feature requests that do not meet the following thresholds will be closed: 5 "up-votes" after 180 days of inactivity or 10 "up-votes" after 365 days.
In all cases, threads can be re-opened by project maintainers and, of course, users can always create a new discussion for related concerns.
Finally, remember that all information remains searchable and 'closed' feature requests can still serve as inspiration for new features.
Thank you all for your contributions.

67
Pipfile.lock generated
View File

@ -392,41 +392,42 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380", "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b",
"sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589", "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce",
"sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea", "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88",
"sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65", "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7",
"sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a", "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20",
"sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3", "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9",
"sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008", "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff",
"sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1", "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1",
"sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2", "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764",
"sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635", "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b",
"sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2", "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298",
"sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90", "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1",
"sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee", "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824",
"sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a", "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257",
"sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242", "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a",
"sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12", "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129",
"sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2", "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb",
"sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d", "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929",
"sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be", "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854",
"sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee", "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52",
"sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6", "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923",
"sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529", "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885",
"sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929", "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0",
"sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1", "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd",
"sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6", "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2",
"sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a", "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18",
"sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446", "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b",
"sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9", "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992",
"sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888", "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74",
"sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4", "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660",
"sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33", "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925",
"sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f" "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"
], ],
"index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==42.0.2" "version": "==42.0.4"
}, },
"dateparser": { "dateparser": {
"hashes": [ "hashes": [

View File

@ -1491,6 +1491,10 @@
<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>
<context context-type="linenumber">96</context> <context context-type="linenumber">96</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">208</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
<context context-type="linenumber">38</context> <context context-type="linenumber">38</context>
@ -2017,7 +2021,7 @@
</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.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">304</context> <context context-type="linenumber">320</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.ts</context> <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.ts</context>
@ -2056,7 +2060,7 @@
</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.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">306</context> <context context-type="linenumber">322</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.ts</context> <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.ts</context>
@ -5025,7 +5029,11 @@
</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.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">302</context> <context context-type="linenumber">204</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">318</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5382975254277698192" datatype="html"> <trans-unit id="5382975254277698192" datatype="html">
@ -6219,7 +6227,7 @@
</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.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">289</context> <context context-type="linenumber">305</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4010735610815226758" datatype="html"> <trans-unit id="4010735610815226758" datatype="html">
@ -6302,26 +6310,26 @@
<source>{VAR_PLURAL, plural, =1 {One <x id="INTERPOLATION"/>} other {<x id="INTERPOLATION_1"/> total <x id="INTERPOLATION_2"/>}}</source> <source>{VAR_PLURAL, plural, =1 {One <x id="INTERPOLATION"/>} other {<x id="INTERPOLATION_1"/> total <x id="INTERPOLATION_2"/>}}</source>
<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>
<context context-type="linenumber">116</context> <context context-type="linenumber">110</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>
<context context-type="linenumber">116</context> <context context-type="linenumber">110</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>
<context context-type="linenumber">116</context> <context context-type="linenumber">110</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>
<context context-type="linenumber">116</context> <context context-type="linenumber">110</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="810888510148304696" datatype="html"> <trans-unit id="810888510148304696" datatype="html">
<source>Automatic</source> <source>Automatic</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">113</context> <context context-type="linenumber">116</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context> <context context-type="sourcefile">src/app/data/matching-model.ts</context>
@ -6332,7 +6340,7 @@
<source>None</source> <source>None</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">115</context> <context context-type="linenumber">118</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context> <context context-type="sourcefile">src/app/data/matching-model.ts</context>
@ -6343,63 +6351,70 @@
<source>Successfully created <x id="PH" equiv-text="this.typeName"/>.</source> <source>Successfully created <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">158</context> <context context-type="linenumber">161</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3928835053823658072" datatype="html"> <trans-unit id="3928835053823658072" datatype="html">
<source>Error occurred while creating <x id="PH" equiv-text="this.typeName"/>.</source> <source>Error occurred while creating <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">163</context> <context context-type="linenumber">166</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2541368547549828690" datatype="html"> <trans-unit id="2541368547549828690" datatype="html">
<source>Successfully updated <x id="PH" equiv-text="this.typeName"/>.</source> <source>Successfully updated <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">178</context> <context context-type="linenumber">181</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6442673774206210733" datatype="html"> <trans-unit id="6442673774206210733" datatype="html">
<source>Error occurred while saving <x id="PH" equiv-text="this.typeName"/>.</source> <source>Error occurred while saving <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">183</context> <context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit id="8371896857609524947" datatype="html">
<source>Associated documents will not be deleted.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">206</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6639207128255974941" datatype="html"> <trans-unit id="6639207128255974941" datatype="html">
<source>Error while deleting element</source> <source>Error while deleting element</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">207</context> <context context-type="linenumber">222</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4863024195229581844" datatype="html"> <trans-unit id="4863024195229581844" datatype="html">
<source>Permissions updated successfully</source> <source>Permissions updated successfully</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">282</context> <context context-type="linenumber">298</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1464476612812630086" datatype="html"> <trans-unit id="1464476612812630086" datatype="html">
<source>This operation will permanently delete all objects.</source> <source>This operation will permanently delete all objects.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">303</context> <context context-type="linenumber">319</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5897787932098828336" datatype="html"> <trans-unit id="5897787932098828336" datatype="html">
<source>Objects deleted successfully</source> <source>Objects deleted successfully</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">317</context> <context context-type="linenumber">333</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8273353839648035634" datatype="html"> <trans-unit id="8273353839648035634" datatype="html">
<source>Error deleting objects</source> <source>Error deleting objects</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<context context-type="linenumber">323</context> <context context-type="linenumber">339</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5101757640976222639" datatype="html"> <trans-unit id="5101757640976222639" datatype="html">

View File

@ -11124,9 +11124,9 @@
} }
}, },
"node_modules/ip": { "node_modules/ip": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
"dev": true "dev": true
}, },
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {

View File

@ -94,6 +94,10 @@ Object.defineProperty(navigator, 'clipboard', {
}) })
Object.defineProperty(navigator, 'canShare', { value: () => true }) Object.defineProperty(navigator, 'canShare', { value: () => true })
Object.defineProperty(window, 'ResizeObserver', { value: mock() }) Object.defineProperty(window, 'ResizeObserver', { value: mock() })
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
})
HTMLCanvasElement.prototype.getContext = < HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext typeof HTMLCanvasElement.prototype.getContext

View File

@ -309,10 +309,15 @@ describe('SettingsComponent', () => {
component.store.getValue()['displayLanguage'] = 'en-US' component.store.getValue()['displayLanguage'] = 'en-US'
component.store.getValue()['updateCheckingEnabled'] = false component.store.getValue()['updateCheckingEnabled'] = false
component.settingsForm.value.displayLanguage = 'en-GB' component.settingsForm.value.displayLanguage = 'en-GB'
component.settingsForm.value.updateCheckingEnabled = true jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
jest.spyOn(settingsService, 'storeSettings').mockReturnValueOnce(of(true))
component.saveSettings() component.saveSettings()
expect(toast.actionName).toEqual('Reload now') expect(toast.actionName).toEqual('Reload now')
component.settingsForm.value.updateCheckingEnabled = true
component.saveSettings()
expect(toast.actionName).toEqual('Reload now')
toast.action()
}) })
it('should allow setting theme color, visually apply change immediately but not save', () => { it('should allow setting theme color, visually apply change immediately but not save', () => {

View File

@ -4,16 +4,16 @@
(click)="isMenuCollapsed = !isMenuCollapsed"> (click)="isMenuCollapsed = !isMenuCollapsed">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<a class="navbar-brand d-flex col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0" <a class="navbar-brand d-flex align-items-center me-0 px-3 py-3 order-sm-0"
[ngClass]="{ 'slim': slimSidebarEnabled, 'd-flex col-auto col-md-3 col-lg-2' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }" [ngClass]="{ 'slim': slimSidebarEnabled, 'col-auto col-md-3 col-lg-2' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
routerLink="/dashboard" routerLink="/dashboard"
tourAnchor="tour.intro"> tourAnchor="tour.intro">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" height="1.5em" fill="currentColor">
<path <path
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
transform="translate(0 0)" /> transform="translate(0 0)" />
</svg> </svg>
<div class="ms-2 d-inline-block" [class.visually-hidden]="slimSidebarEnabled"> <div class="ms-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled">
@if (customAppTitle?.length) { @if (customAppTitle?.length) {
<div class="d-flex flex-column align-items-start"> <div class="d-flex flex-column align-items-start">
<span class="title">{{customAppTitle}}</span> <span class="title">{{customAppTitle}}</span>

View File

@ -493,12 +493,17 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
expect(changedResult.getExcludedItems()).toEqual(items) expect(changedResult.getExcludedItems()).toEqual(items)
})) }))
it('FilterableDropdownSelectionModel should sort items by state', () => { it('selection model should sort items by state', () => {
component.items = items component.items = items.concat([{ id: null, name: 'Null B' }])
component.selectionModel = selectionModel component.selectionModel = selectionModel
selectionModel.toggle(items[1].id) selectionModel.toggle(items[1].id)
selectionModel.apply() selectionModel.apply()
expect(selectionModel.itemsSorted).toEqual([nullItem, items[1], items[0]]) expect(selectionModel.itemsSorted).toEqual([
nullItem,
{ id: null, name: 'Null B' },
items[1],
items[0],
])
}) })
it('should set support create, keep open model and call createRef method', fakeAsync(() => { it('should set support create, keep open model and call createRef method', fakeAsync(() => {
@ -542,4 +547,34 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
tick(300) tick(300)
expect(createSpy).toHaveBeenCalled() expect(createSpy).toHaveBeenCalled()
})) }))
it('should exclude item and trigger change event', () => {
const id = 1
const state = ToggleableItemState.Selected
component.selectionModel = selectionModel
component.manyToOne = true
component.selectionModel.singleSelect = true
component.selectionModel.intersection = Intersection.Include
component.selectionModel['temporarySelectionStates'].set(id, state)
const changedSpy = jest.spyOn(component.selectionModel.changed, 'next')
component.selectionModel.exclude(id)
expect(component.selectionModel.temporaryLogicalOperator).toBe(
LogicalOperator.And
)
expect(component.selectionModel['temporarySelectionStates'].get(id)).toBe(
ToggleableItemState.Excluded
)
expect(component.selectionModel['temporarySelectionStates'].size).toBe(1)
expect(changedSpy).toHaveBeenCalled()
})
it('should initialize selection states and apply changes', () => {
selectionModel.items = items
const map = new Map<number, ToggleableItemState>()
map.set(1, ToggleableItemState.Selected)
map.set(2, ToggleableItemState.Excluded)
selectionModel.init(map)
expect(selectionModel.getSelectedItems()).toEqual([items[0]])
expect(selectionModel.getExcludedItems()).toEqual([items[1]])
})
}) })

View File

@ -275,7 +275,7 @@ export class FilterableDropdownSelectionModel {
) )
} }
init(map) { init(map: Map<number, ToggleableItemState>) {
this.temporarySelectionStates = map this.temporarySelectionStates = map
this.apply() this.apply()
} }

View File

@ -118,4 +118,18 @@ describe('SelectComponent', () => {
tick(3000) tick(3000)
expect(clearSpy).toHaveBeenCalled() expect(clearSpy).toHaveBeenCalled()
})) }))
it('should emit filtered documents', () => {
component.value = 10
component.items = items
const emitSpy = jest.spyOn(component.filterDocuments, 'emit')
component.onFilterDocuments()
expect(emitSpy).toHaveBeenCalledWith([items[2]])
})
it('should return the correct filter button title', () => {
component.title = 'Tag'
const expectedTitle = `Filter documents with this ${component.title}`
expect(component.filterButtonTitle).toEqual(expectedTitle)
})
}) })

View File

@ -169,4 +169,12 @@ describe('TagsComponent', () => {
expect(component.getTag(2)).toEqual(tags[1]) expect(component.getTag(2)).toEqual(tags[1])
expect(component.getTag(4)).toBeUndefined() expect(component.getTag(4)).toBeUndefined()
}) })
it('should emit filtered documents', () => {
component.value = [10]
component.tags = tags
const emitSpy = jest.spyOn(component.filterDocuments, 'emit')
component.onFilterDocuments()
expect(emitSpy).toHaveBeenCalledWith([tags[2]])
})
}) })

View File

@ -119,6 +119,8 @@ describe('UploadFileWidgetComponent', () => {
const processingStatus = new FileStatus() const processingStatus = new FileStatus()
processingStatus.phase = FileStatusPhase.WORKING processingStatus.phase = FileStatusPhase.WORKING
expect(component.getStatusColor(processingStatus)).toEqual('primary') expect(component.getStatusColor(processingStatus)).toEqual('primary')
processingStatus.phase = FileStatusPhase.UPLOADING
expect(component.getStatusColor(processingStatus)).toEqual('primary')
const failedStatus = new FileStatus() const failedStatus = new FileStatus()
failedStatus.phase = FileStatusPhase.FAILED failedStatus.phase = FileStatusPhase.FAILED
expect(component.getStatusColor(failedStatus)).toEqual('danger') expect(component.getStatusColor(failedStatus)).toEqual('danger')

View File

@ -92,15 +92,9 @@
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)"> <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container> <i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button> </button>
<pngx-confirm-button <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
label="Delete" <i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
i18n-label </button>
(confirm)="deleteObject(object)"
*pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }"
[disabled]="!userCanDelete(object)"
buttonClasses=" btn-sm btn-outline-danger"
iconName="trash">
</pngx-confirm-button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -13,7 +13,6 @@ import {
NgbModalModule, NgbModalModule,
NgbModalRef, NgbModalRef,
NgbPaginationModule, NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
@ -38,7 +37,6 @@ import { MATCH_NONE } from 'src/app/data/matching-model'
import { MATCH_LITERAL } from 'src/app/data/matching-model' import { MATCH_LITERAL } from 'src/app/data/matching-model'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component' import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-filter-service' import { BulkEditObjectOperation } from 'src/app/services/rest/abstract-name-filter-service'
const tags: Tag[] = [ const tags: Tag[] = [
@ -78,7 +76,6 @@ describe('ManagementListComponent', () => {
SafeHtmlPipe, SafeHtmlPipe,
ConfirmDialogComponent, ConfirmDialogComponent,
PermissionsDialogComponent, PermissionsDialogComponent,
ConfirmButtonComponent,
], ],
providers: [ providers: [
{ {
@ -100,7 +97,6 @@ describe('ManagementListComponent', () => {
NgbModalModule, NgbModalModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
NgbPopoverModule,
], ],
}).compileComponents() }).compileComponents()
@ -197,23 +193,27 @@ describe('ManagementListComponent', () => {
}) })
it('should support delete, show notification on error / success', () => { it('should support delete, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastErrorSpy = jest.spyOn(toastService, 'showError')
const deleteSpy = jest.spyOn(tagService, 'delete') const deleteSpy = jest.spyOn(tagService, 'delete')
const reloadSpy = jest.spyOn(component, 'reloadData') const reloadSpy = jest.spyOn(component, 'reloadData')
const deleteButton = fixture.debugElement.query( const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8]
By.directive(ConfirmButtonComponent) deleteButton.triggerEventHandler('click')
)
expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as ConfirmDialogComponent
// fail first // fail first
deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting'))) deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting')))
deleteButton.nativeElement.dispatchEvent(new Event('confirm')) editDialog.confirmClicked.emit()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled() expect(reloadSpy).not.toHaveBeenCalled()
// succeed // succeed
deleteSpy.mockReturnValueOnce(of(true)) deleteSpy.mockReturnValueOnce(of(true))
deleteButton.nativeElement.dispatchEvent(new Event('confirm')) editDialog.confirmClicked.emit()
expect(reloadSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled()
}) })

View File

@ -15,7 +15,10 @@ import {
MATCH_NONE, MATCH_NONE,
} from 'src/app/data/matching-model' } from 'src/app/data/matching-model'
import { ObjectWithId } from 'src/app/data/object-with-id' import { ObjectWithId } from 'src/app/data/object-with-id'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions' import {
ObjectWithPermissions,
PermissionsObject,
} from 'src/app/data/object-with-permissions'
import { import {
SortableDirective, SortableDirective,
SortEvent, SortEvent,
@ -194,21 +197,34 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
]) ])
} }
deleteObject(object: T) { openDeleteDialog(object: T) {
var activeModal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
activeModal.componentInstance.title = $localize`Confirm delete`
activeModal.componentInstance.messageBold = this.getDeleteMessage(object)
activeModal.componentInstance.message = $localize`Associated documents will not be deleted.`
activeModal.componentInstance.btnClass = 'btn-danger'
activeModal.componentInstance.btnCaption = $localize`Delete`
activeModal.componentInstance.confirmClicked.subscribe(() => {
activeModal.componentInstance.buttonsEnabled = false
this.service this.service
.delete(object) .delete(object)
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({ .subscribe({
next: () => { next: () => {
activeModal.close()
this.reloadData() this.reloadData()
}, },
error: (error) => { error: (error) => {
activeModal.componentInstance.buttonsEnabled = true
this.toastService.showError( this.toastService.showError(
$localize`Error while deleting element`, $localize`Error while deleting element`,
error error
) )
}, },
}) })
})
} }
get nameFilter() { get nameFilter() {

View File

@ -17,6 +17,7 @@ describe('DirtyFormGuard', () => {
let guard: DirtyFormGuard let guard: DirtyFormGuard
let component: DirtyComponent let component: DirtyComponent
let route: ActivatedRoute let route: ActivatedRoute
let modalService: NgbModal
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -37,6 +38,7 @@ describe('DirtyFormGuard', () => {
guard = TestBed.inject(DirtyFormGuard) guard = TestBed.inject(DirtyFormGuard)
route = TestBed.inject(ActivatedRoute) route = TestBed.inject(ActivatedRoute)
modalService = TestBed.inject(NgbModal)
const fixture = TestBed.createComponent(GenericDirtyComponent) const fixture = TestBed.createComponent(GenericDirtyComponent)
component = fixture.componentInstance component = fixture.componentInstance
@ -57,9 +59,14 @@ describe('DirtyFormGuard', () => {
component.isDirty$ = true component.isDirty$ = true
const confirmSpy = jest.spyOn(guard, 'confirmChanges') const confirmSpy = jest.spyOn(guard, 'confirmChanges')
const canDeactivate = guard.canDeactivate(component, route.snapshot) const canDeactivate = guard.canDeactivate(component, route.snapshot)
let modal
modalService.activeInstances.subscribe((instances) => {
modal = instances[0]
})
canDeactivate.subscribe() canDeactivate.subscribe()
expect(canDeactivate).toHaveProperty('source') // Observable expect(canDeactivate).toHaveProperty('source') // Observable
expect(confirmSpy).toHaveBeenCalled() expect(confirmSpy).toHaveBeenCalled()
modal.componentInstance.confirmClicked.next()
}) })
}) })

View File

@ -108,6 +108,7 @@ describe('OpenDocumentsService', () => {
}) })
it('should close documents', () => { it('should close documents', () => {
openDocumentsService.closeDocument({ id: 999 } as any)
subscriptions.push( subscriptions.push(
openDocumentsService.openDocument(documents[0]).subscribe() openDocumentsService.openDocument(documents[0]).subscribe()
) )
@ -128,15 +129,21 @@ describe('OpenDocumentsService', () => {
subscriptions.push( subscriptions.push(
openDocumentsService.openDocument(documents[0]).subscribe() openDocumentsService.openDocument(documents[0]).subscribe()
) )
openDocumentsService.setDirty({ id: 999 }, true) // coverage
openDocumentsService.setDirty(documents[0], false) openDocumentsService.setDirty(documents[0], false)
expect(openDocumentsService.hasDirty()).toBeFalsy() expect(openDocumentsService.hasDirty()).toBeFalsy()
openDocumentsService.setDirty(documents[0], true) openDocumentsService.setDirty(documents[0], true)
expect(openDocumentsService.hasDirty()).toBeTruthy() expect(openDocumentsService.hasDirty()).toBeTruthy()
let openModal
modalService.activeInstances.subscribe((instances) => {
openModal = instances[0]
})
const modalSpy = jest.spyOn(modalService, 'open') const modalSpy = jest.spyOn(modalService, 'open')
subscriptions.push( subscriptions.push(
openDocumentsService.closeDocument(documents[0]).subscribe() openDocumentsService.closeDocument(documents[0]).subscribe()
) )
expect(modalSpy).toHaveBeenCalled() expect(modalSpy).toHaveBeenCalled()
openModal.componentInstance.confirmClicked.next()
}) })
it('should allow set dirty status, warn on closeAll', () => { it('should allow set dirty status, warn on closeAll', () => {
@ -148,9 +155,14 @@ describe('OpenDocumentsService', () => {
) )
openDocumentsService.setDirty(documents[0], true) openDocumentsService.setDirty(documents[0], true)
expect(openDocumentsService.hasDirty()).toBeTruthy() expect(openDocumentsService.hasDirty()).toBeTruthy()
let openModal
modalService.activeInstances.subscribe((instances) => {
openModal = instances[0]
})
const modalSpy = jest.spyOn(modalService, 'open') const modalSpy = jest.spyOn(modalService, 'open')
subscriptions.push(openDocumentsService.closeAll().subscribe()) subscriptions.push(openDocumentsService.closeAll().subscribe())
expect(modalSpy).toHaveBeenCalled() expect(modalSpy).toHaveBeenCalled()
openModal.componentInstance.confirmClicked.next()
}) })
it('should load open documents from localStorage', () => { it('should load open documents from localStorage', () => {

View File

@ -58,12 +58,25 @@ describe(`Additional service tests for MailAccountService`, () => {
it('should support patchMany', () => { it('should support patchMany', () => {
subscription = service.patchMany(mail_accounts).subscribe() subscription = service.patchMany(mail_accounts).subscribe()
mail_accounts.forEach((mail_account) => { mail_accounts.forEach((mail_account) => {
const reqs = httpTestingController.match( const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${mail_account.id}/` `${environment.apiBaseUrl}${endpoint}/${mail_account.id}/`
) )
expect(reqs).toHaveLength(1) expect(req.request.method).toEqual('PATCH')
expect(reqs[0].request.method).toEqual('PATCH') req.flush(mail_account)
}) })
httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
)
})
it('should support reload', () => {
service['reload']()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
)
expect(req.request.method).toEqual('GET')
req.flush({ results: mail_accounts })
expect(service.allAccounts).toEqual(mail_accounts)
}) })
beforeEach(() => { beforeEach(() => {

View File

@ -76,12 +76,26 @@ describe(`Additional service tests for MailRuleService`, () => {
it('should support patchMany', () => { it('should support patchMany', () => {
subscription = service.patchMany(mail_rules).subscribe() subscription = service.patchMany(mail_rules).subscribe()
mail_rules.forEach((mail_rule) => { mail_rules.forEach((mail_rule) => {
const reqs = httpTestingController.match( const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${mail_rule.id}/` `${environment.apiBaseUrl}${endpoint}/${mail_rule.id}/`
) )
expect(reqs).toHaveLength(1) expect(req.request.method).toEqual('PATCH')
expect(reqs[0].request.method).toEqual('PATCH') req.flush(mail_rule)
}) })
const reloadReq = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
)
reloadReq.flush({ results: mail_rules })
})
it('should support reload', () => {
service['reload']()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
)
expect(req.request.method).toEqual('GET')
req.flush({ results: mail_rules })
expect(service.allRules).toEqual(mail_rules)
}) })
beforeEach(() => { beforeEach(() => {

View File

@ -262,7 +262,7 @@ a.btn-link:focus-visible,
} }
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked { .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked {
background-color: var(--pngx-bg-darker) !important; background-color: var(--pngx-bg-alt) !important;
color: var(--pngx-body-color-accent) !important; color: var(--pngx-body-color-accent) !important;
} }
@ -439,7 +439,7 @@ ul.pagination {
color: var(--bs-body-color); color: var(--bs-body-color);
&:hover, &:focus { &:hover, &:focus {
background-color: var(--pngx-bg-darker); background-color: var(--pngx-bg-alt);
color: var(--bs-body-color); color: var(--bs-body-color);
} }

View File

@ -286,10 +286,10 @@ class Command(BaseCommand):
def handle_inotify(self, directory, recursive, is_testing: bool): def handle_inotify(self, directory, recursive, is_testing: bool):
logger.info(f"Using inotify to watch directory for changes: {directory}") logger.info(f"Using inotify to watch directory for changes: {directory}")
timeout = None timeout_ms = None
if is_testing: if is_testing:
timeout = self.testing_timeout_ms timeout_ms = self.testing_timeout_ms
logger.debug(f"Configuring timeout to {timeout}ms") logger.debug(f"Configuring timeout to {timeout_ms}ms")
inotify = INotify() inotify = INotify()
inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO | flags.MODIFY inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO | flags.MODIFY
@ -298,7 +298,8 @@ class Command(BaseCommand):
else: else:
descriptor = inotify.add_watch(directory, inotify_flags) descriptor = inotify.add_watch(directory, inotify_flags)
inotify_debounce: Final[float] = settings.CONSUMER_INOTIFY_DELAY inotify_debounce_secs: Final[float] = settings.CONSUMER_INOTIFY_DELAY
inotify_debounce_ms: Final[int] = inotify_debounce_secs * 1000
finished = False finished = False
@ -306,7 +307,7 @@ class Command(BaseCommand):
while not finished: while not finished:
try: try:
for event in inotify.read(timeout=timeout): for event in inotify.read(timeout=timeout_ms):
path = inotify.get_path(event.wd) if recursive else directory path = inotify.get_path(event.wd) if recursive else directory
filepath = os.path.join(path, event.name) filepath = os.path.join(path, event.name)
if flags.MODIFY in flags.from_mask(event.mask): if flags.MODIFY in flags.from_mask(event.mask):
@ -323,7 +324,7 @@ class Command(BaseCommand):
# Current time - last time over the configured timeout # Current time - last time over the configured timeout
waited_long_enough = ( waited_long_enough = (
monotonic() - last_event_time monotonic() - last_event_time
) > inotify_debounce ) > inotify_debounce_secs
# Also make sure the file exists still, some scanners might write a # Also make sure the file exists still, some scanners might write a
# temporary file first # temporary file first
@ -342,11 +343,11 @@ class Command(BaseCommand):
# If files are waiting, need to exit read() to check them # If files are waiting, need to exit read() to check them
# Otherwise, go back to infinite sleep time, but only if not testing # Otherwise, go back to infinite sleep time, but only if not testing
if len(notified_files) > 0: if len(notified_files) > 0:
timeout = inotify_debounce timeout_ms = inotify_debounce_ms
elif is_testing: elif is_testing:
timeout = self.testing_timeout_ms timeout_ms = self.testing_timeout_ms
else: else:
timeout = None timeout_ms = None
if self.stop_flag.is_set(): if self.stop_flag.is_set():
logger.debug("Finishing because event is set") logger.debug("Finishing because event is set")

View File

@ -4,26 +4,17 @@ import django.db.models.deletion
import multiselectfield.db.fields import multiselectfield.db.fields
from django.conf import settings from django.conf import settings
from django.contrib.auth.management import create_permissions from django.contrib.auth.management import create_permissions
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.db import migrations from django.db import migrations
from django.db import models from django.db import models
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from paperless_mail.models import MailRule
def add_workflow_permissions(apps, schema_editor): def add_workflow_permissions(apps, schema_editor):
app_name = "auth"
User = apps.get_model(app_label=app_name, model_name="User")
Group = apps.get_model(app_label=app_name, model_name="Group")
Permission = apps.get_model(app_label=app_name, model_name="Permission")
# create permissions without waiting for post_migrate signal # create permissions without waiting for post_migrate signal
for app_config in apps.get_app_configs(): for app_config in apps.get_app_configs():
app_config.models_module = True app_config.models_module = True
@ -43,6 +34,10 @@ def add_workflow_permissions(apps, schema_editor):
def remove_workflow_permissions(apps, schema_editor): def remove_workflow_permissions(apps, schema_editor):
app_name = "auth"
User = apps.get_model(app_label=app_name, model_name="User")
Group = apps.get_model(app_label=app_name, model_name="Group")
Permission = apps.get_model(app_label=app_name, model_name="Permission")
workflow_permissions = Permission.objects.filter( workflow_permissions = Permission.objects.filter(
codename__contains="workflow", codename__contains="workflow",
) )
@ -59,15 +54,28 @@ def migrate_consumption_templates(apps, schema_editor):
Migrate consumption templates to workflows. At this point ConsumptionTemplate still exists Migrate consumption templates to workflows. At this point ConsumptionTemplate still exists
but objects are not returned as their true model so we have to manually do that but objects are not returned as their true model so we have to manually do that
""" """
model_name = "ConsumptionTemplate"
app_name = "documents" app_name = "documents"
ConsumptionTemplate = apps.get_model(app_label=app_name, model_name=model_name) ConsumptionTemplate = apps.get_model(
app_label=app_name,
model_name="ConsumptionTemplate",
)
Workflow = apps.get_model(app_label=app_name, model_name="Workflow")
WorkflowAction = apps.get_model(app_label=app_name, model_name="WorkflowAction")
WorkflowTrigger = apps.get_model(app_label=app_name, model_name="WorkflowTrigger")
DocumentType = apps.get_model(app_label=app_name, model_name="DocumentType")
Correspondent = apps.get_model(app_label=app_name, model_name="Correspondent")
StoragePath = apps.get_model(app_label=app_name, model_name="StoragePath")
Tag = apps.get_model(app_label=app_name, model_name="Tag")
CustomField = apps.get_model(app_label=app_name, model_name="CustomField")
MailRule = apps.get_model(app_label="paperless_mail", model_name="MailRule")
User = apps.get_model(app_label="auth", model_name="User")
Group = apps.get_model(app_label="auth", model_name="Group")
with transaction.atomic(): with transaction.atomic():
for template in ConsumptionTemplate.objects.all(): for template in ConsumptionTemplate.objects.all():
trigger = WorkflowTrigger( trigger = WorkflowTrigger(
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION, type=1, # WorkflowTriggerType.CONSUMPTION
sources=template.sources, sources=template.sources,
filter_path=template.filter_path, filter_path=template.filter_path,
filter_filename=template.filter_filename, filter_filename=template.filter_filename,
@ -143,10 +151,13 @@ def migrate_consumption_templates(apps, schema_editor):
def unmigrate_consumption_templates(apps, schema_editor): def unmigrate_consumption_templates(apps, schema_editor):
model_name = "ConsumptionTemplate"
app_name = "documents" app_name = "documents"
ConsumptionTemplate = apps.get_model(app_label=app_name, model_name=model_name) ConsumptionTemplate = apps.get_model(
app_label=app_name,
model_name="ConsumptionTemplate",
)
Workflow = apps.get_model(app_label=app_name, model_name="Workflow")
for workflow in Workflow.objects.all(): for workflow in Workflow.objects.all():
template = ConsumptionTemplate.objects.create( template = ConsumptionTemplate.objects.create(

View File

@ -14,7 +14,7 @@ from rest_framework.authtoken.models import Token
from rest_framework.filters import OrderingFilter from rest_framework.filters import OrderingFilter
from rest_framework.generics import GenericAPIView from rest_framework.generics import GenericAPIView
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import DjangoObjectPermissions from rest_framework.permissions import DjangoModelPermissions
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@ -171,7 +171,7 @@ class ApplicationConfigurationViewSet(ModelViewSet):
queryset = ApplicationConfiguration.objects queryset = ApplicationConfiguration.objects
serializer_class = ApplicationConfigurationSerializer serializer_class = ApplicationConfigurationSerializer
permission_classes = (IsAuthenticated, DjangoObjectPermissions) permission_classes = (IsAuthenticated, DjangoModelPermissions)
class DisconnectSocialAccountView(GenericAPIView): class DisconnectSocialAccountView(GenericAPIView):