Compare commits

...

209 Commits

Author SHA1 Message Date
shamoon
3e129763c7 v1.14.5 2023-05-15 08:08:59 -07:00
shamoon
c49d086965 Merge branch 'dev' 2023-05-15 08:08:17 -07:00
shamoon
df7bfc4efd Merge pull request #3352 from paperless-ngx/l10n_dev
New Crowdin updates
2023-05-15 08:07:49 -07:00
Paperless-ngx Bot [bot]
7fba1f9ed2 New translations messages.xlf (German)
[ci skip]
2023-05-13 17:16:06 -07:00
Trenton Holmes
3205d52331 Changes the error mode to replace instead of ignore, to better highlight where a problem happened 2023-05-13 09:29:18 -07:00
Trenton H
111960c530 Adds better handling for files with invalid utf8 content 2023-05-13 09:29:18 -07:00
Paperless-ngx Bot [bot]
e1bc1a0129 New translations django.po (Polish)
[ci skip]
2023-05-13 04:51:23 -07:00
Paperless-ngx Bot [bot]
8b543a5fa9 New translations messages.xlf (Polish)
[ci skip]
2023-05-13 04:51:22 -07:00
Paperless-ngx Bot [bot]
dc7a67a1d7 New translations django.po (Polish)
[ci skip]
2023-05-13 03:37:55 -07:00
shamoon
350c20d6ab Merge pull request #3359 from paperless-ngx/feature-fix-autocomplete-respect-perms
Fix: respect permissions for autocomplete suggestions
2023-05-12 13:35:45 -07:00
Trenton H
b5f0cd7c70 Adds back the extras from uvicorn 2023-05-12 06:43:32 -07:00
Trenton H
90488cd77a Removes even more remanents from the lock file 2023-05-12 06:43:32 -07:00
Trenton H
5bbc59e87c Fixes testing requirements, removes further leftover libraries 2023-05-12 06:43:32 -07:00
Trenton H
c02758213b Upgrades to the latest django channels 2023-05-12 06:43:32 -07:00
Paperless-ngx Bot [bot]
09c62d67c1 New translations messages.xlf (French)
[ci skip]
2023-05-12 02:27:06 -07:00
Paperless-ngx Bot [bot]
3f3fa3044c New translations messages.xlf (French)
[ci skip]
2023-05-12 01:19:22 -07:00
Paperless-ngx Bot [bot]
62673145fb New translations messages.xlf (Catalan)
[ci skip]
2023-05-11 22:47:01 -07:00
shamoon
0baf73de5e Update some version strings 2023-05-11 15:06:17 -07:00
shamoon
66a0783e7b Respect permissions for autocomplete suggestions 2023-05-11 14:43:25 -07:00
Trenton H
17144c45e5 Transition to new library for finding IPs from the Django request 2023-05-11 13:51:04 -07:00
shamoon
311c0ba4f1 Resolve CodeQL warnings 2023-05-11 12:56:01 -07:00
shamoon
e293d23ae3 Refactoring a few frontend components 2023-05-11 12:49:33 -07:00
Paperless-ngx Bot [bot]
93769d2608 New translations messages.xlf (German)
[ci skip]
2023-05-11 11:12:58 -07:00
Paperless-ngx Bot [bot]
e7540563d0 New translations messages.xlf (Romanian)
[ci skip]
2023-05-11 10:13:18 -07:00
Paperless-ngx Bot [bot]
fc1047550e New translations messages.xlf (Hungarian)
[ci skip]
2023-05-11 10:13:17 -07:00
Paperless-ngx Bot [bot]
dadc618719 New translations messages.xlf (Serbian (Latin))
[ci skip]
2023-05-11 10:13:15 -07:00
Paperless-ngx Bot [bot]
36f3bd2869 New translations messages.xlf (Luxembourgish)
[ci skip]
2023-05-11 10:13:14 -07:00
Paperless-ngx Bot [bot]
fdcea983a4 New translations messages.xlf (Croatian)
[ci skip]
2023-05-11 10:13:13 -07:00
Paperless-ngx Bot [bot]
081534457c New translations messages.xlf (Indonesian)
[ci skip]
2023-05-11 10:13:11 -07:00
Paperless-ngx Bot [bot]
94a6272a1d New translations messages.xlf (Portuguese, Brazilian)
[ci skip]
2023-05-11 10:13:10 -07:00
Paperless-ngx Bot [bot]
d389e0ecf8 New translations messages.xlf (Chinese Simplified)
[ci skip]
2023-05-11 10:13:09 -07:00
Paperless-ngx Bot [bot]
17eb1c604f New translations messages.xlf (Turkish)
[ci skip]
2023-05-11 10:13:08 -07:00
Paperless-ngx Bot [bot]
99474aab06 New translations messages.xlf (Swedish)
[ci skip]
2023-05-11 10:13:07 -07:00
Paperless-ngx Bot [bot]
f3d3bf20de New translations messages.xlf (Slovenian)
[ci skip]
2023-05-11 10:13:05 -07:00
Paperless-ngx Bot [bot]
3c999e9847 New translations messages.xlf (Russian)
[ci skip]
2023-05-11 10:13:04 -07:00
Paperless-ngx Bot [bot]
692fa5f606 New translations messages.xlf (Portuguese)
[ci skip]
2023-05-11 10:13:03 -07:00
Paperless-ngx Bot [bot]
752b8e79ff New translations messages.xlf (Polish)
[ci skip]
2023-05-11 10:13:02 -07:00
Paperless-ngx Bot [bot]
3f82cf4ab3 New translations messages.xlf (Norwegian)
[ci skip]
2023-05-11 10:13:00 -07:00
Paperless-ngx Bot [bot]
1549b9df74 New translations messages.xlf (Dutch)
[ci skip]
2023-05-11 10:12:59 -07:00
Paperless-ngx Bot [bot]
78ef87a952 New translations messages.xlf (Italian)
[ci skip]
2023-05-11 10:12:58 -07:00
Paperless-ngx Bot [bot]
29ede48e0f New translations messages.xlf (Hebrew)
[ci skip]
2023-05-11 10:12:57 -07:00
Paperless-ngx Bot [bot]
6349d25219 New translations messages.xlf (Finnish)
[ci skip]
2023-05-11 10:12:55 -07:00
Paperless-ngx Bot [bot]
830a450f00 New translations messages.xlf (German)
[ci skip]
2023-05-11 10:12:54 -07:00
Paperless-ngx Bot [bot]
18f9ce9c0b New translations messages.xlf (Danish)
[ci skip]
2023-05-11 10:12:53 -07:00
Paperless-ngx Bot [bot]
2471be0c78 New translations messages.xlf (Czech)
[ci skip]
2023-05-11 10:12:51 -07:00
Paperless-ngx Bot [bot]
60cfd687dc New translations messages.xlf (Catalan)
[ci skip]
2023-05-11 10:12:50 -07:00
Paperless-ngx Bot [bot]
e06c61b95d New translations messages.xlf (Belarusian)
[ci skip]
2023-05-11 10:12:49 -07:00
Paperless-ngx Bot [bot]
471eee0872 New translations messages.xlf (Arabic)
[ci skip]
2023-05-11 10:12:48 -07:00
Paperless-ngx Bot [bot]
20abd8a9f8 New translations messages.xlf (Spanish)
[ci skip]
2023-05-11 10:12:46 -07:00
Paperless-ngx Bot [bot]
88e5c471de New translations messages.xlf (French)
[ci skip]
2023-05-11 10:12:45 -07:00
shamoon
09086e574d Merge pull request #3309 from paperless-ngx/feature-owner-filtering
Feature: owner filtering
2023-05-11 10:05:51 -07:00
Paperless-ngx Bot [bot]
8d95c13e31 New translations messages.xlf (Portuguese, Brazilian)
[ci skip]
2023-05-10 20:17:24 -07:00
Paperless-ngx Bot [bot]
c922cc4351 New translations django.po (Portuguese, Brazilian)
[ci skip]
2023-05-10 19:17:32 -07:00
shamoon
a42f28c502 Merge pull request #3366 from paperless-ngx/fix/huntr-94517f3f-ed86-4d88-bce1-6e9ba11fe1c2
[Security] Render frontend text as plain text
2023-05-10 11:16:24 -07:00
shamoon
b802f3a71f Merge pull request #3329 from paperless-ngx/feature-full-dynamic-counts
Enhancement: dynamic counts include all pages, hide for "Any"
2023-05-10 11:15:47 -07:00
shamoon
f78f212a77 Merge pull request #3347 from paperless-ngx/fix/issue-3346
Fix: default frontend to current owner, allow setting no owner on create
2023-05-10 08:18:08 -07:00
Trenton H
22cbfd473b Upgrades dependencies to their latest allowed versions 2023-05-10 06:59:44 -07:00
shamoon
e5973ef713 Merge pull request #3367 from denilsonsa/patch-2
[Fix] Position:fixed for .global-dropzone-overlay
2023-05-09 23:48:02 -07:00
Denilson Sá Maia
5364a29b5f Position:fixed for .global-dropzone-overlay
If the user tried dropping a file onto the paperless-ngx UI, but the page itself had scrolled down a bit, the overlay would have scrolled together with the page.

This commit makes the overlay fixed to the viewport, independent from the scroll position.

