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: >
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
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:
name: 'Lock Old Threads'
runs-on: ubuntu-latest
@ -43,14 +43,17 @@ jobs:
This issue has been automatically locked since there
has not been any recent activity after it was closed.
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: >
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
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: >
This discussion has been automatically locked since there
has not been any recent activity after it was closed.
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:
name: 'Close Answered Discussions'
runs-on: ubuntu-latest
@ -90,7 +93,7 @@ jobs:
}`;
const commentVariables = {
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)
@ -180,7 +183,7 @@ jobs:
}`;
const commentVariables = {
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);
@ -258,7 +261,7 @@ jobs:
}`;
const commentVariables = {
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);

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.
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": {
"hashes": [
"sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380",
"sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589",
"sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea",
"sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65",
"sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a",
"sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3",
"sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008",
"sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1",
"sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2",
"sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635",
"sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2",
"sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90",
"sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee",
"sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a",
"sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242",
"sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12",
"sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2",
"sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d",
"sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be",
"sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee",
"sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6",
"sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529",
"sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929",
"sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1",
"sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6",
"sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a",
"sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446",
"sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9",
"sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888",
"sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4",
"sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33",
"sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"
"sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b",
"sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce",
"sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88",
"sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7",
"sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20",
"sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9",
"sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff",
"sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1",
"sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764",
"sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b",
"sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298",
"sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1",
"sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824",
"sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257",
"sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a",
"sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129",
"sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb",
"sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929",
"sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854",
"sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52",
"sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923",
"sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885",
"sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0",
"sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd",
"sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2",
"sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18",
"sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b",
"sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992",
"sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74",
"sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660",
"sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925",
"sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==42.0.2"
"version": "==42.0.4"
},
"dateparser": {
"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="linenumber">96</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">208</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
<context context-type="linenumber">38</context>
@ -2017,7 +2021,7 @@
</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">304</context>
<context context-type="linenumber">320</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.ts</context>
@ -2056,7 +2060,7 @@
</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">306</context>
<context context-type="linenumber">322</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.ts</context>
@ -5025,7 +5029,11 @@
</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">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>
</trans-unit>
<trans-unit id="5382975254277698192" datatype="html">
@ -6219,7 +6227,7 @@
</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">289</context>
<context context-type="linenumber">305</context>
</context-group>
</trans-unit>
<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>
<context-group purpose="location">
<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 purpose="location">
<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 purpose="location">
<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 purpose="location">
<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>
</trans-unit>
<trans-unit id="810888510148304696" datatype="html">
<source>Automatic</source>
<context-group purpose="location">
<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 purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
@ -6332,7 +6340,7 @@
<source>None</source>
<context-group purpose="location">
<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 purpose="location">
<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>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="3928835053823658072" datatype="html">
<source>Error occurred while creating <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="2541368547549828690" datatype="html">
<source>Successfully updated <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="6442673774206210733" datatype="html">
<source>Error occurred while saving <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="6639207128255974941" datatype="html">
<source>Error while deleting element</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="4863024195229581844" datatype="html">
<source>Permissions updated successfully</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="1464476612812630086" datatype="html">
<source>This operation will permanently delete all objects.</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="5897787932098828336" datatype="html">
<source>Objects deleted successfully</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="8273353839648035634" datatype="html">
<source>Error deleting objects</source>
<context-group purpose="location">
<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>
</trans-unit>
<trans-unit id="5101757640976222639" datatype="html">

View File

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

View File

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

View File

@ -309,10 +309,15 @@ describe('SettingsComponent', () => {
component.store.getValue()['displayLanguage'] = 'en-US'
component.store.getValue()['updateCheckingEnabled'] = false
component.settingsForm.value.displayLanguage = 'en-GB'
component.settingsForm.value.updateCheckingEnabled = true
jest.spyOn(settingsService, 'storeSettings').mockReturnValueOnce(of(true))
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.saveSettings()
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', () => {

View File

@ -4,16 +4,16 @@
(click)="isMenuCollapsed = !isMenuCollapsed">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand d-flex col-auto col-md-3 col-lg-2 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 }"
<a class="navbar-brand d-flex align-items-center me-0 px-3 py-3 order-sm-0"
[ngClass]="{ 'slim': slimSidebarEnabled, 'col-auto col-md-3 col-lg-2' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
routerLink="/dashboard"
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
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)" />
</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) {
<div class="d-flex flex-column align-items-start">
<span class="title">{{customAppTitle}}</span>

View File

@ -493,12 +493,17 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
expect(changedResult.getExcludedItems()).toEqual(items)
}))
it('FilterableDropdownSelectionModel should sort items by state', () => {
component.items = items
it('selection model should sort items by state', () => {
component.items = items.concat([{ id: null, name: 'Null B' }])
component.selectionModel = selectionModel
selectionModel.toggle(items[1].id)
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(() => {
@ -542,4 +547,34 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
tick(300)
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.apply()
}

View File

@ -118,4 +118,18 @@ describe('SelectComponent', () => {
tick(3000)
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(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()
processingStatus.phase = FileStatusPhase.WORKING
expect(component.getStatusColor(processingStatus)).toEqual('primary')
processingStatus.phase = FileStatusPhase.UPLOADING
expect(component.getStatusColor(processingStatus)).toEqual('primary')
const failedStatus = new FileStatus()
failedStatus.phase = FileStatusPhase.FAILED
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)">
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<pngx-confirm-button
label="Delete"
i18n-label
(confirm)="deleteObject(object)"
*pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }"
[disabled]="!userCanDelete(object)"
buttonClasses=" btn-sm btn-outline-danger"
iconName="trash">
</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)">
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</td>
</tr>

View File

@ -13,7 +13,6 @@ import {
NgbModalModule,
NgbModalRef,
NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { of, throwError } from 'rxjs'
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 { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
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'
const tags: Tag[] = [
@ -78,7 +76,6 @@ describe('ManagementListComponent', () => {
SafeHtmlPipe,
ConfirmDialogComponent,
PermissionsDialogComponent,
ConfirmButtonComponent,
],
providers: [
{
@ -100,7 +97,6 @@ describe('ManagementListComponent', () => {
NgbModalModule,
RouterTestingModule.withRoutes(routes),
NgxBootstrapIconsModule.pick(allIcons),
NgbPopoverModule,
],
}).compileComponents()
@ -197,23 +193,27 @@ describe('ManagementListComponent', () => {
})
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 deleteSpy = jest.spyOn(tagService, 'delete')
const reloadSpy = jest.spyOn(component, 'reloadData')
const deleteButton = fixture.debugElement.query(
By.directive(ConfirmButtonComponent)
)
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8]
deleteButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as ConfirmDialogComponent
// fail first
deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting')))
deleteButton.nativeElement.dispatchEvent(new Event('confirm'))
editDialog.confirmClicked.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
deleteSpy.mockReturnValueOnce(of(true))
deleteButton.nativeElement.dispatchEvent(new Event('confirm'))
editDialog.confirmClicked.emit()
expect(reloadSpy).toHaveBeenCalled()
})

View File

@ -15,7 +15,10 @@ import {
MATCH_NONE,
} from 'src/app/data/matching-model'
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 {
SortableDirective,
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
.delete(object)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
activeModal.close()
this.reloadData()
},
error: (error) => {
activeModal.componentInstance.buttonsEnabled = true
this.toastService.showError(
$localize`Error while deleting element`,
error
)
},
})
})
}
get nameFilter() {

View File

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

View File

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

View File

@ -58,12 +58,25 @@ describe(`Additional service tests for MailAccountService`, () => {
it('should support patchMany', () => {
subscription = service.patchMany(mail_accounts).subscribe()
mail_accounts.forEach((mail_account) => {
const reqs = httpTestingController.match(
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${mail_account.id}/`
)
expect(reqs).toHaveLength(1)
expect(reqs[0].request.method).toEqual('PATCH')
expect(req.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(() => {

View File

@ -76,12 +76,26 @@ describe(`Additional service tests for MailRuleService`, () => {
it('should support patchMany', () => {
subscription = service.patchMany(mail_rules).subscribe()
mail_rules.forEach((mail_rule) => {
const reqs = httpTestingController.match(
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${mail_rule.id}/`
)
expect(reqs).toHaveLength(1)
expect(reqs[0].request.method).toEqual('PATCH')
expect(req.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(() => {

View File

@ -262,7 +262,7 @@ a.btn-link:focus-visible,
}
.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;
}
@ -439,7 +439,7 @@ ul.pagination {
color: var(--bs-body-color);
&:hover, &:focus {
background-color: var(--pngx-bg-darker);
background-color: var(--pngx-bg-alt);
color: var(--bs-body-color);
}

View File

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

View File

@ -4,26 +4,17 @@ import django.db.models.deletion
import multiselectfield.db.fields
from django.conf import settings
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 models
from django.db import transaction
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):
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
for app_config in apps.get_app_configs():
app_config.models_module = True
@ -43,6 +34,10 @@ def add_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(
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
but objects are not returned as their true model so we have to manually do that
"""
model_name = "ConsumptionTemplate"
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():
for template in ConsumptionTemplate.objects.all():
trigger = WorkflowTrigger(
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
type=1, # WorkflowTriggerType.CONSUMPTION
sources=template.sources,
filter_path=template.filter_path,
filter_filename=template.filter_filename,
@ -143,10 +151,13 @@ def migrate_consumption_templates(apps, schema_editor):
def unmigrate_consumption_templates(apps, schema_editor):
model_name = "ConsumptionTemplate"
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():
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.generics import GenericAPIView
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.response import Response
from rest_framework.views import APIView
@ -171,7 +171,7 @@ class ApplicationConfigurationViewSet(ModelViewSet):
queryset = ApplicationConfiguration.objects
serializer_class = ApplicationConfigurationSerializer
permission_classes = (IsAuthenticated, DjangoObjectPermissions)
permission_classes = (IsAuthenticated, DjangoModelPermissions)
class DisconnectSocialAccountView(GenericAPIView):