This one-word commit was done directly through the GitHub web interface.
2023-05-10 08:01:51 +02:00
shamoon
49754d33fa Render frontend html as plain text 2023-05-09 21:59:24 -07:00
shamoon
d7d95037be Update document-detail.component.ts 2023-05-09 21:48:31 -07:00
shamoon
515146d4a2 Default frontend to current owner, allow setting no owner on create 2023-05-09 19:53:34 -07:00
shamoon
b7540fab58 Apply code suggestions
Co-Authored-By: Trenton H <797416+stumpylog@users.noreply.github.com>
2023-05-09 19:48:19 -07:00
shamoon
88e6f8abf6 Update frontend strings 2023-05-09 19:48:04 -07:00
shamoon
3c4dadd905 Re-work filter editor, bulk editor & reset buttons 2023-05-09 19:48:04 -07:00
Paperless-ngx Bot [bot]
f18f997796 New translations messages.xlf (Hungarian)
[ci skip]
2023-05-09 16:56:48 -07:00
Paperless-ngx Bot [bot]
3a1daf46ae New translations django.po (Hungarian)
[ci skip]
2023-05-09 16:56:46 -07:00
Paperless-ngx Bot [bot]
8dffea4a42 New translations messages.xlf (French)
[ci skip]
2023-05-09 07:15:58 -07:00
Paperless-ngx Bot [bot]
3852a6c5cf New translations django.po (Slovenian)
[ci skip]
2023-05-09 05:47:20 -07:00
Paperless-ngx Bot [bot]
6493f51a29 New translations messages.xlf (Slovenian)
[ci skip]
2023-05-09 05:47:19 -07:00
Paperless-ngx Bot [bot]
028f42e775 New translations django.po (Slovenian)
[ci skip]
2023-05-09 03:37:38 -07:00
Paperless-ngx Bot [bot]
eb1cc55f94 New translations messages.xlf (German)
[ci skip]
2023-05-09 02:31:20 -07:00
Paperless-ngx Bot [bot]
fb864f1132 New translations messages.xlf (Slovenian)
[ci skip]
2023-05-08 23:51:59 -07:00
Paperless-ngx Bot [bot]
8b8d988c07 New translations messages.xlf (Catalan)
[ci skip]
2023-05-08 23:51:58 -07:00
shamoon
c2b5451fe4 Add frontend owner filtering
Add owner to doc cards, table
Frontend testing for owner filtering
2023-05-08 15:34:14 -07:00
shamoon
487d3a6262 Support owner API query vars 2023-05-08 15:34:14 -07:00
shamoon
fe990b4cd2 Merge pull request #3336 from paperless-ngx/fix/issue-3332
Fix: dont perform mail actions when rule filename filter not met
2023-05-08 14:26:17 -07:00
Paperless-ngx Bot [bot]
019c7e2f78 New translations messages.xlf (Serbian (Latin))
[ci skip]
2023-05-08 08:04:35 -07:00
Paperless-ngx Bot [bot]
1c64a4f145 New translations messages.xlf (Luxembourgish)
[ci skip]
2023-05-08 08:04:33 -07:00
Paperless-ngx Bot [bot]
fc869aa203 New translations messages.xlf (Croatian)
[ci skip]
2023-05-08 08:04:32 -07:00
Paperless-ngx Bot [bot]
3a0ada9f46 New translations messages.xlf (Indonesian)
[ci skip]
2023-05-08 08:04:31 -07:00
Paperless-ngx Bot [bot]
cc9980fc19 New translations messages.xlf (Portuguese, Brazilian)
[ci skip]
2023-05-08 08:04:30 -07:00
Paperless-ngx Bot [bot]
7515d8af64 New translations messages.xlf (Chinese Simplified)
[ci skip]
2023-05-08 08:04:28 -07:00
Paperless-ngx Bot [bot]
5e7579c1fd New translations messages.xlf (Turkish)
[ci skip]
2023-05-08 08:04:27 -07:00
Paperless-ngx Bot [bot]
38af53f281 New translations messages.xlf (Swedish)
[ci skip]
2023-05-08 08:04:25 -07:00
Paperless-ngx Bot [bot]
a26bec5b00 New translations messages.xlf (Slovenian)
[ci skip]
2023-05-08 08:04:24 -07:00
Paperless-ngx Bot [bot]
feb943b6df New translations messages.xlf (Russian)
[ci skip]
2023-05-08 08:04:23 -07:00
Paperless-ngx Bot [bot]
059e37a41f New translations messages.xlf (Portuguese)
[ci skip]
2023-05-08 08:04:22 -07:00
Paperless-ngx Bot [bot]
7ce67fd465 New translations messages.xlf (Polish)
[ci skip]
2023-05-08 08:04:20 -07:00
Paperless-ngx Bot [bot]
6b8b8209f3 New translations messages.xlf (Norwegian)
[ci skip]
2023-05-08 08:04:19 -07:00
Paperless-ngx Bot [bot]
fd1d12859d New translations messages.xlf (Dutch)
[ci skip]
2023-05-08 08:04:18 -07:00
Paperless-ngx Bot [bot]
efb00b2387 New translations messages.xlf (Italian)
[ci skip]
2023-05-08 08:04:17 -07:00
Paperless-ngx Bot [bot]
9b2ca57038 New translations messages.xlf (Hebrew)
[ci skip]
2023-05-08 08:04:15 -07:00
Paperless-ngx Bot [bot]
9694face16 New translations messages.xlf (Finnish)
[ci skip]
2023-05-08 08:04:14 -07:00
Paperless-ngx Bot [bot]
7ef14832d0 New translations messages.xlf (German)
[ci skip]
2023-05-08 08:04:13 -07:00
Paperless-ngx Bot [bot]
33f7b58e6e New translations messages.xlf (Danish)
[ci skip]
2023-05-08 08:04:11 -07:00
Paperless-ngx Bot [bot]
9e992da863 New translations messages.xlf (Czech)
[ci skip]
2023-05-08 08:04:10 -07:00
Paperless-ngx Bot [bot]
c986a218c7 New translations messages.xlf (Catalan)
[ci skip]
2023-05-08 08:04:09 -07:00
Paperless-ngx Bot [bot]
8ee6312402 New translations messages.xlf (Belarusian)
[ci skip]
2023-05-08 08:04:08 -07:00
Paperless-ngx Bot [bot]
3c86b12ef9 New translations messages.xlf (Arabic)
[ci skip]
2023-05-08 08:04:07 -07:00
Paperless-ngx Bot [bot]
02d09edd49 New translations messages.xlf (Spanish)
[ci skip]
2023-05-08 08:04:05 -07:00
Paperless-ngx Bot [bot]
55af3c3dd1 New translations messages.xlf (French)
[ci skip]
2023-05-08 08:04:04 -07:00
shamoon
f8f5a77744 Merge pull request #3321 from paperless-ngx/feature-dismissable-welcome-widget 2023-05-08 07:06:11 -07:00
shamoon
5b6956ff24 Merge pull request #3345 from paperless-ngx/fix/issue-3341 2023-05-08 07:04:49 -07:00
shamoon
f1c138eaed Merge pull request #3315 from paperless-ngx/fix/__in-search-testing 2023-05-08 07:03:59 -07:00
Ross Brown
caf43638de Bump django from 4.1.7 to 4.1.9 2023-05-07 18:22:52 -07:00
shamoon
b783d2e210 Fix PassUserMixin not properly being used in DocumentViewSet 2023-05-07 17:40:09 -07:00
shamoon
9a40a5f019 Add proper testing for *__id__in testing 2023-05-07 00:04:23 -07:00
shamoon
81a7b34101 Dont perform mail actions when rule filename filter not met
Update mail.py
2023-05-06 23:59:33 -07:00
shamoon
f124e2a889 Add "all" property to results 2023-05-06 11:31:47 -07:00
Trenton H
02b2bcafc5 Fixes a small step naming thing, updates codecov to only upload on pull request events 2023-05-05 11:47:43 -07:00
Trenton Holmes
81a5fd377e Use a tagged version of the image cleaner action 2023-05-05 11:47:43 -07:00
Trenton H
f875ae4abf CI cleanup and improvements.
Removes the building of installers from the repo, they can now be built elsewhere,
on demand, as their building is no longer tied to the Dockerfile
2023-05-05 11:47:43 -07:00
Trenton H
01fd400ec7 Moves to the new action for cleaning the published images 2023-05-05 11:47:43 -07:00
shamoon
c59420581c Dynamic counts include all pages, hide for "Any" 2023-05-05 01:01:57 -07:00
shamoon
0aa9462cea Save tour completion, hide welcome widget 2023-05-04 23:29:20 -07:00
shamoon
bf2f6f84e5 Merge pull request #3314 from paperless-ngx/v1.14.4-changelog 2023-05-04 10:03:26 -07:00
github-actions
5126f01b57 Changelog v1.14.4 - GHA 2023-05-04 15:41:23 +00:00
Trenton H
ec4814a76e Bumps version to 1.14.4 2023-05-04 07:48:55 -07:00
Trenton H
69b53d70c5 Merge remote-tracking branch 'origin/dev' 2023-05-04 07:48:00 -07:00
Paperless-ngx Bot [bot]
02875f5a34 New Crowdin updates (#3298)
* New translations messages.xlf (Spanish)
[ci skip]

* New translations messages.xlf (Dutch)
[ci skip]

* New translations messages.xlf (Portuguese, Brazilian)
[ci skip]

* New translations django.po (Portuguese, Brazilian)
[ci skip]

* New translations messages.xlf (German)
[ci skip]

* New translations messages.xlf (Finnish)
[ci skip]

* New translations messages.xlf (Catalan)
[ci skip]
2023-05-04 07:45:48 -07:00
Trenton H
29d8c4e08d Fixes inversion in tagged mail searching 2023-05-04 06:29:41 -07:00
shamoon
df203311fe Fix note sorting, testing, bump search index version 2023-05-04 02:07:48 -07:00
shamoon
10f9b91c44 fix __in filtering 2023-05-04 02:07:16 -07:00
shamoon
cd861364a2 Merge pull request #3303 from paperless-ngx/fix/discussion-3300
Fix dynamic count labels hidden in light mode
2023-05-03 15:14:05 -07:00
shamoon
90b52abc04 Fix dynamic count labels hidden in light mode 2023-05-03 13:22:16 -07:00
github-actions
093b726c52 Changelog v1.14.3 - GHA 2023-05-03 09:29:40 -07:00
Trenton Holmes
fd84fc9dbe Re-add -dev version tag 2023-05-03 06:55:59 -07:00
Trenton Holmes
4353646b3a Bumps version to 1.14.3 2023-05-03 06:54:37 -07:00
Trenton Holmes
7545e5312c Merge remote-tracking branch 'origin/dev' 2023-05-03 06:53:33 -07:00
Paperless-ngx Bot [bot]
bd494ce9ec New Crowdin updates (#3226)
* New translations django.po (Indonesian)
[ci skip]

* New translations django.po (Indonesian)
[ci skip]

* New translations messages.xlf (Indonesian)
[ci skip]

* New translations django.po (Indonesian)
[ci skip]

* New translations messages.xlf (German)
[ci skip]

* New translations messages.xlf (Serbian (Latin))
[ci skip]

* New translations messages.xlf (Indonesian)
[ci skip]

* New translations messages.xlf (Indonesian)
[ci skip]

* New translations messages.xlf (Catalan)
[ci skip]

* New translations messages.xlf (Indonesian)
[ci skip]

* New translations django.po (Arabic)
[ci skip]

* New translations messages.xlf (Arabic)
[ci skip]

* New translations django.po (Arabic)
[ci skip]

* New translations messages.xlf (Portuguese, Brazilian)
[ci skip]

* New translations django.po (Portuguese, Brazilian)
[ci skip]

* New translations messages.xlf (Indonesian)
[ci skip]

* New translations messages.xlf (Polish)
[ci skip]
2023-05-03 06:45:59 -07:00
Ross Brown
ee3cf8e6d1 Bump filelock from 3.10.2 to 3.12.0 to fix permissions bug 2023-05-02 07:32:41 -07:00
dependabot[bot]
df524fdc1f Merge pull request #3276 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/eslint-8.39.0 2023-05-01 22:38:59 +00:00
dependabot[bot]
f0c0cfee1d Bump eslint from 8.38.0 to 8.39.0 in /src-ui
Bumps [eslint](https://github.com/eslint/eslint) from 8.38.0 to 8.39.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.38.0...v8.39.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 22:28:37 +00:00
dependabot[bot]
cf1bf3c163 Merge pull request #3278 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/typescript-eslint/parser-5.59.2 2023-05-01 22:17:43 +00:00
dependabot[bot]
b9d703fe25 Bump @typescript-eslint/parser from 5.58.0 to 5.59.2 in /src-ui
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.58.0 to 5.59.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.59.2/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 22:07:13 +00:00
shamoon
46f7e685b6 Merge pull request #3275 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/types/node-18.16.3
Bump @types/node from 18.15.11 to 18.16.3 in /src-ui
2023-05-01 15:03:58 -07:00
shamoon
8023331fca Merge pull request #3277 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/rxjs-7.8.1
Bump rxjs from 7.8.0 to 7.8.1 in /src-ui
2023-05-01 15:03:43 -07:00
dependabot[bot]
597db7d4bd Bump @types/node from 18.15.11 to 18.16.3 in /src-ui
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.15.11 to 18.16.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 21:48:24 +00:00
dependabot[bot]
773bd32cd0 Merge pull request #3274 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/typescript-eslint/eslint-plugin-5.59.2 2023-05-01 21:47:45 +00:00
dependabot[bot]
c7e3756de1 Merge pull request #3268 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/cypress-12.11.0 2023-05-01 21:45:12 +00:00
dependabot[bot]
64bf122c95 Bump rxjs from 7.8.0 to 7.8.1 in /src-ui
Bumps [rxjs](https://github.com/reactivex/rxjs) from 7.8.0 to 7.8.1.
- [Release notes](https://github.com/reactivex/rxjs/releases)
- [Changelog](https://github.com/ReactiveX/rxjs/blob/7.8.1/CHANGELOG.md)
- [Commits](https://github.com/reactivex/rxjs/compare/7.8.0...7.8.1)

---
updated-dependencies:
- dependency-name: rxjs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 21:36:08 +00:00
dependabot[bot]
a92b0411fd Bump @typescript-eslint/eslint-plugin from 5.58.0 to 5.59.2 in /src-ui
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.58.0 to 5.59.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.59.2/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 21:35:22 +00:00
dependabot[bot]
728d61762a Bump cypress from 12.9.0 to 12.11.0 in /src-ui
Bumps [cypress](https://github.com/cypress-io/cypress) from 12.9.0 to 12.11.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v12.9.0...v12.11.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 21:34:33 +00:00
shamoon
d9783e2a4d Merge pull request #3270 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/angular/cli-15.2.7
Bulk bump angular packages to 15.2.8 in /src-ui
2023-05-01 14:33:41 -07:00
shamoon
b6303d2c16 Bulk bump angular packages to 15.2.8 2023-05-01 14:24:06 -07:00
dependabot[bot]
dd673a62b5 Bump @angular/cli from 15.2.6 to 15.2.7 in /src-ui
Bumps [@angular/cli](https://github.com/angular/angular-cli) from 15.2.6 to 15.2.7.
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/15.2.6...15.2.7)

---
updated-dependencies:
- dependency-name: "@angular/cli"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 20:58:46 +00:00
Trenton Holmes
b7577038a0 Replace usages of os.rename with shutil.move to properly handle cases where the source and dest arent't on the same filesystem 2023-05-01 07:28:52 -07:00
Trenton Holmes
613b71d23b Ignores a specific _FILE setting which doesn't actually get set to a file 2023-05-01 07:23:31 -07:00
shamoon
0284100c2d Merge pull request #3243 from paperless-ngx/fix/issue-3229 2023-04-29 12:37:12 -07:00
Trenton H
26cd470d31 Don't ever send GMail related keywords if the server doesn't report support for the extensions 2023-04-29 09:34:50 -07:00
shamoon
ebaf509a42 Retain doc changes on tab switch after refresh doc 2023-04-29 00:23:30 -07:00
shamoon
646db73061 Update document-notes.component.ts 2023-04-28 21:56:40 -07:00
shamoon
e6df581909 Merge pull request #3232 from paperless-ngx/fix/issue-3231
Fix: close all docs on logout
2023-04-28 20:51:47 -07:00
shamoon
a8e12409b5 Merge pull request #3227 from paperless-ngx/feature/better-keyboard-dropdowns
Enhancement: better keyboard nav for filter/edit dropdowns
2023-04-28 20:51:38 -07:00
shamoon
b7c7e293f7 Doc detail tab switch fixes 2023-04-28 08:14:24 -07:00
shamoon
fe85aff052 Merge pull request #3222 from paperless-ngx/fix/advanced-queries-perms-fixes
Fix: Respect superuser for advanced queries, test coverage for object perms
2023-04-28 07:11:37 -07:00
shamoon
12d8bcad6e Close all docs on logout 2023-04-28 07:07:59 -07:00
shamoon
bbfc244f16 Better keyboard nav for filter/edit dropdowns 2023-04-27 23:54:43 -07:00
shamoon
e275a2736a Respect superuser for advanced queries, test coverage for object perms 2023-04-27 15:51:34 -07:00
shamoon
d2a8076596 Merge pull request #3218 from ikaruswill/fix/issue-3214
Fix: ALLOWED_HOSTS logic being overwritten when * is set
2023-04-27 13:24:35 -07:00
Will Ho
83344f748f Fix appends to ALLOWED_HOSTS should be string instead of list 2023-04-28 03:28:19 +08:00
shamoon
1d5dbc454d Update version string for dev 2023-04-27 11:43:59 -07:00
shamoon
16e2dc60aa Merge pull request #3219 from paperless-ngx/v1.14.2-changelog
[Documentation] Add v1.14.2 changelog
2023-04-27 11:26:32 -07:00
github-actions
a6fd4a8472 Changelog v1.14.2 - GHA 2023-04-27 18:19:05 +00:00
Will Ho
c25698dfa7 Update docs to reflect localhost being always included in ALLOWED_HOSTS 2023-04-28 02:09:26 +08:00
Will Ho
2ab2064a72 Fix ALLOWED_HOSTS logic being overwritten when * is set 2023-04-28 02:08:55 +08:00
shamoon
356c26ce84 v1.14.2 2023-04-27 10:57:03 -07:00
shamoon
bc56dfbcb5 Merge branch 'dev' 2023-04-27 10:47:43 -07:00
shamoon
daaeb36363 Merge pull request #3207 from paperless-ngx/l10n_dev
New Crowdin updates
2023-04-27 10:47:21 -07:00
Paperless-ngx Bot [bot]
a46a9cf0bf New translations messages.xlf (Luxembourgish)
[ci skip]
2023-04-27 10:46:14 -07:00
Paperless-ngx Bot [bot]
cbf435169a New translations messages.xlf (Croatian)
[ci skip]
2023-04-27 10:46:12 -07:00
Paperless-ngx Bot [bot]
7d05f6c54a New translations messages.xlf (Indonesian)
[ci skip]
2023-04-27 10:46:11 -07:00
Paperless-ngx Bot [bot]
8a8667d1f4 New translations messages.xlf (Portuguese, Brazilian)
[ci skip]
2023-04-27 10:46:10 -07:00
Paperless-ngx Bot [bot]
b9b8b764db New translations messages.xlf (Chinese Simplified)
[ci skip]
2023-04-27 10:46:09 -07:00
Paperless-ngx Bot [bot]
4978af351d New translations messages.xlf (Turkish)
[ci skip]
2023-04-27 10:46:07 -07:00
Paperless-ngx Bot [bot]
b5d639652d New translations messages.xlf (Swedish)
[ci skip]
2023-04-27 10:46:06 -07:00
Paperless-ngx Bot [bot]
c4ebfaf7f6 New translations messages.xlf (Slovenian)
[ci skip]
2023-04-27 10:46:05 -07:00
Paperless-ngx Bot [bot]
256266280d New translations messages.xlf (Russian)
[ci skip]
2023-04-27 10:46:03 -07:00
Paperless-ngx Bot [bot]
f886b58529 New translations messages.xlf (Portuguese)
[ci skip]
2023-04-27 10:46:02 -07:00
Paperless-ngx Bot [bot]
14fe93b9ab New translations messages.xlf (Polish)
[ci skip]
2023-04-27 10:46:01 -07:00
Paperless-ngx Bot [bot]
a2fb0ceb7d New translations messages.xlf (Norwegian)
[ci skip]
2023-04-27 10:46:00 -07:00
Paperless-ngx Bot [bot]
bc284ecf6d New translations messages.xlf (Dutch)
[ci skip]
2023-04-27 10:45:58 -07:00
Paperless-ngx Bot [bot]
6113f586c9 New translations messages.xlf (Italian)
[ci skip]
2023-04-27 10:45:57 -07:00
Paperless-ngx Bot [bot]
6c0862248c New translations messages.xlf (Hebrew)
[ci skip]
2023-04-27 10:45:56 -07:00
Paperless-ngx Bot [bot]
0e8f2a7c6c New translations messages.xlf (Danish)
[ci skip]
2023-04-27 10:45:55 -07:00
Paperless-ngx Bot [bot]
a808f8bbd5 New translations messages.xlf (Czech)
[ci skip]
2023-04-27 10:45:54 -07:00
Paperless-ngx Bot [bot]
9428d5638e New translations messages.xlf (Belarusian)
[ci skip]
2023-04-27 10:45:52 -07:00
Paperless-ngx Bot [bot]
e00cd5e304 New translations messages.xlf (Spanish)
[ci skip]
2023-04-27 10:45:51 -07:00
Paperless-ngx Bot [bot]
3c04bf2742 New translations messages.xlf (Romanian)
[ci skip]
2023-04-27 10:45:50 -07:00
Paperless-ngx Bot [bot]
2ef6d450bc New translations messages.xlf (Arabic)
[ci skip]
2023-04-27 10:45:34 -07:00
Paperless-ngx Bot [bot]
628b0bffeb New translations messages.xlf (Finnish)
[ci skip]
2023-04-27 10:45:33 -07:00
Paperless-ngx Bot [bot]
27eaa566a5 New translations messages.xlf (German)
[ci skip]
2023-04-27 10:45:32 -07:00
Paperless-ngx Bot [bot]
fb36646bd3 New translations messages.xlf (Serbian (Latin))
[ci skip]
2023-04-27 10:45:30 -07:00
Paperless-ngx Bot [bot]
304cc37618 New translations messages.xlf (Catalan)
[ci skip]
2023-04-27 10:45:28 -07:00
Paperless-ngx Bot [bot]
8239e8a581 New translations messages.xlf (French)
[ci skip]
2023-04-27 10:45:27 -07:00
shamoon
69e117d898 Merge pull request #3215 from paperless-ngx/feature-finnish-translation
Feature: Finnish translation
2023-04-27 10:44:44 -07:00
Paperless-ngx Bot [bot]
c773ec8a30 New translations messages.xlf (Arabic)
[ci skip]
2023-04-27 10:16:09 -07:00
shamoon
b4b49ee096 Add Finnish translation 2023-04-27 10:12:35 -07:00
shamoon
cf5ab87db9 Merge pull request #3211 from paperless-ngx/fix/issue-3210
Fix: Load saved views from app frame, not dashboard
2023-04-27 10:09:56 -07:00
shamoon
deaff293d2 Merge pull request #3209 from paperless-ngx/fix/issue-3206
Fix: advanced search or date searching + doc type/correspondent/storage path broken
2023-04-27 10:09:20 -07:00
shamoon
dccdebd2c0 Merge pull request #3212 from e1mo/fix-MixedContentTypeError
Fix MixedContentTypeError in add_inbox_tags handler
2023-04-27 09:57:46 -07:00
Moritz 'e1mo' Fromm
2674d4f034 Fix MixedContentTypeError in add_inbox_tags handler
The fact that Tags were fetched while the `view_documenttype` permission
was validated caused a MixedContentTypeError, thus the document
consumptio to fail because the list of available tags could not be
fetched.
2023-04-27 18:15:05 +02:00
github-actions
cb529561e1 Changelog v1.14.1 - GHA 2023-04-27 09:10:15 -07:00
shamoon
1a1cf49c67 Testing for whoosh support for multi-object query vars 2023-04-27 08:47:36 -07:00
shamoon
757b61a010 Load saved views from app frame, not dashboard 2023-04-27 08:20:21 -07:00
shamoon
448dcbab46 Include multi object queries in whoosh searcher 2023-04-27 08:06:55 -07:00
Paperless-ngx Bot [bot]
30fc5bbb09 New translations messages.xlf (Arabic)
[ci skip]
2023-04-27 07:18:09 -07:00
Trenton H
d3e14818df Reset dev versioning string 2023-04-27 07:16:20 -07:00
139 changed files with 17232 additions and 8040 deletions

View File

@@ -1,9 +0,0 @@
{
"qpdf": {
"version": "11.3.0"
},
"jbig2enc": {
"version": "0.29",
"git_tag": "0.29"
}
}

View File

@@ -1,485 +0,0 @@
import json
import logging
import os
import shutil
import subprocess
from argparse import ArgumentParser
from typing import Dict
from typing import Final
from typing import Iterator
from typing import List
from typing import Optional
from common import get_log_level
from github import ContainerPackage
from github import GithubBranchApi
from github import GithubContainerRegistryApi
logger = logging.getLogger("cleanup-tags")
class ImageProperties:
"""
Data class wrapping the properties of an entry in the image index
manifests list. It is NOT an actual image with layers, etc
https://docs.docker.com/registry/spec/manifest-v2-2/
https://github.com/opencontainers/image-spec/blob/main/manifest.md
https://github.com/opencontainers/image-spec/blob/main/descriptor.md
"""
def __init__(self, data: Dict) -> None:
self._data = data
# This is the sha256: digest string. Corresponds to GitHub API name
# if the package is an untagged package
self.digest = self._data["digest"]
platform_data_os = self._data["platform"]["os"]
platform_arch = self._data["platform"]["architecture"]
platform_variant = self._data["platform"].get(
"variant",
"",
)
self.platform = f"{platform_data_os}/{platform_arch}{platform_variant}"
class ImageIndex:
"""
Data class wrapping up logic for an OCI Image Index
JSON data. Primary use is to access the manifests listing
See https://github.com/opencontainers/image-spec/blob/main/image-index.md
"""
def __init__(self, package_url: str, tag: str) -> None:
self.qualified_name = f"{package_url}:{tag}"
logger.info(f"Getting image index for {self.qualified_name}")
try:
proc = subprocess.run(
[
shutil.which("docker"),
"buildx",
"imagetools",
"inspect",
"--raw",
self.qualified_name,
],
capture_output=True,
check=True,
)
self._data = json.loads(proc.stdout)
except subprocess.CalledProcessError as e:
logger.error(
f"Failed to get image index for {self.qualified_name}: {e.stderr}",
)
raise e
@property
def image_pointers(self) -> Iterator[ImageProperties]:
for manifest_data in self._data["manifests"]:
yield ImageProperties(manifest_data)
class RegistryTagsCleaner:
"""
This is the base class for the image registry cleaning. Given a package
name, it will keep all images which are tagged and all untagged images
referred to by a manifest. This results in only images which have been untagged
and cannot be referenced except by their SHA in being removed. None of these
images should be referenced, so it is fine to delete them.
"""
def __init__(
self,
package_name: str,
repo_owner: str,
repo_name: str,
package_api: GithubContainerRegistryApi,
branch_api: Optional[GithubBranchApi],
):
self.actually_delete = False
self.package_api = package_api
self.branch_api = branch_api
self.package_name = package_name
self.repo_owner = repo_owner
self.repo_name = repo_name
self.tags_to_delete: List[str] = []
self.tags_to_keep: List[str] = []
# Get the information about all versions of the given package
# These are active, not deleted, the default returned from the API
self.all_package_versions = self.package_api.get_active_package_versions(
self.package_name,
)
# Get a mapping from a tag like "1.7.0" or "feature-xyz" to the ContainerPackage
# tagged with it. It makes certain lookups easy
self.all_pkgs_tags_to_version: Dict[str, ContainerPackage] = {}
for pkg in self.all_package_versions:
for tag in pkg.tags:
self.all_pkgs_tags_to_version[tag] = pkg
logger.info(
f"Located {len(self.all_package_versions)} versions of package {self.package_name}",
)
self.decide_what_tags_to_keep()
def clean(self):
"""
This method will delete image versions, based on the selected tags to delete.
It behaves more like an unlinking than actual deletion. Removing the tag
simply removes a pointer to an image, but the actual image data remains accessible
if one has the sha256 digest of it.
"""
for tag_to_delete in self.tags_to_delete:
package_version_info = self.all_pkgs_tags_to_version[tag_to_delete]
if self.actually_delete:
logger.info(
f"Deleting {tag_to_delete} (id {package_version_info.id})",
)
self.package_api.delete_package_version(
package_version_info,
)
else:
logger.info(
f"Would delete {tag_to_delete} (id {package_version_info.id})",
)
else:
logger.info("No tags to delete")
def clean_untagged(self, is_manifest_image: bool):
"""
This method will delete untagged images, that is those which are not named. It
handles if the image tag is actually a manifest, which points to images that look otherwise
untagged.
"""
def _clean_untagged_manifest():
"""
Handles the deletion of untagged images, but where the package is a manifest, ie a multi
arch image, which means some "untagged" images need to exist still.
Ok, bear with me, these are annoying.
Our images are multi-arch, so the manifest is more like a pointer to a sha256 digest.
These images are untagged, but pointed to, and so should not be removed (or every pull fails).
So for each image getting kept, parse the manifest to find the digest(s) it points to. Then
remove those from the list of untagged images. The final result is the untagged, not pointed to
version which should be safe to remove.
Example:
Tag: ghcr.io/paperless-ngx/paperless-ngx:1.7.1 refers to
amd64: sha256:b9ed4f8753bbf5146547671052d7e91f68cdfc9ef049d06690b2bc866fec2690
armv7: sha256:81605222df4ba4605a2ba4893276e5d08c511231ead1d5da061410e1bbec05c3
arm64: sha256:374cd68db40734b844705bfc38faae84cc4182371de4bebd533a9a365d5e8f3b
each of which appears as untagged image, but isn't really.
So from the list of untagged packages, remove those digests. Once all tags which
are being kept are checked, the remaining untagged packages are actually untagged
with no referrals in a manifest to them.
"""
# Simplify the untagged data, mapping name (which is a digest) to the version
# At the moment, these are the images which APPEAR untagged.
untagged_versions = {}
for x in self.all_package_versions:
if x.untagged:
untagged_versions[x.name] = x
skips = 0
# Parse manifests to locate digests pointed to
for tag in sorted(self.tags_to_keep):
try:
image_index = ImageIndex(
f"ghcr.io/{self.repo_owner}/{self.package_name}",
tag,
)
for manifest in image_index.image_pointers:
if manifest.digest in untagged_versions:
logger.info(
f"Skipping deletion of {manifest.digest},"
f" referred to by {image_index.qualified_name}"
f" for {manifest.platform}",
)
del untagged_versions[manifest.digest]
skips += 1
except Exception as err:
self.actually_delete = False
logger.exception(err)
return
logger.info(
f"Skipping deletion of {skips} packages referred to by a manifest",
)
# Delete the untagged and not pointed at packages
logger.info(f"Deleting untagged packages of {self.package_name}")
for to_delete_name in untagged_versions:
to_delete_version = untagged_versions[to_delete_name]
if self.actually_delete:
logger.info(
f"Deleting id {to_delete_version.id} named {to_delete_version.name}",
)
self.package_api.delete_package_version(
to_delete_version,
)
else:
logger.info(
f"Would delete {to_delete_name} (id {to_delete_version.id})",
)
def _clean_untagged_non_manifest():
"""
If the package is not a multi-arch manifest, images without tags are safe to delete.
"""
for package in self.all_package_versions:
if package.untagged:
if self.actually_delete:
logger.info(
f"Deleting id {package.id} named {package.name}",
)
self.package_api.delete_package_version(
package,
)
else:
logger.info(
f"Would delete {package.name} (id {package.id})",
)
else:
logger.info(
f"Not deleting tag {package.tags[0]} of package {self.package_name}",
)
logger.info("Beginning untagged image cleaning")
if is_manifest_image:
_clean_untagged_manifest()
else:
_clean_untagged_non_manifest()
def decide_what_tags_to_keep(self):
"""
This method holds the logic to delete what tags to keep and there fore
what tags to delete.
By default, any image with at least 1 tag will be kept
"""
# By default, keep anything which is tagged
self.tags_to_keep = list(set(self.all_pkgs_tags_to_version.keys()))
def check_remaining_tags_valid(self):
"""
Checks the non-deleted tags are still valid. The assumption is if the
manifest is can be inspected and each image manifest if points to can be
inspected, the image will still pull.
https://github.com/opencontainers/image-spec/blob/main/image-index.md
"""
logger.info("Beginning confirmation step")
a_tag_failed = False
for tag in sorted(self.tags_to_keep):
try:
image_index = ImageIndex(
f"ghcr.io/{self.repo_owner}/{self.package_name}",
tag,
)
for manifest in image_index.image_pointers:
logger.info(f"Checking {manifest.digest} for {manifest.platform}")
# This follows the pointer from the index to an actual image, layers and all
# Note the format is @
digest_name = f"ghcr.io/{self.repo_owner}/{self.package_name}@{manifest.digest}"
try:
subprocess.run(
[
shutil.which("docker"),
"buildx",
"imagetools",
"inspect",
"--raw",
digest_name,
],
capture_output=True,
check=True,
)
except subprocess.CalledProcessError as e:
logger.error(f"Failed to inspect digest: {e.stderr}")
a_tag_failed = True
except subprocess.CalledProcessError as e:
a_tag_failed = True
logger.error(f"Failed to inspect: {e.stderr}")
continue
if a_tag_failed:
raise Exception("At least one image tag failed to inspect")
class MainImageTagsCleaner(RegistryTagsCleaner):
def decide_what_tags_to_keep(self):
"""
Overrides the default logic for deciding what images to keep. Images tagged as "feature-"
will be removed, if the corresponding branch no longer exists.
"""
# Default to everything gets kept still
super().decide_what_tags_to_keep()
# Locate the feature branches
feature_branches = {}
for branch in self.branch_api.get_branches(
repo=self.repo_name,
):
if branch.name.startswith("feature-"):
logger.debug(f"Found feature branch {branch.name}")
feature_branches[branch.name] = branch
logger.info(f"Located {len(feature_branches)} feature branches")
if not len(feature_branches):
# Our work here is done, delete nothing
return
# Filter to packages which are tagged with feature-*
packages_tagged_feature: List[ContainerPackage] = []
for package in self.all_package_versions:
if package.tag_matches("feature-"):
packages_tagged_feature.append(package)
# Map tags like "feature-xyz" to a ContainerPackage
feature_pkgs_tags_to_versions: Dict[str, ContainerPackage] = {}
for pkg in packages_tagged_feature:
for tag in pkg.tags:
feature_pkgs_tags_to_versions[tag] = pkg
logger.info(
f'Located {len(feature_pkgs_tags_to_versions)} versions of package {self.package_name} tagged "feature-"',
)
# All the feature tags minus all the feature branches leaves us feature tags
# with no corresponding branch
self.tags_to_delete = list(
set(feature_pkgs_tags_to_versions.keys()) - set(feature_branches.keys()),
)
# All the tags minus the set of going to be deleted tags leaves us the
# tags which will be kept around
self.tags_to_keep = list(
set(self.all_pkgs_tags_to_version.keys()) - set(self.tags_to_delete),
)
logger.info(
f"Located {len(self.tags_to_delete)} versions of package {self.package_name} to delete",
)
class LibraryTagsCleaner(RegistryTagsCleaner):
"""
Exists for the off chance that someday, the installer library images
will need their own logic
"""
def _main():
parser = ArgumentParser(
description="Using the GitHub API locate and optionally delete container"
" tags which no longer have an associated feature branch",
)
# Requires an affirmative command to actually do a delete
parser.add_argument(
"--delete",
action="store_true",
default=False,
help="If provided, actually delete the container tags",
)
# When a tagged image is updated, the previous version remains, but it no longer tagged
# Add this option to remove them as well
parser.add_argument(
"--untagged",
action="store_true",
default=False,
help="If provided, delete untagged containers as well",
)
# If given, the package is assumed to be a multi-arch manifest. Cache packages are
# not multi-arch, all other types are
parser.add_argument(
"--is-manifest",
action="store_true",
default=False,
help="If provided, the package is assumed to be a multi-arch manifest following schema v2",
)
# Allows configuration of log level for debugging
parser.add_argument(
"--loglevel",
default="info",
help="Configures the logging level",
)
# Get the name of the package being processed this round
parser.add_argument(
"package",
help="The package to process",
)
args = parser.parse_args()
logging.basicConfig(
level=get_log_level(args),
datefmt="%Y-%m-%d %H:%M:%S",
format="%(asctime)s %(levelname)-8s %(message)s",
)
# Must be provided in the environment
repo_owner: Final[str] = os.environ["GITHUB_REPOSITORY_OWNER"]
repo: Final[str] = os.environ["GITHUB_REPOSITORY"]
gh_token: Final[str] = os.environ["TOKEN"]
# Find all branches named feature-*
# Note: Only relevant to the main application, but simpler to
# leave in for all packages
with GithubBranchApi(gh_token) as branch_api:
with GithubContainerRegistryApi(gh_token, repo_owner) as container_api:
if args.package in {"paperless-ngx", "paperless-ngx/builder/cache/app"}:
cleaner = MainImageTagsCleaner(
args.package,
repo_owner,
repo,
container_api,
branch_api,
)
else:
cleaner = LibraryTagsCleaner(
args.package,
repo_owner,
repo,
container_api,
None,
)
# Set if actually doing a delete vs dry run
cleaner.actually_delete = args.delete
# Clean images with tags
cleaner.clean()
# Clean images which are untagged
cleaner.clean_untagged(args.is_manifest)
# Verify remaining tags still pull
if args.is_manifest:
cleaner.check_remaining_tags_valid()
if __name__ == "__main__":
_main()

View File

@@ -1,47 +0,0 @@
import logging
def get_image_tag(
repo_name: str,
pkg_name: str,
pkg_version: str,
) -> str:
"""
Returns a string representing the normal image for a given package
"""
return f"ghcr.io/{repo_name.lower()}/builder/{pkg_name}:{pkg_version}"
def get_cache_image_tag(
repo_name: str,
pkg_name: str,
pkg_version: str,
branch_name: str,
) -> str:
"""
Returns a string representing the expected image cache tag for a given package
Registry type caching is utilized for the builder images, to allow fast
rebuilds, generally almost instant for the same version
"""
return f"ghcr.io/{repo_name.lower()}/builder/cache/{pkg_name}:{pkg_version}"
def get_log_level(args) -> int:
"""
Returns a logging level, based
:param args:
:return:
"""
levels = {
"critical": logging.CRITICAL,
"error": logging.ERROR,
"warn": logging.WARNING,
"warning": logging.WARNING,
"info": logging.INFO,
"debug": logging.DEBUG,
}
level = levels.get(args.loglevel.lower())
if level is None:
level = logging.INFO
return level

View File

@@ -1,91 +0,0 @@
"""
This is a helper script for the mutli-stage Docker image builder.
It provides a single point of configuration for package version control.
The output JSON object is used by the CI workflow to determine what versions
to build and pull into the final Docker image.
Python package information is obtained from the Pipfile.lock. As this is
kept updated by dependabot, it usually will need no further configuration.
The sole exception currently is pikepdf, which has a dependency on qpdf,
and is configured here to use the latest version of qpdf built by the workflow.
Other package version information is configured directly below, generally by
setting the version and Git information, if any.
"""
import argparse
import json
import os
from pathlib import Path
from typing import Final
from common import get_cache_image_tag
from common import get_image_tag
def _main():
parser = argparse.ArgumentParser(
description="Generate a JSON object of information required to build the given package, based on the Pipfile.lock",
)
parser.add_argument(
"package",
help="The name of the package to generate JSON for",
)
PIPFILE_LOCK_PATH: Final[Path] = Path("Pipfile.lock")
BUILD_CONFIG_PATH: Final[Path] = Path(".build-config.json")
# Read the main config file
build_json: Final = json.loads(BUILD_CONFIG_PATH.read_text())
# Read Pipfile.lock file
pipfile_data: Final = json.loads(PIPFILE_LOCK_PATH.read_text())
args: Final = parser.parse_args()
# Read from environment variables set by GitHub Actions
repo_name: Final[str] = os.environ["GITHUB_REPOSITORY"]
branch_name: Final[str] = os.environ["GITHUB_REF_NAME"]
# Default output values
version = None
extra_config = {}
if args.package in pipfile_data["default"]:
# Read the version from Pipfile.lock
pkg_data = pipfile_data["default"][args.package]
pkg_version = pkg_data["version"].split("==")[-1]
version = pkg_version
# Any extra/special values needed
if args.package == "pikepdf":
extra_config["qpdf_version"] = build_json["qpdf"]["version"]
elif args.package in build_json:
version = build_json[args.package]["version"]
else:
raise NotImplementedError(args.package)
# The JSON object we'll output
output = {
"name": args.package,
"version": version,
"image_tag": get_image_tag(repo_name, args.package, version),
"cache_tag": get_cache_image_tag(
repo_name,
args.package,
version,
branch_name,
),
}
# Add anything special a package may need
output.update(extra_config)
# Output the JSON info to stdout
print(json.dumps(output))
if __name__ == "__main__":
_main()

View File

@@ -1,270 +0,0 @@
"""
This module contains some useful classes for interacting with the Github API.
The full documentation for the API can be found here: https://docs.github.com/en/rest
Mostly, this focusses on two areas, repo branches and repo packages, as the use case
is cleaning up container images which are no longer referred to.
"""
import functools
import logging
import re
import urllib.parse
from typing import Dict
from typing import List
from typing import Optional
import httpx
logger = logging.getLogger("github-api")
class _GithubApiBase:
"""
A base class for interacting with the Github API. It
will handle the session and setting authorization headers.
"""
def __init__(self, token: str) -> None:
self._token = token
self._client: Optional[httpx.Client] = None
def __enter__(self) -> "_GithubApiBase":
"""
Sets up the required headers for auth and response
type from the API
"""
self._client = httpx.Client()
self._client.headers.update(
{
"Accept": "application/vnd.github.v3+json",
"Authorization": f"token {self._token}",
},
)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""
Ensures the authorization token is cleaned up no matter
the reason for the exit
"""
if "Accept" in self._client.headers:
del self._client.headers["Accept"]
if "Authorization" in self._client.headers:
del self._client.headers["Authorization"]
# Close the session as well
self._client.close()
self._client = None
def _read_all_pages(self, endpoint):
"""
Helper function to read all pages of an endpoint, utilizing the
next.url until exhausted. Assumes the endpoint returns a list
"""
internal_data = []
while True:
resp = self._client.get(endpoint)
if resp.status_code == 200:
internal_data += resp.json()
if "next" in resp.links:
endpoint = resp.links["next"]["url"]
else:
logger.debug("Exiting pagination loop")
break
else:
logger.warning(f"Request to {endpoint} return HTTP {resp.status_code}")
resp.raise_for_status()
return internal_data
class _EndpointResponse:
"""
For all endpoint JSON responses, store the full
response data, for ease of extending later, if need be.
"""
def __init__(self, data: Dict) -> None:
self._data = data
class GithubBranch(_EndpointResponse):
"""
Simple wrapper for a repository branch, only extracts name information
for now.
"""
def __init__(self, data: Dict) -> None:
super().__init__(data)
self.name = self._data["name"]
class GithubBranchApi(_GithubApiBase):
"""
Wrapper around branch API.
See https://docs.github.com/en/rest/branches/branches
"""
def __init__(self, token: str) -> None:
super().__init__(token)
self._ENDPOINT = "https://api.github.com/repos/{REPO}/branches"
def get_branches(self, repo: str) -> List[GithubBranch]:
"""
Returns all current branches of the given repository owned by the given
owner or organization.
"""
# The environment GITHUB_REPOSITORY already contains the owner in the correct location
endpoint = self._ENDPOINT.format(REPO=repo)
internal_data = self._read_all_pages(endpoint)
return [GithubBranch(branch) for branch in internal_data]
class ContainerPackage(_EndpointResponse):
"""
Data class wrapping the JSON response from the package related
endpoints
"""
def __init__(self, data: Dict):
super().__init__(data)
# This is a numerical ID, required for interactions with this
# specific package, including deletion of it or restoration
self.id: int = self._data["id"]
# A string name. This might be an actual name or it could be a
# digest string like "sha256:"
self.name: str = self._data["name"]
# URL to the package, including its ID, can be used for deletion
# or restoration without needing to build up a URL ourselves
self.url: str = self._data["url"]
# The list of tags applied to this image. Maybe an empty list
self.tags: List[str] = self._data["metadata"]["container"]["tags"]
@functools.cached_property
def untagged(self) -> bool:
"""
Returns True if the image has no tags applied to it, False otherwise
"""
return len(self.tags) == 0
@functools.cache
def tag_matches(self, pattern: str) -> bool:
"""
Returns True if the image has at least one tag which matches the given regex,
False otherwise
"""
return any(re.match(pattern, tag) is not None for tag in self.tags)
def __repr__(self):
return f"Package {self.name}"
class GithubContainerRegistryApi(_GithubApiBase):
"""
Class wrapper to deal with the Github packages API. This class only deals with
container type packages, the only type published by paperless-ngx.
"""
def __init__(self, token: str, owner_or_org: str) -> None:
super().__init__(token)
self._owner_or_org = owner_or_org
if self._owner_or_org == "paperless-ngx":
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-an-organization
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
# https://docs.github.com/en/rest/packages#delete-package-version-for-an-organization
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/orgs/{ORG}/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
else:
# https://docs.github.com/en/rest/packages#get-all-package-versions-for-a-package-owned-by-the-authenticated-user
self._PACKAGES_VERSIONS_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions"
# https://docs.github.com/en/rest/packages#delete-a-package-version-for-the-authenticated-user
self._PACKAGE_VERSION_DELETE_ENDPOINT = "https://api.github.com/user/packages/{PACKAGE_TYPE}/{PACKAGE_NAME}/versions/{PACKAGE_VERSION_ID}"
self._PACKAGE_VERSION_RESTORE_ENDPOINT = (
f"{self._PACKAGE_VERSION_DELETE_ENDPOINT}/restore"
)
def get_active_package_versions(
self,
package_name: str,
) -> List[ContainerPackage]:
"""
Returns all the versions of a given package (container images) from
the API
"""
package_type: str = "container"
# Need to quote this for slashes in the name
package_name = urllib.parse.quote(package_name, safe="")
endpoint = self._PACKAGES_VERSIONS_ENDPOINT.format(
ORG=self._owner_or_org,
PACKAGE_TYPE=package_type,
PACKAGE_NAME=package_name,
)
pkgs = []
for data in self._read_all_pages(endpoint):
pkgs.append(ContainerPackage(data))
return pkgs
def get_deleted_package_versions(
self,
package_name: str,
) -> List[ContainerPackage]:
package_type: str = "container"
# Need to quote this for slashes in the name
package_name = urllib.parse.quote(package_name, safe="")
endpoint = (
self._PACKAGES_VERSIONS_ENDPOINT.format(
ORG=self._owner_or_org,
PACKAGE_TYPE=package_type,
PACKAGE_NAME=package_name,
)
+ "?state=deleted"
)
pkgs = []
for data in self._read_all_pages(endpoint):
pkgs.append(ContainerPackage(data))
return pkgs
def delete_package_version(self, package_data: ContainerPackage):
"""
Deletes the given package version from the GHCR
"""
resp = self._client.delete(package_data.url)
if resp.status_code != 204:
logger.warning(
f"Request to delete {package_data.url} returned HTTP {resp.status_code}",
)
def restore_package_version(
self,
package_name: str,
package_data: ContainerPackage,
):
package_type: str = "container"
endpoint = self._PACKAGE_VERSION_RESTORE_ENDPOINT.format(
ORG=self._owner_or_org,
PACKAGE_TYPE=package_type,
PACKAGE_NAME=package_name,
PACKAGE_VERSION_ID=package_data.id,
)
resp = self._client.post(endpoint)
if resp.status_code != 204:
logger.warning(
f"Request to delete {endpoint} returned HTTP {resp.status_code}",
)

View File

@@ -16,7 +16,7 @@ on:
env:
# This is the version of pipenv all the steps will use
# If changing this, change Dockerfile
DEFAULT_PIP_ENV_VERSION: "2023.3.20"
DEFAULT_PIP_ENV_VERSION: "2023.4.20"
# This is the default version of Python to use in most steps
# If changing this, change Dockerfile
DEFAULT_PYTHON_VERSION: "3.9"
@@ -161,7 +161,7 @@ jobs:
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pytest -ra
-
name: Upload coverage to Codecov
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }}
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION && github.event_name == 'pull_request'}}
uses: codecov/codecov-action@v3
with:
# not required for public repos, but intermittently fails otherwise
@@ -197,90 +197,20 @@ jobs:
- run: cd src-ui && npm run test
- run: cd src-ui && npm run e2e:ci
prepare-docker-build:
name: Prepare Docker Pipeline Data
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
runs-on: ubuntu-22.04
needs:
- documentation
- tests-backend
- tests-frontend
steps:
-
name: Set ghcr repository name
id: set-ghcr-repository
run: |
ghcr_name=$(echo "${GITHUB_REPOSITORY}" | awk '{ print tolower($0) }')
echo "repository=${ghcr_name}" >> $GITHUB_OUTPUT
-
name: Checkout
uses: actions/checkout@v3
-
name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
-
name: Setup qpdf image
id: qpdf-setup
run: |
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py qpdf)
echo ${build_json}
echo "qpdf-json=${build_json}" >> $GITHUB_OUTPUT
-
name: Setup psycopg2 image
id: psycopg2-setup
run: |
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py psycopg2)
echo ${build_json}
echo "psycopg2-json=${build_json}" >> $GITHUB_OUTPUT
-
name: Setup pikepdf image
id: pikepdf-setup
run: |
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py pikepdf)
echo ${build_json}
echo "pikepdf-json=${build_json}" >> $GITHUB_OUTPUT
-
name: Setup jbig2enc image
id: jbig2enc-setup
run: |
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py jbig2enc)
echo ${build_json}
echo "jbig2enc-json=${build_json}" >> $GITHUB_OUTPUT
outputs:
ghcr-repository: ${{ steps.set-ghcr-repository.outputs.repository }}
qpdf-json: ${{ steps.qpdf-setup.outputs.qpdf-json }}
pikepdf-json: ${{ steps.pikepdf-setup.outputs.pikepdf-json }}
psycopg2-json: ${{ steps.psycopg2-setup.outputs.psycopg2-json }}
jbig2enc-json: ${{ steps.jbig2enc-setup.outputs.jbig2enc-json}}
# build and push image to docker hub.
build-docker-image:
name: Build Docker image for ${{ github.ref_name }}
runs-on: ubuntu-22.04
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
concurrency:
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
cancel-in-progress: true
needs:
- prepare-docker-build
- tests-backend
- tests-frontend
steps:
-
name: Check pushing to Docker Hub
id: docker-hub
id: push-other-places
# Only push to Dockerhub from the main repo AND the ref is either:
# main
# dev
@@ -288,22 +218,29 @@ jobs:
# a tag
# Otherwise forks would require a Docker Hub account and secrets setup
run: |
if [[ ${{ needs.prepare-docker-build.outputs.ghcr-repository }} == "paperless-ngx/paperless-ngx" && ( ${{ github.ref_name }} == "main" || ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then
if [[ ${{ github.repository_owner }} == "paperless-ngx" && ( ${{ github.ref_name }} == "main" || ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then
echo "Enabling DockerHub image push"
echo "enable=true" >> $GITHUB_OUTPUT
else
echo "Not pushing to DockerHub"
echo "enable=false" >> $GITHUB_OUTPUT
fi
-
name: Set ghcr repository name
id: set-ghcr-repository
run: |
ghcr_name=$(echo "${{ github.repository }}" | awk '{ print tolower($0) }')
echo "Name is ${ghcr_name}"
echo "ghcr-repository=${ghcr_name}" >> $GITHUB_OUTPUT
-
name: Gather Docker metadata
id: docker-meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}
name=paperlessngx/paperless-ngx,enable=${{ steps.docker-hub.outputs.enable }}
name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.docker-hub.outputs.enable }}
ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}
name=paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }}
name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }}
tags: |
# Tag branches with branch name
type=ref,event=branch
@@ -314,6 +251,9 @@ jobs:
-
name: Checkout
uses: actions/checkout@v3
# If https://github.com/docker/buildx/issues/1044 is resolved,
# the append input with a native arm64 arch could be used to
# significantly speed up building
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -331,15 +271,15 @@ jobs:
name: Login to Docker Hub
uses: docker/login-action@v2
# Don't attempt to login is not pushing to Docker Hub
if: steps.docker-hub.outputs.enable == 'true'
if: steps.push-other-places.outputs.enable == 'true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Login to Quay.io
uses: docker/login-action@v2
# Don't attempt to login is not pushing to Docker Hub
if: steps.docker-hub.outputs.enable == 'true'
# Don't attempt to login is not pushing to Quay.io
if: steps.push-other-places.outputs.enable == 'true'
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
@@ -354,19 +294,13 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
build-args: |
JBIG2ENC_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }}
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }}
# Get cache layers from this branch, then dev, then main
# This allows new branches to get at least some cache benefits, generally from dev
cache-from: |
type=registry,ref=ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
type=registry,ref=ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}/builder/cache/app:dev
type=registry,ref=ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}/builder/cache/app:main
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:dev
cache-to: |
type=registry,mode=max,ref=ghcr.io/${{ needs.prepare-docker-build.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
type=registry,mode=max,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
-
name: Inspect image
run: |

View File

@@ -12,9 +12,6 @@ on:
push:
paths:
- ".github/workflows/cleanup-tags.yml"
- ".github/scripts/cleanup-tags.py"
- ".github/scripts/github.py"
- ".github/scripts/common.py"
concurrency:
group: registry-tags-cleanup
@@ -22,62 +19,65 @@ concurrency:
jobs:
cleanup-images:
name: Cleanup Image Tags for ${{ matrix.primary-name }}
name: Cleanup Image Tags for paperless-ngx
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-22.04
strategy:
matrix:
include:
- primary-name: "paperless-ngx"
cache-name: "paperless-ngx/builder/cache/app"
- primary-name: "paperless-ngx/builder/qpdf"
cache-name: "paperless-ngx/builder/cache/qpdf"
- primary-name: "paperless-ngx/builder/pikepdf"
cache-name: "paperless-ngx/builder/cache/pikepdf"
- primary-name: "paperless-ngx/builder/jbig2enc"
cache-name: "paperless-ngx/builder/cache/jbig2enc"
- primary-name: "paperless-ngx/builder/psycopg2"
cache-name: "paperless-ngx/builder/cache/psycopg2"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Login to Github Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
-
name: Install Python libraries
run: |
python -m pip install httpx docker
#
# Clean up primary package
#
-
name: Cleanup for package "${{ matrix.primary-name }}"
name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --untagged --is-manifest --delete "${{ matrix.primary-name }}"
#
# Clean up registry cache package
#
uses: stumpylog/image-cleaner-action/ephemeral@v0.1.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
is_org: "true"
package_name: "paperless-ngx"
scheme: "branch"
repo_name: "paperless-ngx"
match_regex: "feature-"
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-22.04
needs:
- cleanup-images
strategy:
fail-fast: false
matrix:
include:
- primary-name: "paperless-ngx"
- primary-name: "paperless-ngx/builder/cache/app"
- primary-name: "paperless-ngx/builder/qpdf"
- primary-name: "paperless-ngx/builder/cache/qpdf"
- primary-name: "paperless-ngx/builder/pikepdf"
- primary-name: "paperless-ngx/builder/cache/pikepdf"
- primary-name: "paperless-ngx/builder/jbig2enc"
- primary-name: "paperless-ngx/builder/cache/jbig2enc"
- primary-name: "paperless-ngx/builder/psycopg2"
- primary-name: "paperless-ngx/builder/cache/psycopg2"
# TODO: Remove the above and replace with the below
# - primary-name: "builder/qpdf"
# - primary-name: "builder/cache/qpdf"
# - primary-name: "builder/pikepdf"
# - primary-name: "builder/cache/pikepdf"
# - primary-name: "builder/jbig2enc"
# - primary-name: "builder/cache/jbig2enc"
# - primary-name: "builder/psycopg2"
# - primary-name: "builder/cache/psycopg2"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
steps:
-
name: Cleanup for package "${{ matrix.cache-name }}"
name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
run: |
python ${GITHUB_WORKSPACE}/.github/scripts/cleanup-tags.py --untagged --delete "${{ matrix.cache-name }}"
uses: stumpylog/image-cleaner-action/untagged@v0.1.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
is_org: "true"
package_name: "${{ matrix.primary-name }}"

View File

@@ -1,310 +0,0 @@
# This workflow will run to update the installer library of
# Docker images. These are the images which provide updated wheels
# .deb installation packages or maybe just some compiled library
name: Build Image Library
on:
push:
# Must match one of these branches AND one of the paths
# to be triggered
branches:
- "main"
- "dev"
- "library-*"
- "feature-*"
paths:
# Trigger the workflow if a Dockerfile changed
- "docker-builders/**"
# Trigger if a package was updated
- ".build-config.json"
- "Pipfile.lock"
# Also trigger on workflow changes related to the library
- ".github/workflows/installer-library.yml"
- ".github/workflows/reusable-workflow-builder.yml"
- ".github/scripts/**"
# Set a workflow level concurrency group so primary workflow
# can wait for this to complete if needed
# DO NOT CHANGE without updating main workflow group
concurrency:
group: build-installer-library
cancel-in-progress: false
jobs:
prepare-docker-build:
name: Prepare Docker Image Version Data
runs-on: ubuntu-22.04
steps:
-
name: Set ghcr repository name
id: set-ghcr-repository
run: |
ghcr_name=$(echo "${GITHUB_REPOSITORY}" | awk '{ print tolower($0) }')
echo "repository=${ghcr_name}" >> $GITHUB_OUTPUT
-
name: Checkout
uses: actions/checkout@v3
-
name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
-
name: Install jq
run: |
sudo apt-get update
sudo apt-get install jq
-
name: Setup qpdf image
id: qpdf-setup
run: |
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py qpdf)
echo ${build_json}
echo "qpdf-json=${build_json}" >> $GITHUB_OUTPUT
-
name: Setup psycopg2 image
id: psycopg2-setup
run: |
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py psycopg2)
echo ${build_json}
echo "psycopg2-json=${build_json}" >> $GITHUB_OUTPUT
-
name: Setup pikepdf image
id: pikepdf-setup
run: |
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py pikepdf)
echo ${build_json}
echo "pikepdf-json=${build_json}" >> $GITHUB_OUTPUT
-
name: Setup jbig2enc image
id: jbig2enc-setup
run: |
build_json=$(python ${GITHUB_WORKSPACE}/.github/scripts/get-build-json.py jbig2enc)
echo ${build_json}
echo "jbig2enc-json=${build_json}" >> $GITHUB_OUTPUT
-
name: Setup other versions
id: cache-bust-setup
run: |
pillow_version=$(jq -r '.default.pillow.version | gsub("=";"")' Pipfile.lock)
lxml_version=$(jq -r '.default.lxml.version | gsub("=";"")' Pipfile.lock)
echo "Pillow is ${pillow_version}"
echo "lxml is ${lxml_version}"
echo "pillow-version=${pillow_version}" >> $GITHUB_OUTPUT
echo "lxml-version=${lxml_version}" >> $GITHUB_OUTPUT
outputs:
ghcr-repository: ${{ steps.set-ghcr-repository.outputs.repository }}
qpdf-json: ${{ steps.qpdf-setup.outputs.qpdf-json }}
pikepdf-json: ${{ steps.pikepdf-setup.outputs.pikepdf-json }}
psycopg2-json: ${{ steps.psycopg2-setup.outputs.psycopg2-json }}
jbig2enc-json: ${{ steps.jbig2enc-setup.outputs.jbig2enc-json }}
pillow-version: ${{ steps.cache-bust-setup.outputs.pillow-version }}
lxml-version: ${{ steps.cache-bust-setup.outputs.lxml-version }}
build-qpdf-debs:
name: qpdf
needs:
- prepare-docker-build
uses: ./.github/workflows/reusable-workflow-builder.yml
with:
dockerfile: ./docker-builders/Dockerfile.qpdf
build-platforms: linux/amd64
build-json: ${{ needs.prepare-docker-build.outputs.qpdf-json }}
build-args: |
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
build-jbig2enc:
name: jbig2enc
needs:
- prepare-docker-build
uses: ./.github/workflows/reusable-workflow-builder.yml
with:
dockerfile: ./docker-builders/Dockerfile.jbig2enc
build-json: ${{ needs.prepare-docker-build.outputs.jbig2enc-json }}
build-args: |
JBIG2ENC_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }}
build-psycopg2-wheel:
name: psycopg2
needs:
- prepare-docker-build
uses: ./.github/workflows/reusable-workflow-builder.yml
with:
dockerfile: ./docker-builders/Dockerfile.psycopg2
build-json: ${{ needs.prepare-docker-build.outputs.psycopg2-json }}
build-args: |
PSYCOPG2_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }}
build-pikepdf-wheel:
name: pikepdf
needs:
- prepare-docker-build
- build-qpdf-debs
uses: ./.github/workflows/reusable-workflow-builder.yml
with:
dockerfile: ./docker-builders/Dockerfile.pikepdf
build-json: ${{ needs.prepare-docker-build.outputs.pikepdf-json }}
build-args: |
REPO=${{ needs.prepare-docker-build.outputs.ghcr-repository }}
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
PIKEPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
PILLOW_VERSION=${{ needs.prepare-docker-build.outputs.pillow-version }}
LXML_VERSION=${{ needs.prepare-docker-build.outputs.lxml-version }}
commit-binary-files:
name: Store installers
needs:
- prepare-docker-build
- build-qpdf-debs
- build-jbig2enc
- build-psycopg2-wheel
- build-pikepdf-wheel
runs-on: ubuntu-22.04
steps:
-
name: Checkout
uses: actions/checkout@v3
with:
ref: binary-library
-
name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
-
name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends tree
-
name: Extract qpdf files
run: |
version=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
tag=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).image_tag }}
docker pull --quiet ${tag}
docker create --name qpdf-extract ${tag}
mkdir --parents qpdf/${version}/amd64
docker cp qpdf-extract:/usr/src/qpdf/${version}/amd64 qpdf/${version}
mkdir --parents qpdf/${version}/arm64
docker cp qpdf-extract:/usr/src/qpdf/${version}/arm64 qpdf/${version}
mkdir --parents qpdf/${version}/armv7
docker cp qpdf-extract:/usr/src/qpdf/${version}/armv7 qpdf/${version}
-
name: Extract psycopg2 files
run: |
version=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).version }}
tag=${{ fromJSON(needs.prepare-docker-build.outputs.psycopg2-json).image_tag }}
docker pull --quiet --platform linux/amd64 ${tag}
docker create --platform linux/amd64 --name psycopg2-extract ${tag}
mkdir --parents psycopg2/${version}/amd64
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/amd64
mv psycopg2/${version}/amd64/wheels/* psycopg2/${version}/amd64
rm -r psycopg2/${version}/amd64/wheels/
docker rm psycopg2-extract
docker pull --quiet --platform linux/arm64 ${tag}
docker create --platform linux/arm64 --name psycopg2-extract ${tag}
mkdir --parents psycopg2/${version}/arm64
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/arm64
mv psycopg2/${version}/arm64/wheels/* psycopg2/${version}/arm64
rm -r psycopg2/${version}/arm64/wheels/
docker rm psycopg2-extract
docker pull --quiet --platform linux/arm/v7 ${tag}
docker create --platform linux/arm/v7 --name psycopg2-extract ${tag}
mkdir --parents psycopg2/${version}/armv7
docker cp psycopg2-extract:/usr/src/wheels/ psycopg2/${version}/armv7
mv psycopg2/${version}/armv7/wheels/* psycopg2/${version}/armv7
rm -r psycopg2/${version}/armv7/wheels/
docker rm psycopg2-extract
-
name: Extract pikepdf files
run: |
version=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).version }}
tag=${{ fromJSON(needs.prepare-docker-build.outputs.pikepdf-json).image_tag }}
docker pull --quiet --platform linux/amd64 ${tag}
docker create --platform linux/amd64 --name pikepdf-extract ${tag}
mkdir --parents pikepdf/${version}/amd64
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/amd64
mv pikepdf/${version}/amd64/wheels/* pikepdf/${version}/amd64
rm -r pikepdf/${version}/amd64/wheels/
docker rm pikepdf-extract
docker pull --quiet --platform linux/arm64 ${tag}
docker create --platform linux/arm64 --name pikepdf-extract ${tag}
mkdir --parents pikepdf/${version}/arm64
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/arm64
mv pikepdf/${version}/arm64/wheels/* pikepdf/${version}/arm64
rm -r pikepdf/${version}/arm64/wheels/
docker rm pikepdf-extract
docker pull --quiet --platform linux/arm/v7 ${tag}
docker create --platform linux/arm/v7 --name pikepdf-extract ${tag}
mkdir --parents pikepdf/${version}/armv7
docker cp pikepdf-extract:/usr/src/wheels/ pikepdf/${version}/armv7
mv pikepdf/${version}/armv7/wheels/* pikepdf/${version}/armv7
rm -r pikepdf/${version}/armv7/wheels/
docker rm pikepdf-extract
-
name: Extract jbig2enc files
run: |
version=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).version }}
tag=${{ fromJSON(needs.prepare-docker-build.outputs.jbig2enc-json).image_tag }}
docker pull --quiet --platform linux/amd64 ${tag}
docker create --platform linux/amd64 --name jbig2enc-extract ${tag}
mkdir --parents jbig2enc/${version}/amd64
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/amd64/
mv jbig2enc/${version}/amd64/build/* jbig2enc/${version}/amd64/
docker rm jbig2enc-extract
docker pull --quiet --platform linux/arm64 ${tag}
docker create --platform linux/arm64 --name jbig2enc-extract ${tag}
mkdir --parents jbig2enc/${version}/arm64
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/arm64
mv jbig2enc/${version}/arm64/build/* jbig2enc/${version}/arm64/
docker rm jbig2enc-extract
docker pull --quiet --platform linux/arm/v7 ${tag}
docker create --platform linux/arm/v7 --name jbig2enc-extract ${tag}
mkdir --parents jbig2enc/${version}/armv7
docker cp jbig2enc-extract:/usr/src/jbig2enc/build jbig2enc/${version}/armv7
mv jbig2enc/${version}/armv7/build/* jbig2enc/${version}/armv7/
docker rm jbig2enc-extract
-
name: Show file structure
run: |
tree .
-
name: Commit files
run: |
git config --global user.name "github-actions"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add pikepdf/ qpdf/ psycopg2/ jbig2enc/
git commit -m "Updating installer packages" || true
git push origin || true

View File

@@ -1,57 +0,0 @@
name: Reusable Image Builder
on:
workflow_call:
inputs:
dockerfile:
required: true
type: string
build-json:
required: true
type: string
build-args:
required: false
default: ""
type: string
build-platforms:
required: false
default: linux/amd64,linux/arm64,linux/arm/v7
type: string
concurrency:
group: ${{ github.workflow }}-${{ fromJSON(inputs.build-json).name }}-${{ fromJSON(inputs.build-json).version }}
cancel-in-progress: false
jobs:
build-image:
name: Build ${{ fromJSON(inputs.build-json).name }} @ ${{ fromJSON(inputs.build-json).version }}
runs-on: ubuntu-22.04
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Login to Github Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Build ${{ fromJSON(inputs.build-json).name }}
uses: docker/build-push-action@v4
with:
context: .
file: ${{ inputs.dockerfile }}
tags: ${{ fromJSON(inputs.build-json).image_tag }}
platforms: ${{ inputs.build-platforms }}
build-args: ${{ inputs.build-args }}
push: true
cache-from: type=registry,ref=${{ fromJSON(inputs.build-json).cache_tag }}
cache-to: type=registry,mode=max,ref=${{ fromJSON(inputs.build-json).cache_tag }}

View File

@@ -27,7 +27,7 @@ repos:
- id: check-case-conflict
- id: detect-private-key
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.7.1"
rev: 'v2.7.1'
hooks:
- id: prettier
types_or:
@@ -37,7 +37,7 @@ repos:
exclude: "(^Pipfile\\.lock$)"
# Python hooks
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.263'
rev: 'v0.0.265'
hooks:
- id: ruff
- repo: https://github.com/psf/black

View File

@@ -21,7 +21,7 @@ RUN set -eux \
# Comments:
# - pipenv dependencies are not left in the final image
# - pipenv can't touch the final image somehow
FROM --platform=$BUILDPLATFORM python:3.9-slim-bullseye as pipenv-base
FROM --platform=$BUILDPLATFORM python:3.9-alpine as pipenv-base
WORKDIR /usr/src/pipenv
@@ -29,7 +29,7 @@ COPY Pipfile* ./
RUN set -eux \
&& echo "Installing pipenv" \
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.3.20 \
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.4.20 \
&& echo "Generating requirement.txt" \
&& pipenv requirements > requirements.txt
@@ -170,18 +170,18 @@ RUN set -eux \
ARG TARGETARCH
ARG TARGETVARIANT
# Workflow provided, defaults set for manual building
# Can be workflow provided, defaults set for manual building
ARG JBIG2ENC_VERSION=0.29
ARG QPDF_VERSION=11.3.0
ARG PIKEPDF_VERSION=7.1.1
ARG PSYCOPG2_VERSION=2.9.5
ARG PIKEPDF_VERSION=7.2.0
ARG PSYCOPG2_VERSION=2.9.6
# Install the built packages from the installer library images
# These change sometimes
RUN set -eux \
&& echo "Getting binaries" \
&& mkdir paperless-ngx \
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/paperless-ngx/archive/ba28a1e16c27d121b644b4f6bdb78855a2850561.tar.gz \
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/builder/archive/3d6574e2dbaa8b8cdced864a256b0de59015f605.tar.gz \
&& tar -xf paperless-ngx.tar.gz --directory paperless-ngx --strip-components=1 \
&& cd paperless-ngx \
# Setting a specific revision ensures we know what this installed

27
Pipfile
View File

@@ -10,7 +10,9 @@ name = "piwheels"
[packages]
dateparser = "~=1.1"
django = "~=4.1"
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
django = "~=4.1.9"
django-cors-headers = "*"
django-celery-results = "*"
django-compression-middleware = "*"
@@ -19,18 +21,18 @@ django-extensions = "*"
django-filter = "~=22.1"
djangorestframework = "~=3.14"
djangorestframework-guardian = "*"
django-ipware = "*"
filelock = "*"
gunicorn = "*"
imap-tools = "*"
langdetect = "*"
pathvalidate = "*"
pillow = "~=9.4"
pillow = "*"
pikepdf = "*"
python-gnupg = "*"
python-dotenv = "*"
python-dateutil = "*"
python-magic = "*"
python-ipware = "*"
psycopg2 = "*"
rapidfuzz = "*"
redis = {extras = ["hiredis"], version = "*"}
@@ -43,13 +45,10 @@ inotifyrecursive = "~=0.3"
ocrmypdf = "~=14.0"
tqdm = "*"
tika = "*"
# TODO: This will sadly also install daphne+dependencies,
# which an ASGI server we don't need. Adds about 15MB image size.
channels = "~=3.0"
channels = "~=4.0"
channels-redis = "*"
uvicorn = {extras = ["standard"], version = "*"}
concurrent-log-handler = "*"
"pdfminer.six" = "*"
pyzbar = "*"
mysqlclient = "*"
celery = {extras = ["redis"], version = "*"}
@@ -64,9 +63,15 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
#
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
scipy = "==1.8.1"
# v4 brings in extra dependencies for features not used here
reportlab = "==3.6.12"
[dev-packages]
coveralls = "*"
# Linting
black = "*"
pre-commit = "*"
ruff = "*"
# Testing
factory-boy = "*"
pytest = "*"
pytest-cov = "*"
@@ -74,11 +79,11 @@ pytest-django = "*"
pytest-env = "*"
pytest-sugar = "*"
pytest-xdist = "*"
black = "*"
pre-commit = "*"
"pdfminer.six" = "*"
imagehash = "*"
daphne = "*"
# Documentation
mkdocs-material = "*"
ruff = "*"
[typing-dev]
mypy = "*"

2184
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,81 +0,0 @@
#!/usr/bin/env bash
# Helper script for building the Docker image locally.
# Parses and provides the nessecary versions of other images to Docker
# before passing in the rest of script args.
# First Argument: The Dockerfile to build
# Other Arguments: Additional arguments to docker build
# Example Usage:
# ./build-docker-image.sh Dockerfile -t paperless-ngx:my-awesome-feature
set -eu
if ! command -v jq &> /dev/null ; then
echo "jq required"
exit 1
elif [ ! -f "$1" ]; then
echo "$1 is not a file, please provide the Dockerfile"
exit 1
fi
# Get the branch name (used for caching)
branch_name=$(git rev-parse --abbrev-ref HEAD)
# Parse eithe Pipfile.lock or the .build-config.json
jbig2enc_version=$(jq -r '.jbig2enc.version' .build-config.json)
qpdf_version=$(jq -r '.qpdf.version' .build-config.json)
psycopg2_version=$(jq -r '.default.psycopg2.version | gsub("=";"")' Pipfile.lock)
pikepdf_version=$(jq -r '.default.pikepdf.version | gsub("=";"")' Pipfile.lock)
pillow_version=$(jq -r '.default.pillow.version | gsub("=";"")' Pipfile.lock)
lxml_version=$(jq -r '.default.lxml.version | gsub("=";"")' Pipfile.lock)
base_filename="$(basename -- "${1}")"
build_args_str=""
cache_from_str=""
case "${base_filename}" in
*.jbig2enc)
build_args_str="--build-arg JBIG2ENC_VERSION=${jbig2enc_version}"
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/jbig2enc:${jbig2enc_version}"
;;
*.psycopg2)
build_args_str="--build-arg PSYCOPG2_VERSION=${psycopg2_version}"
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/psycopg2:${psycopg2_version}"
;;
*.qpdf)
build_args_str="--build-arg QPDF_VERSION=${qpdf_version}"
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/qpdf:${qpdf_version}"
;;
*.pikepdf)
build_args_str="--build-arg QPDF_VERSION=${qpdf_version} --build-arg PIKEPDF_VERSION=${pikepdf_version} --build-arg PILLOW_VERSION=${pillow_version} --build-arg LXML_VERSION=${lxml_version}"
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/pikepdf:${pikepdf_version}"
;;
Dockerfile)
build_args_str="--build-arg QPDF_VERSION=${qpdf_version} --build-arg PIKEPDF_VERSION=${pikepdf_version} --build-arg PSYCOPG2_VERSION=${psycopg2_version} --build-arg JBIG2ENC_VERSION=${jbig2enc_version}"
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:${branch_name} --cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev"
;;
*)
echo "Unable to match ${base_filename}"
exit 1
;;
esac
read -r -a build_args_arr <<< "${build_args_str}"
read -r -a cache_from_arr <<< "${cache_from_str}"
set -eux
docker buildx build --file "${1}" \
--progress=plain \
--output=type=docker \
"${cache_from_arr[@]}" \
"${build_args_arr[@]}" \
"${@:2}" .

View File

@@ -1,48 +0,0 @@
# This Dockerfile compiles the jbig2enc library
# Inputs:
# - JBIG2ENC_VERSION - the Git tag to checkout and build
FROM debian:bullseye-slim as main
LABEL org.opencontainers.image.description="A intermediate image with jbig2enc built"
ARG DEBIAN_FRONTEND=noninteractive
ARG JBIG2ENC_VERSION
ARG BUILD_PACKAGES="\
build-essential \
automake \
libtool \
libleptonica-dev \
zlib1g-dev \
git \
ca-certificates"
WORKDIR /usr/src/jbig2enc
RUN set -eux \
&& echo "Installing build tools" \
&& apt-get update --quiet \
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
&& echo "Building jbig2enc" \
&& git clone --quiet --branch $JBIG2ENC_VERSION https://github.com/agl/jbig2enc . \
&& ./autogen.sh \
&& ./configure \
&& make \
&& echo "Gathering package data" \
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./pkg-list.txt \
&& echo "Cleaning up image" \
&& apt-get -y purge ${BUILD_PACKAGES} \
&& apt-get -y autoremove --purge \
&& rm -rf /var/lib/apt/lists/* \
&& echo "Moving files around" \
&& mkdir build \
# Unlink a symlink that causes problems
&& unlink ./src/.libs/libjbig2enc.la \
# Move what the link pointed to
&& mv ./src/libjbig2enc.la ./build/ \
# Move the shared library .so files
&& mv ./src/.libs/libjbig2enc* ./build/ \
# And move the cli binary
&& mv ./src/jbig2 ./build/ \
&& mv ./pkg-list.txt ./build/

View File

@@ -1,118 +0,0 @@
# This Dockerfile builds the pikepdf wheel
# Inputs:
# - REPO - Docker repository to pull qpdf from
# - QPDF_VERSION - The image qpdf version to copy .deb files from
# - PIKEPDF_VERSION - Version of pikepdf to build wheel for
# Default to pulling from the main repo registry when manually building
ARG REPO="paperless-ngx/paperless-ngx"
# This does nothing, except provide a name for a copy below
ARG QPDF_VERSION
FROM --platform=$BUILDPLATFORM ghcr.io/${REPO}/builder/qpdf:${QPDF_VERSION} as qpdf-builder
#
# Stage: builder
# Purpose:
# - Build the pikepdf wheel
# - Build any dependent wheels which can't be found
#
FROM python:3.9-slim-bullseye as builder
LABEL org.opencontainers.image.description="A intermediate image with pikepdf wheel built"
# Buildx provided
ARG TARGETARCH
ARG TARGETVARIANT
ARG DEBIAN_FRONTEND=noninteractive
# Workflow provided
ARG QPDF_VERSION
ARG PIKEPDF_VERSION
# These are not used, but will still bust the cache if one changes
# Otherwise, the main image will try to build thing (and fail)
ARG PILLOW_VERSION
ARG LXML_VERSION
ARG BUILD_PACKAGES="\
build-essential \
python3-dev \
python3-pip \
# qpdf requirement - https://github.com/qpdf/qpdf#crypto-providers
libgnutls28-dev \
# lxml requrements - https://lxml.de/installation.html
libxml2-dev \
libxslt1-dev \
# Pillow requirements - https://pillow.readthedocs.io/en/stable/installation.html#external-libraries
# JPEG functionality
libjpeg62-turbo-dev \
# conpressed PNG
zlib1g-dev \
# compressed TIFF
libtiff-dev \
# type related services
libfreetype-dev \
# color management
liblcms2-dev \
# WebP format
libwebp-dev \
# JPEG 2000
libopenjp2-7-dev \
# improved color quantization
libimagequant-dev \
# complex text layout support
libraqm-dev"
WORKDIR /usr/src
COPY --from=qpdf-builder /usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/*.deb ./
# As this is an base image for a multi-stage final image
# the added size of the install is basically irrelevant
RUN set -eux \
&& echo "Installing build tools" \
&& apt-get update --quiet \
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
&& echo "Installing qpdf" \
&& dpkg --install libqpdf29_*.deb \
&& dpkg --install libqpdf-dev_*.deb \
&& echo "Installing Python tools" \
&& python3 -m pip install --no-cache-dir --upgrade \
pip \
wheel \
# https://pikepdf.readthedocs.io/en/latest/installation.html#requirements
pybind11 \
&& echo "Building pikepdf wheel ${PIKEPDF_VERSION}" \
&& mkdir wheels \
&& python3 -m pip wheel \
# Build the package at the required version
pikepdf==${PIKEPDF_VERSION} \
# Look to piwheels for additional pre-built wheels
--extra-index-url https://www.piwheels.org/simple \
# Output the *.whl into this directory
--wheel-dir wheels \
# Do not use a binary packge for the package being built
--no-binary=pikepdf \
# Do use binary packages for dependencies
--prefer-binary \
# Don't cache build files
--no-cache-dir \
&& ls -ahl wheels \
&& echo "Gathering package data" \
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./wheels/pkg-list.txt \
&& echo "Cleaning up image" \
&& apt-get -y purge ${BUILD_PACKAGES} \
&& apt-get -y autoremove --purge \
&& rm -rf /var/lib/apt/lists/*
#
# Stage: package
# Purpose: Holds the compiled .whl files in a tiny image to pull
#
FROM alpine:3.17 as package
WORKDIR /usr/src/wheels/
COPY --from=builder /usr/src/wheels/*.whl ./
COPY --from=builder /usr/src/wheels/pkg-list.txt ./

View File

@@ -1,66 +0,0 @@
# This Dockerfile builds the psycopg2 wheel
# Inputs:
# - PSYCOPG2_VERSION - Version to build
#
# Stage: builder
# Purpose:
# - Build the psycopg2 wheel
#
FROM python:3.9-slim-bullseye as builder
LABEL org.opencontainers.image.description="A intermediate image with psycopg2 wheel built"
ARG PSYCOPG2_VERSION
ARG DEBIAN_FRONTEND=noninteractive
ARG BUILD_PACKAGES="\
build-essential \
python3-dev \
python3-pip \
# https://www.psycopg.org/docs/install.html#prerequisites
libpq-dev"
WORKDIR /usr/src
# As this is an base image for a multi-stage final image
# the added size of the install is basically irrelevant
RUN set -eux \
&& echo "Installing build tools" \
&& apt-get update --quiet \
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
&& echo "Installing Python tools" \
&& python3 -m pip install --no-cache-dir --upgrade pip wheel \
&& echo "Building psycopg2 wheel ${PSYCOPG2_VERSION}" \
&& cd /usr/src \
&& mkdir wheels \
&& python3 -m pip wheel \
# Build the package at the required version
psycopg2==${PSYCOPG2_VERSION} \
# Output the *.whl into this directory
--wheel-dir wheels \
# Do not use a binary packge for the package being built
--no-binary=psycopg2 \
# Do use binary packages for dependencies
--prefer-binary \
# Don't cache build files
--no-cache-dir \
&& ls -ahl wheels/ \
&& echo "Gathering package data" \
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./wheels/pkg-list.txt \
&& echo "Cleaning up image" \
&& apt-get -y purge ${BUILD_PACKAGES} \
&& apt-get -y autoremove --purge \
&& rm -rf /var/lib/apt/lists/*
#
# Stage: package
# Purpose: Holds the compiled .whl files in a tiny image to pull
#
FROM alpine:3.17 as package
WORKDIR /usr/src/wheels/
COPY --from=builder /usr/src/wheels/*.whl ./
COPY --from=builder /usr/src/wheels/pkg-list.txt ./

View File

@@ -1,156 +0,0 @@
#
# Stage: pre-build
# Purpose:
# - Installs common packages
# - Sets common environment variables related to dpkg
# - Aquires the qpdf source from bookwork
# Useful Links:
# - https://qpdf.readthedocs.io/en/stable/installation.html#system-requirements
# - https://wiki.debian.org/Multiarch/HOWTO
# - https://wiki.debian.org/CrossCompiling
#
FROM debian:bullseye-slim as pre-build
ARG QPDF_VERSION
ARG COMMON_BUILD_PACKAGES="\
cmake \
debhelper\
debian-keyring \
devscripts \
dpkg-dev \
equivs \
packaging-dev \
libtool"
ENV DEB_BUILD_OPTIONS="terse nocheck nodoc parallel=2"
WORKDIR /usr/src
RUN set -eux \
&& echo "Installing common packages" \
&& apt-get update --quiet \
&& apt-get install --yes --quiet --no-install-recommends ${COMMON_BUILD_PACKAGES} \
&& echo "Getting qpdf source" \
&& echo "deb-src http://deb.debian.org/debian/ bookworm main" > /etc/apt/sources.list.d/bookworm-src.list \
&& apt-get update --quiet \
&& apt-get source --yes --quiet qpdf=${QPDF_VERSION}-1/bookworm
#
# Stage: amd64-builder
# Purpose: Builds qpdf for x86_64 (native build)
#
FROM pre-build as amd64-builder
ARG AMD64_BUILD_PACKAGES="\
build-essential \
libjpeg62-turbo-dev:amd64 \
libgnutls28-dev:amd64 \
zlib1g-dev:amd64"
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
RUN set -eux \
&& echo "Beginning amd64" \
&& echo "Install amd64 packages" \
&& apt-get update --quiet \
&& apt-get install --yes --quiet --no-install-recommends ${AMD64_BUILD_PACKAGES} \
&& echo "Building amd64" \
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean \
&& echo "Removing debug files" \
&& rm -f ../libqpdf29-dbgsym* \
&& rm -f ../qpdf-dbgsym* \
&& echo "Gathering package data" \
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
#
# Stage: armhf-builder
# Purpose:
# - Sets armhf specific environment
# - Builds qpdf for armhf (cross compile)
#
FROM pre-build as armhf-builder
ARG ARMHF_PACKAGES="\
crossbuild-essential-armhf \
libjpeg62-turbo-dev:armhf \
libgnutls28-dev:armhf \
zlib1g-dev:armhf"
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
ENV CXX="/usr/bin/arm-linux-gnueabihf-g++" \
CC="/usr/bin/arm-linux-gnueabihf-gcc"
RUN set -eux \
&& echo "Beginning armhf" \
&& echo "Install armhf packages" \
&& dpkg --add-architecture armhf \
&& apt-get update --quiet \
&& apt-get install --yes --quiet --no-install-recommends ${ARMHF_PACKAGES} \
&& echo "Building armhf" \
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean --host-arch armhf \
&& echo "Removing debug files" \
&& rm -f ../libqpdf29-dbgsym* \
&& rm -f ../qpdf-dbgsym* \
&& echo "Gathering package data" \
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
#
# Stage: aarch64-builder
# Purpose:
# - Sets aarch64 specific environment
# - Builds qpdf for aarch64 (cross compile)
#
FROM pre-build as aarch64-builder
ARG ARM64_PACKAGES="\
crossbuild-essential-arm64 \
libjpeg62-turbo-dev:arm64 \
libgnutls28-dev:arm64 \
zlib1g-dev:arm64"
ENV CXX="/usr/bin/aarch64-linux-gnu-g++" \
CC="/usr/bin/aarch64-linux-gnu-gcc"
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
RUN set -eux \
&& echo "Beginning arm64" \
&& echo "Install arm64 packages" \
&& dpkg --add-architecture arm64 \
&& apt-get update --quiet \
&& apt-get install --yes --quiet --no-install-recommends ${ARM64_PACKAGES} \
&& echo "Building arm64" \
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean --host-arch arm64 \
&& echo "Removing debug files" \
&& rm -f ../libqpdf29-dbgsym* \
&& rm -f ../qpdf-dbgsym* \
&& echo "Gathering package data" \
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
#
# Stage: package
# Purpose: Holds the compiled .deb files in arch/variant specific folders
#
FROM alpine:3.17 as package
LABEL org.opencontainers.image.description="A image with qpdf installers stored in architecture & version specific folders"
ARG QPDF_VERSION
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/amd64
COPY --from=amd64-builder /usr/src/*.deb ./
COPY --from=amd64-builder /usr/src/pkg-list.txt ./
# Note this is ${TARGETARCH}${TARGETVARIANT} for armv7
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/armv7
COPY --from=armhf-builder /usr/src/*.deb ./
COPY --from=armhf-builder /usr/src/pkg-list.txt ./
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/arm64
COPY --from=aarch64-builder /usr/src/*.deb ./
COPY --from=aarch64-builder /usr/src/pkg-list.txt ./

View File

@@ -1,57 +0,0 @@
# Installer Library
This folder contains the Dockerfiles for building certain installers or libraries, which are then pulled into the main image.
## [jbig2enc](https://github.com/agl/jbig2enc)
### Why
JBIG is an image coding which can achieve better compression of images for PDFs.
### What
The Docker image builds a shared library file and utility, which is copied into the correct location in the final image.
### Updating
1. Ensure the given qpdf version is present in [Debian bookworm](https://packages.debian.org/bookworm/qpdf)
2. Update `.build-config.json` to the given version
3. If the Debian specific version has incremented, update `Dockerfile.qpdf`
See Also:
- [OCRMyPDF Documentation](https://ocrmypdf.readthedocs.io/en/latest/jbig2.html)
## [psycopg2](https://www.psycopg.org/)
### Why
The pre-built wheels of psycopg2 are built on Debian 9, which provides a quite old version of libpq-dev. This causes issue with authentication methods.
### What
The image builds psycopg2 wheels on Debian 10 and places the produced wheels into `/usr/src/wheels/`.
See Also:
- [Issue 266](https://github.com/paperless-ngx/paperless-ngx/issues/266)
## [qpdf](https://qpdf.readthedocs.io/en/stable/index.html)
### Why
qpdf and it's library provide tools to read, manipulate and fix up PDFs. Version 11 is also required by `pikepdf` 6+ and Debian 9 does not provide above version 10.
### What
The Docker image cross compiles .deb installers for each supported architecture of the main image. The installers are placed in `/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/`
## [pikepdf](https://pikepdf.readthedocs.io/en/latest/)
### Why
Required by OCRMyPdf, this is a general purpose library for PDF manipulation in Python via the qpdf libraries.
### What
The built wheels are placed into `/usr/src/wheels/`

View File

@@ -80,7 +80,7 @@ django_checks() {
search_index() {
local -r index_version=4
local -r index_version=5
local -r index_version_file=${DATA_DIR}/.index_version
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then

View File

@@ -15,6 +15,10 @@ do
env_name=${line%%=*}
# Check if it starts with "PAPERLESS_" and ends in "_FILE"
if [[ ${env_name} == PAPERLESS_*_FILE ]]; then
# This should have been named different..
if [[ ${env_name} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" ]]; then
continue
fi
# Extract the value of the environment
env_value=${line#*=}

View File

@@ -1,5 +1,142 @@
# Changelog
## paperless-ngx 1.14.4
### Bug Fixes
- Fix: Inversion in tagged mail searching [@stumpylog](https://github.com/stumpylog) ([#3305](https://github.com/paperless-ngx/paperless-ngx/pull/3305))
- Fix dynamic count labels hidden in light mode [@shamoon](https://github.com/shamoon) ([#3303](https://github.com/paperless-ngx/paperless-ngx/pull/3303))
### All App Changes
<details>
<summary>3 changes</summary>
- New Crowdin updates [@paperlessngx-bot](https://github.com/paperlessngx-bot) ([#3298](https://github.com/paperless-ngx/paperless-ngx/pull/3298))
- Fix: Inversion in tagged mail searching [@stumpylog](https://github.com/stumpylog) ([#3305](https://github.com/paperless-ngx/paperless-ngx/pull/3305))
- Fix dynamic count labels hidden in light mode [@shamoon](https://github.com/shamoon) ([#3303](https://github.com/paperless-ngx/paperless-ngx/pull/3303))
</details>
## paperless-ngx 1.14.3
### Features
- Enhancement: better keyboard nav for filter/edit dropdowns [@shamoon](https://github.com/shamoon) ([#3227](https://github.com/paperless-ngx/paperless-ngx/pull/3227))
### Bug Fixes
- Bump filelock from 3.10.2 to 3.12.0 to fix permissions bug [@rbrownwsws](https://github.com/rbrownwsws) ([#3282](https://github.com/paperless-ngx/paperless-ngx/pull/3282))
- Fix: Handle cases where media files aren't all in the same filesystem [@stumpylog](https://github.com/stumpylog) ([#3261](https://github.com/paperless-ngx/paperless-ngx/pull/3261))
- Fix: Prevent erroneous warning when starting container [@stumpylog](https://github.com/stumpylog) ([#3262](https://github.com/paperless-ngx/paperless-ngx/pull/3262))
- Retain doc changes on tab switch after refresh doc [@shamoon](https://github.com/shamoon) ([#3243](https://github.com/paperless-ngx/paperless-ngx/pull/3243))
- Fix: Don't send Gmail related setting if the server doesn't support it [@stumpylog](https://github.com/stumpylog) ([#3240](https://github.com/paperless-ngx/paperless-ngx/pull/3240))
- Fix: close all docs on logout [@shamoon](https://github.com/shamoon) ([#3232](https://github.com/paperless-ngx/paperless-ngx/pull/3232))
- Fix: Respect superuser for advanced queries, test coverage for object perms [@shamoon](https://github.com/shamoon) ([#3222](https://github.com/paperless-ngx/paperless-ngx/pull/3222))
- Fix: ALLOWED_HOSTS logic being overwritten when \* is set [@ikaruswill](https://github.com/ikaruswill) ([#3218](https://github.com/paperless-ngx/paperless-ngx/pull/3218))
### Dependencies
<details>
<summary>7 changes</summary>
- Bump eslint from 8.38.0 to 8.39.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3276](https://github.com/paperless-ngx/paperless-ngx/pull/3276))
- Bump [@<!---->typescript-eslint/parser from 5.58.0 to 5.59.2 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/parser from 5.58.0 to 5.59.2 in /src-ui @dependabot) ([#3278](https://github.com/paperless-ngx/paperless-ngx/pull/3278))
- Bump [@<!---->types/node from 18.15.11 to 18.16.3 in /src-ui @dependabot](https://github.com/<!---->types/node from 18.15.11 to 18.16.3 in /src-ui @dependabot) ([#3275](https://github.com/paperless-ngx/paperless-ngx/pull/3275))
- Bump rxjs from 7.8.0 to 7.8.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3277](https://github.com/paperless-ngx/paperless-ngx/pull/3277))
- Bump [@<!---->typescript-eslint/eslint-plugin from 5.58.0 to 5.59.2 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/eslint-plugin from 5.58.0 to 5.59.2 in /src-ui @dependabot) ([#3274](https://github.com/paperless-ngx/paperless-ngx/pull/3274))
- Bump cypress from 12.9.0 to 12.11.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3268](https://github.com/paperless-ngx/paperless-ngx/pull/3268))
- Bulk bump angular packages to 15.2.8 in /src-ui [@dependabot](https://github.com/dependabot) ([#3270](https://github.com/paperless-ngx/paperless-ngx/pull/3270))
</details>
### All App Changes
<details>
<summary>14 changes</summary>
- Bump eslint from 8.38.0 to 8.39.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3276](https://github.com/paperless-ngx/paperless-ngx/pull/3276))
- Bump [@<!---->typescript-eslint/parser from 5.58.0 to 5.59.2 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/parser from 5.58.0 to 5.59.2 in /src-ui @dependabot) ([#3278](https://github.com/paperless-ngx/paperless-ngx/pull/3278))
- Bump [@<!---->types/node from 18.15.11 to 18.16.3 in /src-ui @dependabot](https://github.com/<!---->types/node from 18.15.11 to 18.16.3 in /src-ui @dependabot) ([#3275](https://github.com/paperless-ngx/paperless-ngx/pull/3275))
- Bump rxjs from 7.8.0 to 7.8.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3277](https://github.com/paperless-ngx/paperless-ngx/pull/3277))
- Bump [@<!---->typescript-eslint/eslint-plugin from 5.58.0 to 5.59.2 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/eslint-plugin from 5.58.0 to 5.59.2 in /src-ui @dependabot) ([#3274](https://github.com/paperless-ngx/paperless-ngx/pull/3274))
- Bump cypress from 12.9.0 to 12.11.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3268](https://github.com/paperless-ngx/paperless-ngx/pull/3268))
- Bulk bump angular packages to 15.2.8 in /src-ui [@dependabot](https://github.com/dependabot) ([#3270](https://github.com/paperless-ngx/paperless-ngx/pull/3270))
- Fix: Handle cases where media files aren't all in the same filesystem [@stumpylog](https://github.com/stumpylog) ([#3261](https://github.com/paperless-ngx/paperless-ngx/pull/3261))
- Retain doc changes on tab switch after refresh doc [@shamoon](https://github.com/shamoon) ([#3243](https://github.com/paperless-ngx/paperless-ngx/pull/3243))
- Fix: Don't send Gmail related setting if the server doesn't support it [@stumpylog](https://github.com/stumpylog) ([#3240](https://github.com/paperless-ngx/paperless-ngx/pull/3240))
- Fix: close all docs on logout [@shamoon](https://github.com/shamoon) ([#3232](https://github.com/paperless-ngx/paperless-ngx/pull/3232))
- Enhancement: better keyboard nav for filter/edit dropdowns [@shamoon](https://github.com/shamoon) ([#3227](https://github.com/paperless-ngx/paperless-ngx/pull/3227))
- Fix: Respect superuser for advanced queries, test coverage for object perms [@shamoon](https://github.com/shamoon) ([#3222](https://github.com/paperless-ngx/paperless-ngx/pull/3222))
- Fix: ALLOWED_HOSTS logic being overwritten when \* is set [@ikaruswill](https://github.com/ikaruswill) ([#3218](https://github.com/paperless-ngx/paperless-ngx/pull/3218))
</details>
## paperless-ngx 1.14.2
### Features
- Feature: Finnish translation [@shamoon](https://github.com/shamoon) ([#3215](https://github.com/paperless-ngx/paperless-ngx/pull/3215))
### Bug Fixes
- Fix: Load saved views from app frame, not dashboard [@shamoon](https://github.com/shamoon) ([#3211](https://github.com/paperless-ngx/paperless-ngx/pull/3211))
- Fix: advanced search or date searching + doc type/correspondent/storage path broken [@shamoon](https://github.com/shamoon) ([#3209](https://github.com/paperless-ngx/paperless-ngx/pull/3209))
- Fix MixedContentTypeError in add_inbox_tags handler [@e1mo](https://github.com/e1mo) ([#3212](https://github.com/paperless-ngx/paperless-ngx/pull/3212))
### All App Changes
<details>
<summary>4 changes</summary>
- Feature: Finnish translation [@shamoon](https://github.com/shamoon) ([#3215](https://github.com/paperless-ngx/paperless-ngx/pull/3215))
- Fix: Load saved views from app frame, not dashboard [@shamoon](https://github.com/shamoon) ([#3211](https://github.com/paperless-ngx/paperless-ngx/pull/3211))
- Fix: advanced search or date searching + doc type/correspondent/storage path broken [@shamoon](https://github.com/shamoon) ([#3209](https://github.com/paperless-ngx/paperless-ngx/pull/3209))
- Fix MixedContentTypeError in add_inbox_tags handler [@e1mo](https://github.com/e1mo) ([#3212](https://github.com/paperless-ngx/paperless-ngx/pull/3212))
</details>
## paperless-ngx 1.14.1
### Bug Fixes
- Fix: reduce frequency of permissions queries to speed up v1.14.0 [@shamoon](https://github.com/shamoon) ([#3201](https://github.com/paperless-ngx/paperless-ngx/pull/3201))
- Fix: permissions-aware statistics [@shamoon](https://github.com/shamoon) ([#3199](https://github.com/paperless-ngx/paperless-ngx/pull/3199))
- Fix: Use document owner for matching if set [@shamoon](https://github.com/shamoon) ([#3198](https://github.com/paperless-ngx/paperless-ngx/pull/3198))
- Fix: respect permissions on document view actions [@shamoon](https://github.com/shamoon) ([#3174](https://github.com/paperless-ngx/paperless-ngx/pull/3174))
- Increment API version for 1.14.1+ [@shamoon](https://github.com/shamoon) ([#3191](https://github.com/paperless-ngx/paperless-ngx/pull/3191))
- Fix: dropdown Private items with empty set [@shamoon](https://github.com/shamoon) ([#3189](https://github.com/paperless-ngx/paperless-ngx/pull/3189))
- Documentation: add note for macOS [@shamoon](https://github.com/shamoon) ([#3190](https://github.com/paperless-ngx/paperless-ngx/pull/3190))
- Fix: make the importer a little more robust against some errors [@stumpylog](https://github.com/stumpylog) ([#3188](https://github.com/paperless-ngx/paperless-ngx/pull/3188))
- Fix: Specify backend for auto-login [@shamoon](https://github.com/shamoon) ([#3163](https://github.com/paperless-ngx/paperless-ngx/pull/3163))
- Fix: StoragePath missing the owned or granted filter [@stumpylog](https://github.com/stumpylog) ([#3180](https://github.com/paperless-ngx/paperless-ngx/pull/3180))
- Fix: Redis socket connections fail due to redis-py [@stumpylog](https://github.com/stumpylog) ([#3176](https://github.com/paperless-ngx/paperless-ngx/pull/3176))
- Fix: Handle delete mail action with no filters [@shamoon](https://github.com/shamoon) ([#3161](https://github.com/paperless-ngx/paperless-ngx/pull/3161))
- Fix typos and wrong version number in doc [@FizzyMUC](https://github.com/FizzyMUC) ([#3171](https://github.com/paperless-ngx/paperless-ngx/pull/3171))
### Documentation
- Documentation: add note for macOS [@shamoon](https://github.com/shamoon) ([#3190](https://github.com/paperless-ngx/paperless-ngx/pull/3190))
- Fix typos and wrong version number in doc [@FizzyMUC](https://github.com/FizzyMUC) ([#3171](https://github.com/paperless-ngx/paperless-ngx/pull/3171))
### Maintenance
- Chore: Fix isort not running, upgrade to the latest black [@stumpylog](https://github.com/stumpylog) ([#3177](https://github.com/paperless-ngx/paperless-ngx/pull/3177))
### All App Changes
<details>
<summary>11 changes</summary>
- Fix: reduce frequency of permissions queries to speed up v1.14.0 [@shamoon](https://github.com/shamoon) ([#3201](https://github.com/paperless-ngx/paperless-ngx/pull/3201))
- Fix: permissions-aware statistics [@shamoon](https://github.com/shamoon) ([#3199](https://github.com/paperless-ngx/paperless-ngx/pull/3199))
- Fix: Use document owner for matching if set [@shamoon](https://github.com/shamoon) ([#3198](https://github.com/paperless-ngx/paperless-ngx/pull/3198))
- Chore: Fix isort not running, upgrade to the latest black [@stumpylog](https://github.com/stumpylog) ([#3177](https://github.com/paperless-ngx/paperless-ngx/pull/3177))
- Fix: respect permissions on document view actions [@shamoon](https://github.com/shamoon) ([#3174](https://github.com/paperless-ngx/paperless-ngx/pull/3174))
- Increment API version for 1.14.1+ [@shamoon](https://github.com/shamoon) ([#3191](https://github.com/paperless-ngx/paperless-ngx/pull/3191))
- Fix: dropdown Private items with empty set [@shamoon](https://github.com/shamoon) ([#3189](https://github.com/paperless-ngx/paperless-ngx/pull/3189))
- Fix: make the importer a little more robust against some errors [@stumpylog](https://github.com/stumpylog) ([#3188](https://github.com/paperless-ngx/paperless-ngx/pull/3188))
- Fix: Specify backend for auto-login [@shamoon](https://github.com/shamoon) ([#3163](https://github.com/paperless-ngx/paperless-ngx/pull/3163))
- Fix: StoragePath missing the owned or granted filter [@stumpylog](https://github.com/stumpylog) ([#3180](https://github.com/paperless-ngx/paperless-ngx/pull/3180))
- Fix: Handle delete mail action with no filters [@shamoon](https://github.com/shamoon) ([#3161](https://github.com/paperless-ngx/paperless-ngx/pull/3161))
</details>
## paperless-ngx 1.14.0
### Notable Changes

View File

@@ -322,8 +322,7 @@ You can read more about this in [the Django project's documentation](https://doc
Can also be set using PAPERLESS_URL (see above).
If manually set, please remember to include "localhost". Otherwise
docker healthcheck will fail.
"localhost" is always allowed for docker healthcheck
Defaults to "\*", which is all hosts.

View File

@@ -374,13 +374,10 @@ If you want to build the documentation locally, this is how you do it:
The docker image is primarily built by the GitHub actions workflow, but
it can be faster when developing to build and tag an image locally.
To provide the build arguments automatically, build the image using the
helper script `build-docker-image.sh`.
Building the image works as with any image:
Building the docker image from source:
```bash
./build-docker-image.sh Dockerfile -t <your-tag>
```
docker build --file Dockerfile --tag paperless:local --progress simple .
```
## Extending Paperless-ngx

View File

@@ -24,6 +24,7 @@
"de-DE": "src/locale/messages.de_DE.xlf",
"en-GB": "src/locale/messages.en_GB.xlf",
"es-ES": "src/locale/messages.es_ES.xlf",
"fi-FI": "src/locale/messages.fi_FI.xlf",
"fr-FR": "src/locale/messages.fr_FR.xlf",
"it-IT": "src/locale/messages.it_IT.xlf",
"lb-LU": "src/locale/messages.lb_LU.xlf",

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

378
src-ui/package-lock.json generated
View File

@@ -8,14 +8,14 @@
"name": "paperless-ui",
"version": "0.0.0",
"dependencies": {
"@angular/common": "~15.2.7",
"@angular/compiler": "~15.2.7",
"@angular/core": "~15.2.7",
"@angular/forms": "~15.2.7",
"@angular/localize": "~15.2.7",
"@angular/platform-browser": "~15.2.7",
"@angular/platform-browser-dynamic": "~15.2.7",
"@angular/router": "~15.2.7",
"@angular/common": "~15.2.8",
"@angular/compiler": "~15.2.8",
"@angular/core": "~15.2.8",
"@angular/forms": "~15.2.8",
"@angular/localize": "~15.2.8",
"@angular/platform-browser": "~15.2.8",
"@angular/platform-browser-dynamic": "~15.2.8",
"@angular/router": "~15.2.8",
"@ng-bootstrap/ng-bootstrap": "^14.1.0",
"@ng-select/ng-select": "^10.0.4",
"@ngneat/dirty-check-forms": "^3.0.3",
@@ -28,7 +28,7 @@
"ngx-cookie-service": "^15.0.0",
"ngx-file-drop": "^15.0.0",
"ngx-ui-tour-ng-bootstrap": "^12.6.0",
"rxjs": "^7.8.0",
"rxjs": "^7.8.1",
"tslib": "^2.4.1",
"uuid": "^9.0.0",
"zone.js": "^0.13.0"
@@ -41,14 +41,14 @@
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "~15.2.6",
"@angular/compiler-cli": "~15.2.7",
"@angular/cli": "~15.2.7",
"@angular/compiler-cli": "~15.2.8",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"@types/node": "^18.16.3",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"concurrently": "^8.0.1",
"eslint": "^8.38.0",
"eslint": "^8.39.0",
"jest": "28.1.3",
"jest-environment-jsdom": "^29.5.0",
"jest-preset-angular": "^12.2.6",
@@ -58,7 +58,7 @@
},
"optionalDependencies": {
"@cypress/schematic": "^2.1.1",
"cypress": "^12.9.0"
"cypress": "^12.11.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -306,12 +306,12 @@
}
},
"node_modules/@angular-devkit/architect": {
"version": "0.1502.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.6.tgz",
"integrity": "sha512-n4oJ9vzFWwabf+AfgqqevVzdJhNKNCav7ytefjD/Y01vkNwlXqWnHcvyyHCLkVibJ6WR8J9lK4t77j/HFlDvWQ==",
"version": "0.1502.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.7.tgz",
"integrity": "sha512-MzB6D/yUo6cBJfQ31zNDHJ3C3iKmBtxP3i9WIRnnkZwS1VUfO8OX3TZ6lycYbREF1oL/AQ/r9GK+KA5DNEBSAw==",
"devOptional": true,
"dependencies": {
"@angular-devkit/core": "15.2.6",
"@angular-devkit/core": "15.2.7",
"rxjs": "6.6.7"
},
"engines": {
@@ -339,15 +339,15 @@
"devOptional": true
},
"node_modules/@angular-devkit/build-angular": {
"version": "15.2.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.6.tgz",
"integrity": "sha512-OmMcdXXUrAdZNxwxDE8SUx1FMcq9FyMnrSv1PmP9sHPBoxAdBVc/qNdGA9V7C5yHvWHGgzsx7ZK5TDuvifzS5g==",
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.7.tgz",
"integrity": "sha512-zZ+tlt5aNGY9APUdjQHeVFJpVLeixlZRNHmfdXD+rN4WR2q9E0pTvLUThrkOmO8YrVyGbdvcw1O7XNdL+3b02w==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "2.2.0",
"@angular-devkit/architect": "0.1502.6",
"@angular-devkit/build-webpack": "0.1502.6",
"@angular-devkit/core": "15.2.6",
"@angular-devkit/architect": "0.1502.7",
"@angular-devkit/build-webpack": "0.1502.7",
"@angular-devkit/core": "15.2.7",
"@babel/core": "7.20.12",
"@babel/generator": "7.20.14",
"@babel/helper-annotate-as-pure": "7.18.6",
@@ -359,7 +359,7 @@
"@babel/runtime": "7.20.13",
"@babel/template": "7.20.7",
"@discoveryjs/json-ext": "0.5.7",
"@ngtools/webpack": "15.2.6",
"@ngtools/webpack": "15.2.7",
"ansi-colors": "4.1.3",
"autoprefixer": "10.4.13",
"babel-loader": "9.1.2",
@@ -479,12 +479,12 @@
"dev": true
},
"node_modules/@angular-devkit/build-webpack": {
"version": "0.1502.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.6.tgz",
"integrity": "sha512-X7XQ11QDz2Bs5qpJ3a5glIytvI+S74ORQxdzvT6a6KB8ayW0SgZEhTwD+GF7pa5My8draIaXBGzzQR1qmpWK5Q==",
"version": "0.1502.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.7.tgz",
"integrity": "sha512-sNE4t4shSwxagqm+jdojbkYfuo/CHNMi4faItDWTTsCOf9wQxCxV4Waxee4akAkv3K6fzrnZy3ad/oQQMUl0Iw==",
"dev": true,
"dependencies": {
"@angular-devkit/architect": "0.1502.6",
"@angular-devkit/architect": "0.1502.7",
"rxjs": "6.6.7"
},
"engines": {
@@ -516,9 +516,9 @@
"dev": true
},
"node_modules/@angular-devkit/core": {
"version": "15.2.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.6.tgz",
"integrity": "sha512-YVTWZ+M+xNKdFX4EnY9QX49PZraawiaA0iTd2CUW8ZoTUvU7yOGMKZLSdz6aokTMRVfm0449wt6YL994ibOo1g==",
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.7.tgz",
"integrity": "sha512-k2MKUm4ygTD9+89neqMmBphDr0o8Tp9RtgfzbS8VHgGkGYlbu0KPsxHyHB3Mvzl1EkSz6EHyrU3t89m+Rcj1lw==",
"devOptional": true,
"dependencies": {
"ajv": "8.12.0",
@@ -560,12 +560,12 @@
"devOptional": true
},
"node_modules/@angular-devkit/schematics": {
"version": "15.2.6",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.6.tgz",
"integrity": "sha512-f7VgnAcok7AwR/DhX0ZWskB0rFBo/KsvtIUA2qZSrpKMf8eFiwu03dv/b2mI0vnf+1FBfIQzJvO0ww45zRp6dA==",
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.7.tgz",
"integrity": "sha512-umQ+SgEMjqPHimHOBVhDn5NNGVoMLKQkI2fwbENXV72BqQqdh1K3D4QSNlUXitTaH0NEZZaAawE1vZHzzeAoNA==",
"devOptional": true,
"dependencies": {
"@angular-devkit/core": "15.2.6",
"@angular-devkit/core": "15.2.7",
"jsonc-parser": "3.2.0",
"magic-string": "0.29.0",
"ora": "5.4.1",
@@ -700,15 +700,15 @@
}
},
"node_modules/@angular/cli": {
"version": "15.2.6",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.2.6.tgz",
"integrity": "sha512-wNkQ/qCVbd4pERaGVagKJPifEvjRNY5otwsd4iRVubY/XOcIHcYChUThZwgQdVfNAImfJPMZNrhbGxejuWLA9w==",
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-15.2.7.tgz",
"integrity": "sha512-gGUIjaVN//bO72zRK3GNcCRVeism56BCRfkXSywKedCWFK4IZsatIL1IXT6OiJC22NsUCMaAFPD0wygSUCZaig==",
"devOptional": true,
"dependencies": {
"@angular-devkit/architect": "0.1502.6",
"@angular-devkit/core": "15.2.6",
"@angular-devkit/schematics": "15.2.6",
"@schematics/angular": "15.2.6",
"@angular-devkit/architect": "0.1502.7",
"@angular-devkit/core": "15.2.7",
"@angular-devkit/schematics": "15.2.7",
"@schematics/angular": "15.2.7",
"@yarnpkg/lockfile": "1.1.0",
"ansi-colors": "4.1.3",
"ini": "3.0.1",
@@ -734,9 +734,9 @@
}
},
"node_modules/@angular/common": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-15.2.7.tgz",
"integrity": "sha512-CbmrQeZ0yChQrF/ab3v+gv6x2uLbv/s1wZNUBSO/p1STz6BZzHRJqObVlfPlQvyBx5btBBy/+I1sUh1yumARDA==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-15.2.8.tgz",
"integrity": "sha512-yLDQihiRcVl38HrWMPbqgzOaSUw85AQH5BsGdjbS6BpoBQj3EXOpccCMFsuxOKxPG4toatgawNqrEnK0Jpv9Mw==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -744,14 +744,14 @@
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/core": "15.2.7",
"@angular/core": "15.2.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-15.2.7.tgz",
"integrity": "sha512-SesyYI2ExUa13XukXgIsmfg3ar90HbWeWDJTgmzsIfph0M9t6+SaPGpf3FCtdBgNADIpUFp3cieCOJgLESzxYQ==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-15.2.8.tgz",
"integrity": "sha512-+dvspIDvuGoYqdL7r/3o9ojkR3fH1zevgC0ISJivcIrMi+WcJ0FV2JmJdnm8V52oNsHy+sMF9eEZGEbCbACE/A==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -759,7 +759,7 @@
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/core": "15.2.7"
"@angular/core": "15.2.8"
},
"peerDependenciesMeta": {
"@angular/core": {
@@ -768,9 +768,9 @@
}
},
"node_modules/@angular/compiler-cli": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-15.2.7.tgz",
"integrity": "sha512-4v51dOaT8GDUzRh6+mCLZOaYuU9FYX6vOHaLod9np3tVWPhcpoF2ZklRSiQDeFqrhr5B4vuCp/Lh9N2wzc22XQ==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-15.2.8.tgz",
"integrity": "sha512-fFxaDlbILo0t2t662qA0cjgn+kWItGlc1tFYKU6X7bvYb3t2e0cd9FzrFPLXUQVboGis83ULcJ2zkDxScnuPuQ==",
"dependencies": {
"@babel/core": "7.19.3",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -792,7 +792,7 @@
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/compiler": "15.2.7",
"@angular/compiler": "15.2.8",
"typescript": ">=4.8.2 <5.0"
}
},
@@ -834,9 +834,9 @@
}
},
"node_modules/@angular/core": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-15.2.7.tgz",
"integrity": "sha512-iS7JCJubRFqdndoUdAnvNkQRT3tY5tNFupBQS/sytkwxVrdBg+Is5jpdgk741n824vTMsE+CnuY0SETar8rN6g==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-15.2.8.tgz",
"integrity": "sha512-NDs+g4uM4EhyCvluf8a0YBCFXsDAEfCMHOD5cS00Bl+liTQ7JwtmepkWXMyjLB92irC9JaR79kdy4BoIKOh8WA==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -849,9 +849,9 @@
}
},
"node_modules/@angular/forms": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-15.2.7.tgz",
"integrity": "sha512-rzrebDIrtxxOeMcBzRBxqaOBZ+T1DJrysG/6YWZy428W/Z3MfPxUarPxgfx/oZI+x5uUsDaZmyoRdhVPJ2KhZg==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-15.2.8.tgz",
"integrity": "sha512-VyevVj20DdQWjAQUyiFTe+DAzqG9GqfAOWn376Y/lhPcwxAojXePTGNgtQud566/urDrNrP5haaLD6O36/3n+w==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -859,16 +859,16 @@
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/common": "15.2.7",
"@angular/core": "15.2.7",
"@angular/platform-browser": "15.2.7",
"@angular/common": "15.2.8",
"@angular/core": "15.2.8",
"@angular/platform-browser": "15.2.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/localize": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-15.2.7.tgz",
"integrity": "sha512-ySuy35QKApWH9sW3PfnAAnZjLl3NT+SacvlEWigrTeCqfBEuDPUG57ugvc1/Lzuo09UOh3HQkrQBbdWAILd8JA==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-15.2.8.tgz",
"integrity": "sha512-wJLBp0MUnET9kHzBtqIlZ3RQ56JFItXSgmBXagQq+MU+uJZmGvuw6fez0i5wkgv9Rgnr25oCULVtpTF+T5RGYA==",
"dependencies": {
"@babel/core": "7.19.3",
"glob": "8.1.0",
@@ -883,8 +883,8 @@
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/compiler": "15.2.7",
"@angular/compiler-cli": "15.2.7"
"@angular/compiler": "15.2.8",
"@angular/compiler-cli": "15.2.8"
}
},
"node_modules/@angular/localize/node_modules/@babel/core": {
@@ -925,9 +925,9 @@
}
},
"node_modules/@angular/platform-browser": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-15.2.7.tgz",
"integrity": "sha512-aCbd7xyuP7c2eDITkOTDO2mqP550WHCBN8U6VnjysqtB5ocbJtR6z/MIRItN/Zx+xj3piiaKei//XIkb3Q5fXQ==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-15.2.8.tgz",
"integrity": "sha512-8sKFUld54inj0FnQ1ydhFxnDgsbbf43W9FALye/5uEtLgwwE/ZvkNYMaQ7hq1JPuQRMDj3gJkFqaLeFjplpHDA==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -935,9 +935,9 @@
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/animations": "15.2.7",
"@angular/common": "15.2.7",
"@angular/core": "15.2.7"
"@angular/animations": "15.2.8",
"@angular/common": "15.2.8",
"@angular/core": "15.2.8"
},
"peerDependenciesMeta": {
"@angular/animations": {
@@ -946,9 +946,9 @@
}
},
"node_modules/@angular/platform-browser-dynamic": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.7.tgz",
"integrity": "sha512-t1Nf7hgbcYvhmxuzgUtsV47jrI5CXUBqrtz5I0ilWG92zZTig5qvfd1/2Ub8NHz87uHNrnggyZpL2+4MJ26nyQ==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.8.tgz",
"integrity": "sha512-75HyoZNibA3u/FvdK4Aw5KMzUmS/nDk5N8s7gfM09fe1resSPgFiW8JJEkr1xiUdA2WtSRbHs34y5rHLDe7n1Q==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -956,16 +956,16 @@
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/common": "15.2.7",
"@angular/compiler": "15.2.7",
"@angular/core": "15.2.7",
"@angular/platform-browser": "15.2.7"
"@angular/common": "15.2.8",
"@angular/compiler": "15.2.8",
"@angular/core": "15.2.8",
"@angular/platform-browser": "15.2.8"
}
},
"node_modules/@angular/router": {
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-15.2.7.tgz",
"integrity": "sha512-Wkk+oJSUrVafJjmv9uE1SoY4wDE9bjX7ald+UXePz+QyM/PFoLkm/CzLYjFBkJnsOkOVxw1VmvacoUjWN6BCTQ==",
"version": "15.2.8",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-15.2.8.tgz",
"integrity": "sha512-C62QBEeJSBTNTrQHZiklPrxwJwuENoZzWX22MMJ7dxl+7VjRgnmj8J7mcX9fLjHlL+mC3RvesMlX7sGZRQV1cg==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -973,9 +973,9 @@
"node": "^14.20.0 || ^16.13.0 || >=18.10.0"
},
"peerDependencies": {
"@angular/common": "15.2.7",
"@angular/core": "15.2.7",
"@angular/platform-browser": "15.2.7",
"@angular/common": "15.2.8",
"@angular/core": "15.2.8",
"@angular/platform-browser": "15.2.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@@ -3262,9 +3262,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz",
"integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz",
"integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -4119,9 +4119,9 @@
}
},
"node_modules/@ngtools/webpack": {
"version": "15.2.6",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.6.tgz",
"integrity": "sha512-I+kekKItfsCLdX+ZjjmsWqd0AyoYGTQPjlbQAiPtmdH73/rfPOF4Q/3AU4tzTdn0n0GXqZWv6VOs91w99ydi0A==",
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.7.tgz",
"integrity": "sha512-iUCSR03PzGSpwwZ5soioTIWsTPBayzkZfhKMkfz1RqtkbcxC4I07NRoQ1djofhsYyW2I1n7XS8w3K7NILtN3gQ==",
"dev": true,
"engines": {
"node": "^14.20.0 || ^16.13.0 || >=18.10.0",
@@ -4332,13 +4332,13 @@
}
},
"node_modules/@schematics/angular": {
"version": "15.2.6",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.2.6.tgz",
"integrity": "sha512-OcBUvVAxZEMBX+fi0ytybeAdmStra+GwtlvipS70yOxcAgJ84ZrnZGN7a072cCVQcq7AgqUfssnyqCx1wu+yCg==",
"version": "15.2.7",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-15.2.7.tgz",
"integrity": "sha512-5fC6Es6HWpvmCnpPwTxHQq6KQuxtPaheFgoElHJM6uBgJDTr993MIw/3FsZvqLkO9hv/yWbr4gilqjEoesJSWg==",
"devOptional": true,
"dependencies": {
"@angular-devkit/core": "15.2.6",
"@angular-devkit/schematics": "15.2.6",
"@angular-devkit/core": "15.2.7",
"@angular-devkit/schematics": "15.2.7",
"jsonc-parser": "3.2.0"
},
"engines": {
@@ -4495,9 +4495,9 @@
}
},
"node_modules/@types/connect-history-api-fallback": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz",
"integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz",
"integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==",
"dev": true,
"dependencies": {
"@types/express-serve-static-core": "*",
@@ -4543,14 +4543,15 @@
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.17.33",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz",
"integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==",
"version": "4.17.34",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz",
"integrity": "sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/graceful-fs": {
@@ -4563,9 +4564,9 @@
}
},
"node_modules/@types/http-proxy": {
"version": "1.17.10",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz",
"integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==",
"version": "1.17.11",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz",
"integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -4891,15 +4892,15 @@
"dev": true
},
"node_modules/@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
"integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"node_modules/@types/node": {
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
"version": "18.16.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz",
"integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==",
"devOptional": true
},
"node_modules/@types/parse-json": {
@@ -4944,6 +4945,16 @@
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
"node_modules/@types/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz",
"integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==",
"dev": true,
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-index": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz",
@@ -5030,15 +5041,15 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.58.0.tgz",
"integrity": "sha512-vxHvLhH0qgBd3/tW6/VccptSfc8FxPQIkmNTVLWcCOVqSBvqpnKkBTYrhcGlXfSnd78azwe+PsjYFj0X34/njA==",
"version": "5.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz",
"integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.58.0",
"@typescript-eslint/type-utils": "5.58.0",
"@typescript-eslint/utils": "5.58.0",
"@typescript-eslint/scope-manager": "5.59.2",
"@typescript-eslint/type-utils": "5.59.2",
"@typescript-eslint/utils": "5.59.2",
"debug": "^4.3.4",
"grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0",
@@ -5064,13 +5075,13 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.58.0.tgz",
"integrity": "sha512-FF5vP/SKAFJ+LmR9PENql7fQVVgGDOS+dq3j+cKl9iW/9VuZC/8CFmzIP0DLKXfWKpRHawJiG70rVH+xZZbp8w==",
"version": "5.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz",
"integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "5.58.0",
"@typescript-eslint/utils": "5.58.0",
"@typescript-eslint/typescript-estree": "5.59.2",
"@typescript-eslint/utils": "5.59.2",
"debug": "^4.3.4",
"tsutils": "^3.21.0"
},
@@ -5091,17 +5102,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz",
"integrity": "sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ==",
"version": "5.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz",
"integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@types/json-schema": "^7.0.9",
"@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.58.0",
"@typescript-eslint/types": "5.58.0",
"@typescript-eslint/typescript-estree": "5.58.0",
"@typescript-eslint/scope-manager": "5.59.2",
"@typescript-eslint/types": "5.59.2",
"@typescript-eslint/typescript-estree": "5.59.2",
"eslint-scope": "^5.1.1",
"semver": "^7.3.7"
},
@@ -5139,14 +5150,14 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.58.0.tgz",
"integrity": "sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==",
"version": "5.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz",
"integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.58.0",
"@typescript-eslint/types": "5.58.0",
"@typescript-eslint/typescript-estree": "5.58.0",
"@typescript-eslint/scope-manager": "5.59.2",
"@typescript-eslint/types": "5.59.2",
"@typescript-eslint/typescript-estree": "5.59.2",
"debug": "^4.3.4"
},
"engines": {
@@ -5166,13 +5177,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz",
"integrity": "sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==",
"version": "5.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz",
"integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.58.0",
"@typescript-eslint/visitor-keys": "5.58.0"
"@typescript-eslint/types": "5.59.2",
"@typescript-eslint/visitor-keys": "5.59.2"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5267,9 +5278,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz",
"integrity": "sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==",
"version": "5.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz",
"integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5280,13 +5291,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz",
"integrity": "sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==",
"version": "5.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz",
"integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.58.0",
"@typescript-eslint/visitor-keys": "5.58.0",
"@typescript-eslint/types": "5.59.2",
"@typescript-eslint/visitor-keys": "5.59.2",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -5429,12 +5440,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "5.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz",
"integrity": "sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==",
"version": "5.59.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz",
"integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.58.0",
"@typescript-eslint/types": "5.59.2",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
@@ -6988,9 +6999,9 @@
}
},
"node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"optional": true,
"engines": {
"node": ">= 6"
@@ -7563,9 +7574,9 @@
"dev": true
},
"node_modules/cypress": {
"version": "12.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.9.0.tgz",
"integrity": "sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg==",
"version": "12.11.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.11.0.tgz",
"integrity": "sha512-TJE+CCWI26Hwr5Msb9GpQhFLubdYooW0fmlPwTsfiyxmngqc7+SZGLPeIkj2dTSSZSEtpQVzOzvcnzH0o8G7Vw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@@ -7583,7 +7594,7 @@
"check-more-types": "^2.24.0",
"cli-cursor": "^3.1.0",
"cli-table3": "~0.6.1",
"commander": "^5.1.0",
"commander": "^6.2.1",
"common-tags": "^1.8.0",
"dayjs": "^1.10.4",
"debug": "^4.3.4",
@@ -7601,7 +7612,7 @@
"listr2": "^3.8.3",
"lodash": "^4.17.21",
"log-symbols": "^4.0.0",
"minimist": "^1.2.6",
"minimist": "^1.2.8",
"ospath": "^1.2.2",
"pretty-bytes": "^5.6.0",
"proxy-from-env": "1.0.0",
@@ -8013,9 +8024,9 @@
"dev": true
},
"node_modules/dns-packet": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.5.0.tgz",
"integrity": "sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz",
"integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==",
"dev": true,
"dependencies": {
"@leichtgewicht/ip-codec": "^2.0.1"
@@ -8447,15 +8458,15 @@
}
},
"node_modules/eslint": {
"version": "8.38.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz",
"integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz",
"integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.2",
"@eslint/js": "8.38.0",
"@eslint/js": "8.39.0",
"@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -8465,7 +8476,7 @@
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.1.1",
"eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.0",
"espree": "^9.5.1",
"esquery": "^1.4.2",
@@ -8504,9 +8515,9 @@
}
},
"node_modules/eslint-scope": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
"integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
@@ -8514,6 +8525,9 @@
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-utils": {
@@ -14071,9 +14085,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"devOptional": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -16249,9 +16263,9 @@
}
},
"node_modules/rxjs": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dependencies": {
"tslib": "^2.1.0"
}

View File

@@ -13,14 +13,14 @@
},
"private": true,
"dependencies": {
"@angular/common": "~15.2.7",
"@angular/compiler": "~15.2.7",
"@angular/core": "~15.2.7",
"@angular/forms": "~15.2.7",
"@angular/localize": "~15.2.7",
"@angular/platform-browser": "~15.2.7",
"@angular/platform-browser-dynamic": "~15.2.7",
"@angular/router": "~15.2.7",
"@angular/common": "~15.2.8",
"@angular/compiler": "~15.2.8",
"@angular/core": "~15.2.8",
"@angular/forms": "~15.2.8",
"@angular/localize": "~15.2.8",
"@angular/platform-browser": "~15.2.8",
"@angular/platform-browser-dynamic": "~15.2.8",
"@angular/router": "~15.2.8",
"@ng-bootstrap/ng-bootstrap": "^14.1.0",
"@ng-select/ng-select": "^10.0.4",
"@ngneat/dirty-check-forms": "^3.0.3",
@@ -33,7 +33,7 @@
"ngx-cookie-service": "^15.0.0",
"ngx-file-drop": "^15.0.0",
"ngx-ui-tour-ng-bootstrap": "^12.6.0",
"rxjs": "^7.8.0",
"rxjs": "^7.8.1",
"tslib": "^2.4.1",
"uuid": "^9.0.0",
"zone.js": "^0.13.0"
@@ -46,14 +46,14 @@
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "~15.2.6",
"@angular/compiler-cli": "~15.2.7",
"@angular/cli": "~15.2.7",
"@angular/compiler-cli": "~15.2.8",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"@types/node": "^18.16.3",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"concurrently": "^8.0.1",
"eslint": "^8.38.0",
"eslint": "^8.39.0",
"jest": "28.1.3",
"jest-environment-jsdom": "^29.5.0",
"jest-preset-angular": "^12.2.6",
@@ -63,6 +63,6 @@
},
"optionalDependencies": {
"@cypress/schematic": "^2.1.1",
"cypress": "^12.9.0"
"cypress": "^12.11.0"
}
}

View File

@@ -2,7 +2,7 @@ import { SettingsService } from './services/settings.service'
import { SETTINGS_KEYS } from './data/paperless-uisettings'
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
import { Router } from '@angular/router'
import { Subscription } from 'rxjs'
import { Subscription, first } from 'rxjs'
import { ConsumerStatusService } from './services/consumer-status.service'
import { ToastService } from './services/toast.service'
import { NgxFileDropEntry } from 'ngx-file-drop'
@@ -240,13 +240,14 @@ export class AppComponent implements OnInit, OnDestroy {
this.tourService.start$.subscribe(() => {
this.renderer.addClass(document.body, 'tour-active')
})
this.tourService.end$.subscribe(() => {
// animation time
setTimeout(() => {
this.renderer.removeClass(document.body, 'tour-active')
}, 500)
this.tourService.end$.pipe(first()).subscribe(() => {
this.settings.completeTour()
// animation time
setTimeout(() => {
this.renderer.removeClass(document.body, 'tour-active')
}, 500)
})
})
}

View File

@@ -88,6 +88,10 @@ import { PermissionsUserComponent } from './components/common/input/permissions/
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
import { IfOwnerDirective } from './directives/if-owner.directive'
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
import { PermissionsFilterDropdownComponent } from './components/common/permissions-filter-dropdown/permissions-filter-dropdown.component'
import { UsernamePipe } from './pipes/username.pipe'
import localeAr from '@angular/common/locales/ar'
import localeBe from '@angular/common/locales/be'
@@ -97,6 +101,7 @@ import localeDa from '@angular/common/locales/da'
import localeDe from '@angular/common/locales/de'
import localeEnGb from '@angular/common/locales/en-GB'
import localeEs from '@angular/common/locales/es'
import localeFi from '@angular/common/locales/fi'
import localeFr from '@angular/common/locales/fr'
import localeIt from '@angular/common/locales/it'
import localeLb from '@angular/common/locales/lb'
@@ -110,8 +115,6 @@ import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr'
import localeZh from '@angular/common/locales/zh'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
registerLocaleData(localeAr)
registerLocaleData(localeBe)
@@ -121,6 +124,7 @@ registerLocaleData(localeDa)
registerLocaleData(localeDe)
registerLocaleData(localeEnGb)
registerLocaleData(localeEs)
registerLocaleData(localeFi)
registerLocaleData(localeFr)
registerLocaleData(localeIt)
registerLocaleData(localeLb)
@@ -211,6 +215,8 @@ function initializeApp(settings: SettingsService) {
IfObjectPermissionsDirective,
PermissionsDialogComponent,
PermissionsFormComponent,
PermissionsFilterDropdownComponent,
UsernamePipe,
],
imports: [
BrowserModule,
@@ -251,6 +257,7 @@ function initializeApp(settings: SettingsService) {
PermissionsGuard,
DirtyDocGuard,
DirtySavedViewGuard,
UsernamePipe,
],
bootstrap: [AppComponent],
})

View File

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

View File

@@ -27,6 +27,11 @@ import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { ToastService } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import {
PermissionAction,
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
@Component({
selector: 'app-app-frame',
@@ -47,9 +52,19 @@ export class AppFrameComponent
private list: DocumentListViewService,
public settingsService: SettingsService,
public tasksService: TasksService,
private readonly toastService: ToastService
private readonly toastService: ToastService,
private permissionsService: PermissionsService
) {
super()
if (
permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.SavedView
)
) {
savedViewService.initialize()
}
}
ngOnInit(): void {
@@ -228,4 +243,8 @@ export class AppFrameComponent
this.checkForUpdates()
}
}
onLogout() {
this.openDocumentsService.closeAll()
}
}

View File

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

View File

@@ -6,6 +6,7 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@Component({
selector: 'app-correspondent-edit-dialog',
@@ -16,9 +17,10 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
constructor(
service: CorrespondentService,
activeModal: NgbActiveModal,
userService: UserService
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService)
super(service, activeModal, userService, settingsService)
}
getCreateTitle() {

View File

@@ -6,6 +6,7 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@Component({
selector: 'app-document-type-edit-dialog',
@@ -16,9 +17,10 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
constructor(
service: DocumentTypeService,
activeModal: NgbActiveModal,
userService: UserService
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService)
super(service, activeModal, userService, settingsService)
}
getCreateTitle() {

View File

@@ -13,6 +13,7 @@ import { PaperlessUser } from 'src/app/data/paperless-user'
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
import { UserService } from 'src/app/services/rest/user.service'
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
import { SettingsService } from 'src/app/services/settings.service'
@Directive()
export abstract class EditDialogComponent<
@@ -22,7 +23,8 @@ export abstract class EditDialogComponent<
constructor(
protected service: AbstractPaperlessService<T>,
private activeModal: NgbActiveModal,
private userService: UserService
private userService: UserService,
private settingsService: SettingsService
) {}
users: PaperlessUser[]
@@ -64,7 +66,14 @@ export abstract class EditDialogComponent<
this.closeEnabled = true
})
this.userService.listAll().subscribe((r) => (this.users = r.results))
this.userService.listAll().subscribe((r) => {
this.users = r.results
if (this.dialogMode === 'create') {
this.objectForm.get('permissions_form').setValue({
owner: this.settingsService.currentUser.id,
})
}
})
}
getCreateTitle() {

View File

@@ -5,6 +5,7 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@Component({
selector: 'app-group-edit-dialog',
@@ -15,9 +16,10 @@ export class GroupEditDialogComponent extends EditDialogComponent<PaperlessGroup
constructor(
service: GroupService,
activeModal: NgbActiveModal,
userService: UserService
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService)
super(service, activeModal, userService, settingsService)
}
getCreateTitle() {

View File

@@ -8,6 +8,7 @@ import {
} from 'src/app/data/paperless-mail-account'
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
const IMAP_SECURITY_OPTIONS = [
{ id: IMAPSecurity.None, name: $localize`No encryption` },
@@ -30,9 +31,10 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<Paperles
constructor(
service: MailAccountService,
activeModal: NgbActiveModal,
userService: UserService
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService)
super(service, activeModal, userService, settingsService)
}
getCreateTitle() {

View File

@@ -19,6 +19,7 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
const ATTACHMENT_TYPE_OPTIONS = [
{
@@ -115,9 +116,10 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa
accountService: MailAccountService,
correspondentService: CorrespondentService,
documentTypeService: DocumentTypeService,
userService: UserService
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService)
super(service, activeModal, userService, settingsService)
accountService
.listAll()

View File

@@ -6,6 +6,7 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@Component({
selector: 'app-storage-path-edit-dialog',
@@ -16,9 +17,10 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<Paperles
constructor(
service: StoragePathService,
activeModal: NgbActiveModal,
userService: UserService
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService)
super(service, activeModal, userService, settingsService)
}
get pathHint() {

View File

@@ -7,6 +7,7 @@ import { TagService } from 'src/app/services/rest/tag.service'
import { randomColor } from 'src/app/utils/color'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@Component({
selector: 'app-tag-edit-dialog',
@@ -17,9 +18,10 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
constructor(
service: TagService,
activeModal: NgbActiveModal,
userService: UserService
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService)
super(service, activeModal, userService, settingsService)
}
getCreateTitle() {

View File

@@ -7,6 +7,7 @@ import { PaperlessGroup } from 'src/app/data/paperless-group'
import { PaperlessUser } from 'src/app/data/paperless-user'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@Component({
selector: 'app-user-edit-dialog',
@@ -23,9 +24,10 @@ export class UserEditDialogComponent
constructor(
service: UserService,
activeModal: NgbActiveModal,
groupsService: GroupService
groupsService: GroupService,
settingsService: SettingsService
) {
super(service, activeModal, service)
super(service, activeModal, service, settingsService)
groupsService
.listAll()

View File

@@ -1,4 +1,4 @@
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)">
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
<svg class="toolbaricon" fill="currentColor">
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
@@ -31,9 +31,11 @@
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div>
</div>
<div *ngIf="selectionModel.items" class="items">
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" [disabled]="disabled"></app-toggleable-dropdown-button>
<div *ngIf="selectionModel.items" class="items" #buttonItems>
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index">
<app-toggleable-dropdown-button
*ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i)" [disabled]="disabled">
</app-toggleable-dropdown-button>
</ng-container>
</div>
<button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">

View File

@@ -12,6 +12,7 @@ import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dro
import { MatchingModel } from 'src/app/data/matching-model'
import { Subject } from 'rxjs'
import { SelectionDataItem } from 'src/app/services/rest/document.service'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
export interface ChangedItems {
itemsToAdd: MatchingModel[]
@@ -324,6 +325,7 @@ export class FilterableDropdownSelectionModel {
export class FilterableDropdownComponent {
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
@ViewChild('dropdown') dropdown: NgbDropdown
@ViewChild('buttonItems') buttonItems: ElementRef
filterText: string
@@ -416,14 +418,10 @@ export class FilterableDropdownComponent {
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
}
getUpdatedDocumentCount(id: number) {
if (this.documentCounts) {
return this.documentCounts.find((c) => c.id === id)?.document_count
}
}
modelIsDirty: boolean = false
private keyboardIndex: number
constructor(private filterPipe: FilterPipe) {
this.selectionModelChange.subscribe((updatedModel) => {
this.modelIsDirty = updatedModel.isDirty()
@@ -461,11 +459,13 @@ export class FilterableDropdownComponent {
let filtered = this.filterPipe.transform(this.items, this.filterText)
if (filtered.length == 1) {
this.selectionModel.toggle(filtered[0].id)
if (this.editing) {
this.applyClicked()
} else {
this.dropdown.close()
}
setTimeout(() => {
if (this.editing) {
this.applyClicked()
} else {
this.dropdown.close()
}
}, 200)
}
}
@@ -481,4 +481,85 @@ export class FilterableDropdownComponent {
this.selectionModel.reset(true)
this.selectionModelChange.emit(this.selectionModel)
}
getUpdatedDocumentCount(id: number) {
if (this.documentCounts) {
return this.documentCounts.find((c) => c.id === id)?.document_count
}
}
listKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'ArrowDown':
if (event.target instanceof HTMLInputElement) {
if (
!this.filterText ||
event.target.selectionStart === this.filterText.length
) {
this.keyboardIndex = -1
this.focusNextButtonItem()
event.preventDefault()
}
} else if (event.target instanceof HTMLButtonElement) {
this.focusNextButtonItem()
event.preventDefault()
}
break
case 'ArrowUp':
if (event.target instanceof HTMLButtonElement) {
if (this.keyboardIndex === 0) {
this.listFilterTextInput.nativeElement.focus()
} else {
this.focusPreviousButtonItem()
}
event.preventDefault()
}
break
case 'Tab':
// just track the index in case user uses arrows
if (event.target instanceof HTMLInputElement) {
this.keyboardIndex = 0
} else if (event.target instanceof HTMLButtonElement) {
if (event.shiftKey) {
if (this.keyboardIndex > 0) {
this.focusPreviousButtonItem(false)
}
} else {
this.focusNextButtonItem(false)
}
}
default:
break
}
}
focusNextButtonItem(setFocus: boolean = true) {
this.keyboardIndex = Math.min(this.items.length - 1, this.keyboardIndex + 1)
if (setFocus) this.setButtonItemFocus()
}
focusPreviousButtonItem(setFocus: boolean = true) {
this.keyboardIndex = Math.max(0, this.keyboardIndex - 1)
if (setFocus) this.setButtonItemFocus()
}
setButtonItemFocus() {
this.buttonItems.nativeElement.children[
this.keyboardIndex
]?.children[0].focus()
}
setButtonItemIndex(index: number) {
// just track the index in case user uses arrows
this.keyboardIndex = index
}
hideCount(item: ObjectWithPermissions) {
// counts are pointless when clicking item would add to the set of docs
return (
this.selectionModel.logicalOperator === LogicalOperator.Or &&
this.manyToOne &&
this.selectionModel.get(item.id) !== ToggleableItemState.Selected
)
}
}

View File

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

View File

@@ -26,6 +26,9 @@ export class ToggleableDropdownButtonComponent {
@Input()
disabled: boolean = false
@Input()
hideCount: boolean = false
@Output()
toggle = new EventEmitter()

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ import {
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
@Component({
providers: [
@@ -25,11 +26,9 @@ import {
styleUrls: ['./permissions-select.component.scss'],
})
export class PermissionsSelectComponent
extends ComponentWithPermissions
implements OnInit, ControlValueAccessor
{
PermissionType = PermissionType
PermissionAction = PermissionAction
@Input()
title: string = 'Permissions'
@@ -62,6 +61,7 @@ export class PermissionsSelectComponent
inheritedWarning: string = $localize`Inherited from group`
constructor(private readonly permissionsService: PermissionsService) {
super()
for (const type in PermissionType) {
const control = new FormGroup({})
for (const action in PermissionAction) {

View File

@@ -21,22 +21,20 @@
<div class="row">
<div class="col-lg-8">
<ng-container *ngIf="savedViewService.loading">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</ng-container>
<app-welcome-widget *ngIf="settingsService.offerTour()" tourAnchor="tour.dashboard"></app-welcome-widget>
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
<app-saved-view-widget *ngIf="isFirst; else noTour" [savedView]="v" tourAnchor="tour.dashboard"></app-saved-view-widget>
<ng-template #noTour>
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
</ng-template>
<div tourAnchor="tour.dashboard">
<ng-container *ngIf="savedViewService.loading">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</ng-container>
</div>
<app-welcome-widget *ngIf="settingsService.offerTour()" (dismiss)="completeTour()"></app-welcome-widget>
<div *appIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
</ng-container>
</div>
</div>
</div>
<div class="col-lg-4">

View File

@@ -1,12 +1,8 @@
import { Component } from '@angular/core'
import {
PermissionAction,
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
@Component({
selector: 'app-dashboard',
@@ -16,19 +12,10 @@ import { ComponentWithPermissions } from '../with-permissions/with-permissions.c
export class DashboardComponent extends ComponentWithPermissions {
constructor(
public settingsService: SettingsService,
private permissionsService: PermissionsService,
public savedViewService: SavedViewService
public savedViewService: SavedViewService,
private tourService: TourService
) {
super()
if (
permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.SavedView
)
) {
savedViewService.initialize()
}
}
get subtitle() {
@@ -38,4 +25,12 @@ export class DashboardComponent extends ComponentWithPermissions {
return $localize`Welcome to Paperless-ngx`
}
}
completeTour() {
if (this.tourService.getStatus() !== 0) {
this.tourService.end() // will call settingsService.completeTour()
} else {
this.settingsService.completeTour()
}
}
}

View File

@@ -1,5 +1,4 @@
<ngb-alert type="primary" [dismissible]="false">
<!-- [dismissible]="isFinished(status)" (closed)="dismiss(status)" -->
<ngb-alert class="pe-3" type="primary" [dismissible]="true" (closed)="dismiss.emit(true)">
<h4 class="alert-heading"><ng-container i18n>Paperless-ngx is running!</ng-container> 🎉</h4>
<p i18n>You're ready to start uploading documents! Explore the various features of this web app on your own, or start a quick tour using the button below.</p>
<p i18n>More detail on how to use and configure Paperless-ngx is always available in the <a href="https://docs.paperless-ngx.com" target="_blank">documentation</a>.</p>

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'
import { Component, EventEmitter, Output } from '@angular/core'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
@Component({
@@ -8,4 +8,7 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap'
})
export class WelcomeWidgetComponent {
constructor(public readonly tourService: TourService) {}
@Output()
dismiss: EventEmitter<boolean> = new EventEmitter()
}

View File

@@ -100,7 +100,7 @@
<a ngbNavLink i18n>Metadata</a>
<ng-template ngbNavContent>
<table class="table table-borderless">
<table class="table table-borderless" *ngIf="document">
<tbody>
<tr>
<td i18n>Date modified</td>
@@ -207,8 +207,8 @@
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
</ng-template>
</ng-container>
<ng-container *ngIf="getContentType() === 'text/plain'">
<div [innerHTML]="previewHtml | safeHtml" class="preview-sticky bg-light p-3" width="100%"></div>
<ng-container *ngIf="renderAsPlainText">
<div [innerText]="previewText" class="preview-sticky bg-light p-3" width="100%"></div>
</ng-container>
<div *ngIf="requiresPassword" class="password-prompt">
<form>

View File

@@ -44,6 +44,7 @@ import { PaperlessUser } from 'src/app/data/paperless-user'
import { UserService } from 'src/app/services/rest/user.service'
import { PaperlessDocumentNote } from 'src/app/data/paperless-document-note'
import { HttpClient } from '@angular/common/http'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
enum DocumentDetailNavIDs {
Details = 1,
@@ -60,6 +61,7 @@ enum DocumentDetailNavIDs {
styleUrls: ['./document-detail.component.scss'],
})
export class DocumentDetailComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy, DirtyComponent
{
@ViewChild('inputTitle')
@@ -81,7 +83,7 @@ export class DocumentDetailComponent
title: string
titleSubject: Subject<string> = new Subject()
previewUrl: string
_previewHtml: string
previewText: string
downloadUrl: string
downloadOriginalUrl: string
@@ -127,8 +129,6 @@ export class DocumentDetailComponent
}
}
PermissionAction = PermissionAction
PermissionType = PermissionType
DocumentDetailNavIDs = DocumentDetailNavIDs
activeNavID: number
@@ -148,7 +148,9 @@ export class DocumentDetailComponent
private permissionsService: PermissionsService,
private userService: UserService,
private http: HttpClient
) {}
) {
super()
}
titleKeyUp(event) {
this.titleSubject.next(event.target?.value)
@@ -164,6 +166,12 @@ export class DocumentDetailComponent
: this.metadata?.original_mime_type
}
get renderAsPlainText(): boolean {
return ['text/plain', 'application/csv', 'text/csv'].includes(
this.getContentType()
)
}
get isRTL() {
if (!this.metadata || !this.metadata.lang) return false
else {
@@ -220,10 +228,10 @@ export class DocumentDetailComponent
this.previewUrl = this.documentsService.getPreviewUrl(this.documentId)
this.http.get(this.previewUrl, { responseType: 'text' }).subscribe({
next: (res) => {
this._previewHtml = res.toString()
this.previewText = res.toString()
},
error: (err) => {
this._previewHtml = $localize`An error occurred loading content: ${
this.previewText = $localize`An error occurred loading content: ${
err.message ?? err.toString()
}`
},
@@ -236,10 +244,21 @@ export class DocumentDetailComponent
true
)
this.suggestions = null
if (this.openDocumentService.getOpenDocument(this.documentId)) {
this.updateComponent(
this.openDocumentService.getOpenDocument(this.documentId)
)
const openDocument = this.openDocumentService.getOpenDocument(
this.documentId
)
if (openDocument) {
if (this.documentForm.dirty) {
Object.assign(openDocument, this.documentForm.value)
openDocument['owner'] =
this.documentForm.get('permissions_form').value['owner']
openDocument['permissions'] =
this.documentForm.get('permissions_form').value[
'set_permissions'
]
delete openDocument['permissions_form']
}
this.updateComponent(openDocument)
} else {
this.openDocumentService.openDocument(doc)
this.updateComponent(doc)
@@ -317,6 +336,10 @@ export class DocumentDetailComponent
if (navIDKey) {
this.activeNavID = DocumentDetailNavIDs[navIDKey]
}
} else if (paramMap.get('id')) {
this.router.navigate(['documents', +paramMap.get('id'), 'details'], {
replaceUrl: true,
})
}
})
}
@@ -371,7 +394,9 @@ export class DocumentDetailComponent
error: (error) => {
this.suggestions = null
this.toastService.showError(
$localize`Error retrieving suggestions` + ': ' + error.toString()
$localize`Error retrieving suggestions: ${JSON.stringify(
error
).slice(0, 500)}`
)
},
})
@@ -735,8 +760,4 @@ export class DocumentDetailComponent
)
)
}
get previewHtml(): string {
return this._previewHtml
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -86,7 +86,7 @@ export class DocumentNotesComponent extends ComponentWithPermissions {
displayName(note: PaperlessDocumentNote): string {
if (!note.user) return ''
const user = this.users.find((u) => u.id === note.user)
const user = this.users?.find((u) => u.id === note.user)
if (!user) return ''
const nameComponents = []
if (user.first_name) nameComponents.unshift(user.first_name)

View File

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

View File

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

View File

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

View File

@@ -41,6 +41,7 @@ export const SETTINGS_KEYS = {
'general-settings:update-checking:backend-setting',
SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE:
'general-settings:saved-views:warn-on-unsaved-change',
TOUR_COMPLETE: 'general-settings:tour-complete',
}
export const SETTINGS: PaperlessUiSetting[] = [
@@ -144,4 +145,9 @@ export const SETTINGS: PaperlessUiSetting[] = [
type: 'boolean',
default: true,
},
{
key: SETTINGS_KEYS.TOUR_COMPLETE,
type: 'boolean',
default: false,
},
]

View File

@@ -2,4 +2,6 @@ export interface Results<T> {
count: number
results: T[]
all: number[]
}

View File

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

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'
import { ParamMap, Router } from '@angular/router'
import { Observable } from 'rxjs'
import { Observable, first } from 'rxjs'
import {
filterRulesDiffer,
cloneFilterRules,
@@ -230,7 +230,8 @@ export class DocumentListViewService {
activeListViewState.documents = result.results
this.documentService
.getSelectionData(result.results.map((d) => d.id))
.getSelectionData(result.all)
.pipe(first())
.subscribe({
next: (selectionData) => {
this.selectionData = selectionData

View File

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

View File

@@ -208,6 +208,12 @@ export class SettingsService {
englishName: 'Spanish',
dateInputFormat: 'dd/mm/yyyy',
},
{
code: 'fi-fi',
name: $localize`Finnish`,
englishName: 'Finnish',
dateInputFormat: 'dd.mm.yyyy',
},
{
code: 'fr-fr',
name: $localize`French`,
@@ -478,7 +484,22 @@ export class SettingsService {
offerTour(): boolean {
return (
!this.savedViewService.loading &&
this.savedViewService.dashboardViews.length == 0
this.savedViewService.dashboardViews.length == 0 &&
!this.get(SETTINGS_KEYS.TOUR_COMPLETE)
)
}
completeTour() {
const tourCompleted = this.get(SETTINGS_KEYS.TOUR_COMPLETE)
if (!tourCompleted) {
this.set(SETTINGS_KEYS.TOUR_COMPLETE, true)
this.storeSettings()
.pipe(first())
.subscribe(() => {
this.toastService.showInfo(
$localize`You can restart the tour from the settings page.`
)
})
}
}
}

View File

@@ -3,9 +3,9 @@ const base_url = new URL(document.baseURI)
export const environment = {
production: true,
apiBaseUrl: document.baseURI + 'api/',
apiVersion: '2',
apiVersion: '3',
appTitle: 'Paperless-ngx',
version: '1.14.1',
version: '1.14.5',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',

View File

@@ -5,7 +5,7 @@
export const environment = {
production: false,
apiBaseUrl: 'http://localhost:8000/api/',
apiVersion: '2',
apiVersion: '3',
appTitle: 'Paperless-ngx',
version: 'DEVELOPMENT',
webSocketHost: 'localhost:8000',

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More