Compare commits

..

222 Commits

Author SHA1 Message Date
Trenton H
a0ece589b0 Bumps version to 1.17.2 2023-08-24 11:38:17 -07:00
Trenton H
8165071edf Merge remote-tracking branch 'origin/dev' 2023-08-24 11:35:16 -07:00
shamoon
a667974378 Update settings.component.spec.ts 2023-08-24 00:21:06 -07:00
Trenton Holmes
fe1f88ce5d Sets the http timeouts equal to the task timeout, so it's either done or really done 2023-08-23 18:40:22 -07:00
shamoon
8f2715e437 Merge pull request #4014 from paperless-ngx/ui-perms-tweaks
Enhancement: disable / hide some UI buttons / elements if insufficient permissions, show errors
2023-08-23 10:28:37 -07:00
shamoon
57a3223c77 Merge branch 'dev' into ui-perms-tweaks 2023-08-23 08:48:42 -07:00
Trenton H
df82ac8ac4 Adjusts to use a different loading of certificates and updates the docs for it 2023-08-23 08:22:01 -07:00
Trenton H
16adddc803 Allow users to set a combined certificte and key file for additional certificates in the SSL context 2023-08-23 08:22:01 -07:00
shamoon
d1ae82c5c2 Merge pull request #4038 from paperless-ngx/fix/issue-4036
Fix: tag creation sometimes retained search text
2023-08-21 11:15:15 -07:00
shamoon
0098936347 Fix: tag creation sometimes retained search text 2023-08-20 16:18:34 -07:00
shamoon
17b85f6400 Merge pull request #4035 from brakhane/polling-note
docs: add note about polling when using double-sided collation
2023-08-20 11:04:45 -07:00
Dennis Brakhane
48be9c0fd1 docs: add note about polling when using double-sided collation 2023-08-20 19:51:14 +02:00
shamoon
ce36c2d0ea Merge pull request #4007 from paperless-ngx/fix/issue-4003
Fix: enforce permissions on bulk_edit operations
2023-08-18 06:37:08 -07:00
shamoon
06c63ef4a4 Disable / hide some UI buttons / elements if insufficient permissions 2023-08-17 20:34:19 -07:00
shamoon
03d93a7d6e Fix: enforce permissions on bulk_edit operations 2023-08-17 00:12:46 -07:00
amo13
a0005c8b3e Enhancement: Allow to set a prefix for keys and channels in redis (#3993) 2023-08-16 00:05:01 +00:00
shamoon
2220343c50 Merge pull request #3998 from paperless-ngx/v1.17.1-changelog
[Documentation] Add v1.17.1 changelog
2023-08-15 13:35:50 -07:00
github-actions
68fbed996f Changelog v1.17.1 - GHA 2023-08-15 16:00:47 +00:00
Trenton H
20a4d8949d Reset to -dev version string 2023-08-15 08:42:06 -07:00
Trenton H
1029ecfd49 Bumps version to 1.17.1 2023-08-15 08:38:52 -07:00
Trenton H
4de2a3b09e Merge remote-tracking branch 'origin/dev' 2023-08-15 08:38:11 -07:00
Paperless-ngx Bot [bot]
651407c88e New Crowdin updates (#3930)
* New translations messages.xlf (Ukrainian)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* New translations messages.xlf (Serbian (Latin))
[ci skip]
2023-08-15 08:32:03 -07:00
shamoon
3e2e485f66 Update playwright 2023-08-14 21:51:46 -07:00
shamoon
10efaad224 Merge pull request #3988 from paperless-ngx/fix/issue-3986 2023-08-12 17:27:34 -07:00
shamoon
3dda02660c Implement isNumber pipe 2023-08-12 08:31:42 -07:00
shamoon
0b4e8141b0 Merge pull request #3980 from paperless-ngx/fix/issue-3978
Fix: improve light color filled primary button text legibility
2023-08-11 20:03:46 -07:00
shamoon
16ab0efa59 Merge branch 'dev' into fix/issue-3978 2023-08-11 07:49:44 -07:00
shamoon
20ca1ec547 Merge pull request #3979 from paperless-ngx/fix/ukrainian-spelling
Fix: Correct spelling of Ukrainian
2023-08-11 07:49:33 -07:00
shamoon
a1c9ab237f Fix light color filled primary button text legibility 2023-08-11 07:43:41 -07:00
shamoon
a65239f7f1 Correct spelling of Ukrainian 2023-08-11 07:25:31 -07:00
Trenton H
a0622675fd Loosen restriction and update a couple of packages which were out of date 2023-08-10 17:23:16 -07:00
shamoon
1e0b778097 Merge pull request #3959 from paperless-ngx/fix/issue-3945
Fix: restrict status messages by owner if set
2023-08-09 17:52:47 -07:00
shamoon
3b666fef77 Add backend check for ws message ownership 2023-08-09 16:46:48 -07:00
shamoon
9291c98189 Improve 404 navigation and styling 2023-08-09 00:30:20 -07:00
shamoon
f6dadd8c82 Merge branch 'dev' into fix/issue-3945 2023-08-08 22:37:13 -07:00
shamoon
022bb272e6 Restrict status messages by owner if set 2023-08-08 20:39:55 -07:00
shamoon
edad1a41e8 Merge pull request #3953 from paperless-ngx/fix/issue-3944
Handle very old date strings in correspondent list
2023-08-08 06:59:29 -07:00
shamoon
421e78a748 Handle very old date strings in correspondent list 2023-08-07 17:31:38 -07:00
Trenton H
b961df90a7 Reduces the 2 mail tests flakiness 2023-08-07 15:32:33 -07:00
shamoon
0b26b7098a Merge pull request #3941 from paperless-ngx/feature-ukranian-translation
Feature: Add Ukranian translation
2023-08-06 08:20:32 -07:00
shamoon
b09566a9a9 Adds Ukranian translation 2023-08-06 08:08:07 -07:00
github-actions
b869deab66 Changelog v1.17.0 - GHA 2023-08-05 09:17:18 -07:00
Jens van Almsick
3d552c3112 docs(bare-metal): add new dependency
mysqlclient 2.2.0 now requires pkg-config during installation via pip see https://github.com/PyMySQL/mysqlclient/discussions/638#discussioncomment-6333394
2023-08-05 07:55:18 -07:00
shamoon
2a2bf3bf55 Update environment.prod.ts 2023-08-04 12:04:36 -07:00
shamoon
26863b8cdc Bumps version to 1.17.0 2023-08-04 11:39:14 -07:00
shamoon
b0f7d07214 Merge branch 'dev' 2023-08-04 11:38:12 -07:00
Paperless-ngx Bot [bot]
30c6557a32 New Crowdin updates (#3742)
* New translations messages.xlf (Swedish)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-08-04 18:37:15 +00:00
shamoon
e18e173089 Update messages.xlf 2023-08-04 11:25:22 -07:00
shamoon
6a8bdbd4f6 Merge pull request #3925 from paperless-ngx/fix/cancel-slow-queries 2023-08-04 07:22:23 -07:00
shamoon
b5dec87a62 Cancel possibly slow queries on leave doc details 2023-08-03 21:49:11 -07:00
Trenton H
e50d30876a Merge pull request #3760 from a17t/dev
[BUG] Set office document creation date with timezone, if it is naive
2023-08-03 10:35:56 -07:00
Trenton Holmes
cbcd9ed67d Merge remote-tracking branch 'paperless-ngx/dev' into dev 2023-08-03 10:00:14 -07:00
Trenton Holmes
6bcc26b487 Sets the timezone of creation, if the date is known and naive 2023-08-03 09:57:52 -07:00
Simon Siebert
56fcb3fee1 Working arround current TIKA Library Bugs - lint 2023-08-03 09:55:10 -07:00
shamoon
557e1790dd Merge pull request #3903 from paperless-ngx/fix/issue-3902
Fix: note creation / deletion should respect doc permissions
2023-08-02 08:33:44 -07:00
shamoon
2e67697d36 Note creation / deletion should respect doc permissions
- Disable add note button on frontend
- Explicitly disable add / delete via api
2023-08-01 22:28:27 -07:00
shamoon
ca4500692f remove unused e2e code 2023-08-01 19:27:52 -07:00
shamoon
5aba6bff09 Merge pull request #3918 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/frontend-angular-dependencies-f0885b8c36
Chore: Bump the frontend-angular-dependencies group in /src-ui with 11 updates
2023-08-01 19:27:23 -07:00
shamoon
9ed74068b9 Merge branch 'dev' into dependabot/npm_and_yarn/src-ui/dev/frontend-angular-dependencies-f0885b8c36 2023-08-01 19:27:11 -07:00
shamoon
be4685742c remove redundant e2e tests 2023-08-01 19:23:11 -07:00
dependabot[bot]
86b0a38811 Bump the frontend-angular-dependencies group in /src-ui with 11 updates
Bumps the frontend-angular-dependencies group in /src-ui with 11 updates:

| Package | Update |
| --- | --- |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | 16.1.5 to 16.1.7 |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | 16.1.5 to 16.1.7 |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | 16.1.5 to 16.1.7 |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | 16.1.5 to 16.1.7 |
| [@angular/localize](https://github.com/angular/angular) | 16.1.5 to 16.1.7 |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | 16.1.5 to 16.1.7 |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | 16.1.5 to 16.1.7 |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | 16.1.5 to 16.1.7 |
| [@ng-select/ng-select](https://github.com/ng-select/ng-select) | 11.0.0 to 11.1.1 |
| [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | 16.1.4 to 16.1.6 |
| [@angular/cli](https://github.com/angular/angular-cli) | 16.1.4 to 16.1.6 |


Updates `@angular/common` from 16.1.5 to 16.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.7/packages/common)

Updates `@angular/compiler` from 16.1.5 to 16.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.7/packages/compiler)

Updates `@angular/core` from 16.1.5 to 16.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.7/packages/core)

Updates `@angular/forms` from 16.1.5 to 16.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.7/packages/forms)

Updates `@angular/localize` from 16.1.5 to 16.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/compare/16.1.5...16.1.7)

Updates `@angular/platform-browser` from 16.1.5 to 16.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.7/packages/platform-browser)

Updates `@angular/platform-browser-dynamic` from 16.1.5 to 16.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.7/packages/platform-browser-dynamic)

Updates `@angular/router` from 16.1.5 to 16.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.7/packages/router)

Updates `@ng-select/ng-select` from 11.0.0 to 11.1.1
- [Release notes](https://github.com/ng-select/ng-select/releases)
- [Changelog](https://github.com/ng-select/ng-select/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ng-select/ng-select/compare/v11.0.0...v11.1.1)

Updates `@angular-devkit/build-angular` from 16.1.4 to 16.1.6
- [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/16.1.4...16.1.6)

Updates `@angular/cli` from 16.1.4 to 16.1.6
- [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/16.1.4...16.1.6)

---
updated-dependencies:
- dependency-name: "@angular/common"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@ng-select/ng-select"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/build-angular"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/cli"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-02 02:04:39 +00:00
dependabot[bot]
3e528f0a9a Bump stumpylog/image-cleaner-action from 0.1.0 to 0.2.0
Bumps [stumpylog/image-cleaner-action](https://github.com/stumpylog/image-cleaner-action) from 0.1.0 to 0.2.0.
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/v0.1.0...v0.2.0)

---
updated-dependencies:
- dependency-name: stumpylog/image-cleaner-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 17:33:43 -07:00
shamoon
170d7b6922 Merge pull request #3911 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/frontend-eslint-dependencies-a4ef7a9a8b
Bump the frontend-eslint-dependencies group in /src-ui with 3 updates
2023-08-01 16:25:51 -07:00
dependabot[bot]
f05249a9ad Bump the frontend-eslint-dependencies group in /src-ui with 3 updates
Bumps the frontend-eslint-dependencies group in /src-ui with 3 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [eslint](https://github.com/eslint/eslint).


Updates `@typescript-eslint/eslint-plugin` from 6.1.0 to 6.2.1
- [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/v6.2.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 6.1.0 to 6.2.1
- [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/v6.2.1/packages/parser)

Updates `eslint` from 8.44.0 to 8.46.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.44.0...v8.46.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 23:14:30 +00:00
shamoon
5957ed7af3 Merge pull request #3909 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/tslib-2.6.1
Bump tslib from 2.6.0 to 2.6.1 in /src-ui
2023-08-01 16:12:29 -07:00
shamoon
8ed63893eb Merge branch 'dev' into dependabot/npm_and_yarn/src-ui/dev/tslib-2.6.1 2023-08-01 15:37:47 -07:00
shamoon
ea9dd926bc Merge pull request #3916 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/jest-environment-jsdom-29.6.2
Bump jest-environment-jsdom from 29.5.0 to 29.6.2 in /src-ui
2023-08-01 15:35:40 -07:00
dependabot[bot]
f31d3b531f Bump jest-environment-jsdom from 29.5.0 to 29.6.2 in /src-ui
Bumps [jest-environment-jsdom](https://github.com/facebook/jest/tree/HEAD/packages/jest-environment-jsdom) from 29.5.0 to 29.6.2.
- [Release notes](https://github.com/facebook/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/jest/commits/v29.6.2/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: jest-environment-jsdom
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 22:17:56 +00:00
shamoon
d851448c32 Merge pull request #3915 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/types/node-20.4.5
Bump @types/node from 20.3.3 to 20.4.5 in /src-ui
2023-08-01 15:17:02 -07:00
shamoon
65327d52a6 Merge branch 'dev' into dependabot/npm_and_yarn/src-ui/dev/tslib-2.6.1 2023-08-01 15:06:07 -07:00
dependabot[bot]
2ea5ae59b2 Bump @types/node from 20.3.3 to 20.4.5 in /src-ui
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.3.3 to 20.4.5.
- [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-08-01 22:03:07 +00:00
shamoon
a04d09028b Merge pull request #3914 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/bootstrap-5.3.1
Bump bootstrap from 5.3.0 to 5.3.1 in /src-ui
2023-08-01 15:02:11 -07:00
dependabot[bot]
dd9255cb81 Bump bootstrap from 5.3.0 to 5.3.1 in /src-ui
Bumps [bootstrap](https://github.com/twbs/bootstrap) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/twbs/bootstrap/releases)
- [Commits](https://github.com/twbs/bootstrap/compare/v5.3.0...v5.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 21:35:01 +00:00
shamoon
536d7ecd3e Merge pull request #3912 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/playwright/test-1.36.2
Bump @playwright/test from 1.36.1 to 1.36.2 in /src-ui
2023-08-01 14:33:42 -07:00
shamoon
f50aac08df Merge branch 'dev' into dependabot/npm_and_yarn/src-ui/dev/tslib-2.6.1 2023-08-01 14:02:26 -07:00
dependabot[bot]
db0f1d2159 Bump @playwright/test from 1.36.1 to 1.36.2 in /src-ui
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.36.1 to 1.36.2.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.36.1...v1.36.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 21:02:21 +00:00
shamoon
d97b565d6c Merge pull request #3906 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/frontend-jest-dependencies-7b2ae8246b
Bump the frontend-jest-dependencies group in /src-ui with 1 update
2023-08-01 14:01:26 -07:00
shamoon
d3071f13d8 Merge branch 'dev' into dependabot/npm_and_yarn/src-ui/dev/tslib-2.6.1 2023-08-01 13:48:39 -07:00
dependabot[bot]
768407c1d7 Bump the frontend-jest-dependencies group in /src-ui with 1 update
Bumps the frontend-jest-dependencies group in /src-ui with 1 update: [jest](https://github.com/facebook/jest/tree/HEAD/packages/jest).

- [Release notes](https://github.com/facebook/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/jest/commits/v29.6.2/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-jest-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 20:28:34 +00:00
shamoon
c5d18b03cd Add eslint to eslint dependabot group 2023-08-01 13:27:58 -07:00
shamoon
5f05b44cde Merge pull request #3904 from paperless-ngx/fix/notes-scrollbars
Fix: notes show persistent scrollbars
2023-08-01 13:25:16 -07:00
dependabot[bot]
beaa09e9b3 Bump tslib from 2.6.0 to 2.6.1 in /src-ui
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/2.6.0...v2.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 20:14:35 +00:00
shamoon
d6960f537b Do not force scroll on notes .card-body 2023-08-01 09:14:32 -07:00
brainrecursion
0918eab004 Remove debug 2023-07-30 08:34:33 -07:00
brainrecursion
9b16789a17 Add additional test for date parsing 2023-07-30 08:34:33 -07:00
brainrecursion
157240351f Add support for more date formats 2023-07-30 08:34:33 -07:00
mechanarchy
6ad3d45d60 Add 'doc_pk' to PAPERLESS_FILENAME_FORMAT handling (#3861)
* Add 'doc_pk' to PAPERLESS_FILENAME_FORMAT handling

* Add test for 'doc_pk' filename formatter
2023-07-30 08:30:50 -07:00
Trenton H
b715e4d426 Creates and provides a default SSL context to the IMAP library 2023-07-28 10:43:37 -07:00
Trenton H
aad5e9e99f Locks and upgrades all dependencies 2023-07-27 12:30:41 -07:00
shamoon
2a104ad33f Merge pull request #3875 from paperless-ngx/feature-widget-buttons 2023-07-27 12:20:20 -07:00
shamoon
851290ee89 Merge branch 'dev' into feature-widget-buttons 2023-07-27 11:49:17 -07:00
shamoon
a8c6c55e3b Useful buttons for saved view widgets 2023-07-26 11:36:55 -07:00
shamoon
992a647424 Fix large card date hover text color 2023-07-26 11:31:37 -07:00
shamoon
c22461a1b6 Merge pull request #3869 from paperless-ngx/fix/issue-3868
Fix/enhancement: permissions for mail rules & accounts
2023-07-26 07:37:54 -07:00
shamoon
23fefc3ab7 Include permissions for mail rules & accounts 2023-07-26 07:28:25 -07:00
shamoon
0beb9f0b5f Update frontend consumer status phases 2023-07-26 07:03:43 -07:00
Trenton Holmes
d376f9e7a3 Adding more typing around the classification and matching 2023-07-26 07:03:43 -07:00
Trenton Holmes
07e7bcd30b Small improvement to the consumer status with stronger typing 2023-07-26 07:03:43 -07:00
shamoon
95e86cb649 Add tip about permissions behavior 2023-07-25 13:27:50 -07:00
Trenton H
802e5591ce Also handles confirming returned predictions are still automatic matching, in case the classifier hasn't been run since a type was changed 2023-07-24 12:31:56 -07:00
Trenton H
26d5730ad2 Handles a special case where the classification model exists, but no items are auto matching any longer 2023-07-24 12:31:56 -07:00
Dennis Brakhane
8c7554e081 Feature: collate two single-sided multipage scans (#3784)
* Feature: collate two single-sided scans

Some ADF only support single-sided scans, making scanning
double-sided documents a bit annoying.

This new feature enables Paperless to do most of the work,
by merging two seperate scans into a single one, collating
the even and odd numbered pages.

* Documentation: clarify that collation is disabled by default

* Apply suggestions from code review

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>

* Address code review remarks

* Grammar fixes

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-07-24 00:29:04 -07:00
Trenton H
9f5d47c320 Fixes issues with copy2 or copystat and SELinux see #3665 2023-07-22 06:27:49 -07:00
shamoon
4aa452ce63 Merge pull request #3844 from mechanarchy/patch-1
Documentation improvements
2023-07-21 10:26:51 -07:00
mechanarchy
7ef81ae10f Updates documentation formatting, grammar, links
Correct indentation of lists, code blocks

Update faq.md

Update CONTRIBUTING.md

Update development.md

Update advanced_usage.md

Update administration.md

Update configuration.md

Update configuration.md

Update advanced_usage.md

Update faq.md
2023-07-21 10:17:57 -07:00
shamoon
3628292afa Updates frontend strings from latest prettier changes 2023-07-20 18:30:11 -07:00
Trenton H
8aa5ecde62 Updates some Python dependencies and the hooks 2023-07-20 18:30:11 -07:00
shamoon
2f149eac9d Merge pull request #3672 from paperless-ngx/feature-permissions-exportimport
Feature: include global and object-level permissions in export / import
2023-07-20 11:40:53 -07:00
Trenton H
fcfc705b87 Fixes the timezone value in a test 2023-07-20 07:01:38 -07:00
Trenton H
7bd5c010a1 Updates to latest tika-client 2023-07-20 07:01:38 -07:00
shamoon
52168d8e61 Merge pull request #3835 from paperless-ngx/fix/issue-3833
Fix: Add warning to install script need for permissions
2023-07-19 18:16:08 -07:00
Trenton H
a3842d9228 Fixes the barcode setting always overriding the ASN, even if one wasn't found 2023-07-19 18:10:38 -07:00
shamoon
452c51bd16 Add warning to install script need for permissions 2023-07-19 16:03:35 -07:00
dependabot[bot]
22bedd9957 Merge pull request #3826 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/frontend-angular-dependencies-33417c6be4 2023-07-19 18:09:56 +00:00
dependabot[bot]
cb318c723d Bump the frontend-angular-dependencies group in /src-ui with 16 updates
Bumps the frontend-angular-dependencies group in /src-ui with 16 updates:

| Package | Update |
| --- | --- |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | 16.1.3 to 16.1.5 |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | 16.1.3 to 16.1.5 |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | 16.1.3 to 16.1.5 |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | 16.1.3 to 16.1.5 |
| [@angular/localize](https://github.com/angular/angular) | 16.1.3 to 16.1.5 |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | 16.1.3 to 16.1.5 |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | 16.1.3 to 16.1.5 |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | 16.1.3 to 16.1.5 |
| [@ng-bootstrap/ng-bootstrap](https://github.com/ng-bootstrap/ng-bootstrap) | 15.0.1 to 15.1.0 |
| [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | 16.1.3 to 16.1.4 |
| [@angular-eslint/builder](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/builder) | 16.0.3 to 16.1.0 |
| [@angular-eslint/eslint-plugin](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin) | 16.0.3 to 16.1.0 |
| [@angular-eslint/eslint-plugin-template](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin-template) | 16.0.3 to 16.1.0 |
| [@angular-eslint/schematics](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/schematics) | 16.0.3 to 16.1.0 |
| [@angular-eslint/template-parser](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/template-parser) | 16.0.3 to 16.1.0 |
| [@angular/cli](https://github.com/angular/angular-cli) | 16.1.3 to 16.1.4 |


Updates `@angular/common` from 16.1.3 to 16.1.5
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.5/packages/common)

Updates `@angular/compiler` from 16.1.3 to 16.1.5
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.5/packages/compiler)

Updates `@angular/core` from 16.1.3 to 16.1.5
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.5/packages/core)

Updates `@angular/forms` from 16.1.3 to 16.1.5
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.5/packages/forms)

Updates `@angular/localize` from 16.1.3 to 16.1.5
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/compare/16.1.3...16.1.5)

Updates `@angular/platform-browser` from 16.1.3 to 16.1.5
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.5/packages/platform-browser)

Updates `@angular/platform-browser-dynamic` from 16.1.3 to 16.1.5
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.5/packages/platform-browser-dynamic)

Updates `@angular/router` from 16.1.3 to 16.1.5
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/16.1.5/packages/router)

Updates `@ng-bootstrap/ng-bootstrap` from 15.0.1 to 15.1.0
- [Release notes](https://github.com/ng-bootstrap/ng-bootstrap/releases)
- [Changelog](https://github.com/ng-bootstrap/ng-bootstrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ng-bootstrap/ng-bootstrap/compare/15.0.1...15.1.0)

Updates `@angular-devkit/build-angular` from 16.1.3 to 16.1.4
- [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/16.1.3...16.1.4)

Updates `@angular-eslint/builder` from 16.0.3 to 16.1.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/builder/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v16.1.0/packages/builder)

Updates `@angular-eslint/eslint-plugin` from 16.0.3 to 16.1.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v16.1.0/packages/eslint-plugin)

Updates `@angular-eslint/eslint-plugin-template` from 16.0.3 to 16.1.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v16.1.0/packages/eslint-plugin-template)

Updates `@angular-eslint/schematics` from 16.0.3 to 16.1.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/schematics/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v16.1.0/packages/schematics)

Updates `@angular-eslint/template-parser` from 16.0.3 to 16.1.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/template-parser/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v16.1.0/packages/template-parser)

Updates `@angular/cli` from 16.1.3 to 16.1.4
- [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/16.1.3...16.1.4)

---
updated-dependencies:
- dependency-name: "@angular/common"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@ng-bootstrap/ng-bootstrap"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/build-angular"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/builder"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/eslint-plugin-template"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/schematics"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/template-parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/cli"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-19 17:47:16 +00:00
shamoon
9a81d3c28e Merge pull request #3719 from paperless-ngx/fix/issue-3712
Enhancement / Fix: Migrate encrypted png thumbnails to webp
2023-07-19 10:33:46 -07:00
dependabot[bot]
3ca59e3b7a Bump @typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui (#3829) 2023-07-19 17:28:31 +00:00
dependabot[bot]
07a12bdf15 Merge pull request #3828 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/jest-and-types/jest-29.6.1 2023-07-19 16:54:02 +00:00
shamoon
62e81d8bf0 Add more frontend dependabot groups 2023-07-19 09:48:23 -07:00
dependabot[bot]
e295a41caa Bump jest and @types/jest in /src-ui
Bumps [jest](https://github.com/facebook/jest/tree/HEAD/packages/jest) and [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest). These dependencies needed to be updated together.

Updates `jest` from 29.5.0 to 29.6.1
- [Release notes](https://github.com/facebook/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/jest/commits/v29.6.1/packages/jest)

Updates `@types/jest` from 29.5.2 to 29.5.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-19 16:33:43 +00:00
dependabot[bot]
c545a80aa3 Merge pull request #3827 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/playwright/test-1.36.1 2023-07-19 16:32:22 +00:00
dependabot[bot]
56f1a0cb51 Bump @playwright/test from 1.36.0 to 1.36.1 in /src-ui
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.36.0 to 1.36.1.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.36.0...v1.36.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-19 16:19:47 +00:00
shamoon
7218b6da97 Merge pull request #3750 from paperless-ngx/chore/frontend-angular-grouped-dependabot
Chore: group frontend angular dependabot updates
2023-07-19 09:13:50 -07:00
shamoon
83f9f2d387 Merge pull request #3793 from paperless-ngx/dependabot/npm_and_yarn/src-ui/semver-5.7.2
Bump semver from 5.7.1 to 5.7.2 in /src-ui
2023-07-13 09:26:41 -07:00
dependabot[bot]
13a2e38385 Bump semver from 5.7.1 to 5.7.2 in /src-ui
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-13 15:51:18 +00:00
shamoon
996d942387 Merge pull request #3794 from paperless-ngx/fix/playwright-1.36
Fix: Bumps playwright to 1.36.0
2023-07-13 08:50:28 -07:00
shamoon
cc42eb9fab Bumps playwright to 1.36.0 2023-07-13 08:18:00 -07:00
shamoon
44125be979 Merge pull request #3791 from hakimio/ui-tour-fix
Enhancement: add UI tour step padding
2023-07-13 08:08:17 -07:00
Tomas Rimkus
c2e9cc9a51 UI tour fixes and improvements
Update UI tour library
Fix popover padding in tour steps
Add 10px backdrop offset where needed
Refactor tour initialization to use defaults
Make popover-body rule more specific
Update messages.xlf
2023-07-13 07:58:41 -07:00
shamoon
fcd10f2adc Merge pull request #3783 from brakhane/ocr_test_fix
Tolerate improved tesseract results in ocr tests
2023-07-11 08:17:16 -07:00
Dennis Brakhane
93009c1eed Don't consider better OCR as failing
Tesseract 5.3.0 does a better job at OCR, and correctly
reads "a webp" instead of "awebp", this is good, so we
don't want the test to fail.
2023-07-11 16:44:18 +02:00
Simon Siebert
d875be60d4 Working arround current TIKA Library Bugs 2023-07-06 23:26:01 +02:00
shamoon
db48d4c576 Merge pull request #3749 from paperless-ngx/fix/issue-3747
Fix: translate file tasks types in footer
2023-07-05 10:14:37 -07:00
shamoon
3293231ad2 Merge pull request #3722 from paperless-ngx/feature-slovak-translation
Feature: Add Slovak translation
2023-07-05 09:59:12 -07:00
shamoon
aa1f2d3b59 Group frontend angular dependabot updates 2023-07-05 09:57:44 -07:00
shamoon
f492b679e3 Translate file tasks types in footer 2023-07-05 09:39:44 -07:00
shamoon
7ca84322bd Update messages.xlf 2023-07-05 09:39:36 -07:00
shamoon
e3257b8fa3 Merge pull request #3731 from paperless-ngx/fix/issue-3730
Fix: limit ng-select size for addition of filter button
2023-07-02 08:12:17 -07:00
shamoon
0bcda5ded8 Limit ng-select size for addition of filter button 2023-07-02 07:50:26 -07:00
shamoon
7ec82c0891 Merge pull request #3727 from paperless-ngx/dependabot/npm_and_yarn/src-ui/dev/angular/cli-16.1.3
Bump Angular to v16 and required packages
2023-07-01 21:42:12 -07:00
shamoon
3241ac7dc2 Upgrade to Angular 16
update ngx-file-drop
Update ng-bootstrap, ng-select, cookie-service and ui-tour
Update setup-jest.ts
bump typescript to 5.1.6

bump ngx-color and tslib
2023-07-01 19:59:24 -07:00
dependabot[bot]
e974605fc8 Bump @angular/cli from 15.2.7 to 16.1.3 in /src-ui
Bumps [@angular/cli](https://github.com/angular/angular-cli) from 15.2.7 to 16.1.3.
- [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.7...16.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-01 20:35:06 +00:00
shamoon
ce13380533 Add Slovak translation 2023-06-30 20:24:22 -07:00
Paperless-ngx Bot [bot]
e23e3acda3 New Crowdin updates (#3711)
* New translations messages.xlf (Slovak)
[ci skip]

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

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

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

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

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

* New translations messages.xlf (Slovak)
[ci skip]
2023-06-30 19:59:22 -07:00
shamoon
63ab9972da Fixes tests broken by #3721 2023-06-30 08:36:47 -07:00
shamoon
feb4901620 Merge pull request #3721 from andreasbrett/patch-1
fixed typo in signals.pay log output
2023-06-30 07:52:14 -07:00
Andreas Brett
67788a1b1b fixed typo
full stop should be outside of the quoted IP address
2023-06-30 15:11:14 +02:00
shamoon
001faf9ed7 Migrate encrypted png thumbnails to webp 2023-06-29 13:21:15 -07:00
shamoon
7a464d8a6e Update dev version string 2023-06-27 16:44:05 -07:00
shamoon
5acd1c7c1b Merge pull request #3710 from paperless-ngx/v1.16.5-changelog
[Documentation] Add v1.16.5 changelog
2023-06-27 16:14:27 -07:00
github-actions
de14540374 Changelog v1.16.5 - GHA 2023-06-27 23:10:02 +00:00
shamoon
37e0c2667b Bumps version to 1.16.5 2023-06-27 14:51:49 -07:00
shamoon
252abb41c3 Merge branch 'dev' 2023-06-27 14:51:08 -07:00
shamoon
fb2af341d8 Adding explicit doc permissions test 2023-06-27 10:53:48 -07:00
Bastian Machek
931f5f9c27 Feature: support barcode upscaling for better detection of small barcodes (#3655) 2023-06-27 10:18:47 -07:00
shamoon
b5d04e575e Merge pull request #3702 from paperless-ngx/fix/issue-3347
Fix: owner removed when set_permissions passed on object create
2023-06-27 07:17:01 -07:00
shamoon
3d395601fe Fix owner removed when set_permissions passed 2023-06-27 01:41:20 -07:00
shamoon
6630ce646c Merge pull request #3699 from paperless-ngx/v1.16.4-changelog
[Documentation] Add v1.16.4 changelog
2023-06-26 14:25:17 -07:00
shamoon
f5508eea1c Update dev version string 2023-06-26 12:56:47 -07:00
github-actions
c8c460432f Changelog v1.16.4 - GHA 2023-06-26 18:43:20 +00:00
shamoon
18299dafd2 Bumps version to 1.16.4 2023-06-26 11:25:18 -07:00
shamoon
817d09026e Merge branch 'dev' 2023-06-26 11:24:04 -07:00
Paperless-ngx Bot [bot]
d76b009390 New Crowdin updates (#3698)
* New translations messages.xlf (Spanish)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* New translations messages.xlf (Serbian (Latin))
[ci skip]
2023-06-26 11:23:42 -07:00
shamoon
9effed3ce1 Update messages.xlf 2023-06-26 11:21:20 -07:00
shamoon
c1bbfc5dcf Update api.md 2023-06-26 10:08:21 -07:00
shamoon
b6c9cfb76f Merge pull request #3697 from paperless-ngx/docs/update-api-perms-docs
Documentation: update API docs re permissions
2023-06-26 10:06:05 -07:00
shamoon
59ca7bbcf2 Update API docs re permissions 2023-06-26 10:03:44 -07:00
Paperless-ngx Bot [bot]
52c8d5e999 New translations messages.xlf (Spanish) (#3659)
[ci skip]
2023-06-26 09:58:12 -07:00
shamoon
5851e7f1b7 Merge pull request #3682 from paperless-ngx/fix/issue-3679
Fix: prevent button wrapping when sidebar narrows in MS Edge
2023-06-26 09:43:20 -07:00
Trenton H
e05b3441de Updates tika client library and handle the changes to it 2023-06-26 10:41:05 -06:00
Trenton H
0d6e79cb93 Fixes generation of thumbnails when the archive file hasn't already been created 2023-06-26 10:36:50 -06:00
shamoon
76a102d901 Prevent button wrapping 2023-06-25 07:25:07 -07:00
shamoon
bbd4659fbf Include global and object-level permissions in export / import
adds test for transaction
2023-06-23 23:33:36 -07:00
shamoon
0880420ef6 Merge pull request #3662 from kleinweby/fix-filter-row-gap
Fix: Use row gap for filter editor
2023-06-22 11:15:26 -07:00
Christian Speich
2351c79282 Use row gap for filter editor
Fixes other indentation in filter-editor
2023-06-22 11:13:17 -07:00
shamoon
d6016fc798 Update PULL_REQUEST_TEMPLATE.md 2023-06-22 09:46:18 -07:00
github-actions[bot]
bc17291006 Changelog v1.16.3 - GHA (#3661)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-22 09:42:36 -07:00
Trenton H
ed6cb14c4d Updates codecov configuration for the flag settings and notification interaction 2023-06-22 08:20:04 -07:00
Trenton Holmes
08de8a04b8 Resets -dev versioning 2023-06-22 06:34:51 -07:00
Trenton Holmes
5c67de8b47 Bumps version to 1.16.3 2023-06-22 06:32:25 -07:00
Paperless-ngx Bot [bot]
38b0408b1a New Crowdin updates (#3654)
* New translations messages.xlf (Spanish)
[ci skip]

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

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

* New translations django.po (Chinese Simplified)
[ci skip]
2023-06-21 21:02:14 -07:00
shamoon
9ccad7ea86 Fix date component incorrect translation unit 2023-06-21 20:59:29 -07:00
shamoon
4a4e810a14 Fix invalid translation unit 2023-06-21 20:40:29 -07:00
Trenton H
76d2df3bde Resets frontend version tag to -dev 2023-06-21 09:50:50 -07:00
Trenton H
c02563d894 Bumps version to 1.16.3 2023-06-21 09:49:36 -07:00
Trenton H
574ec6780b Merge remote-tracking branch 'origin/dev' 2023-06-21 09:48:33 -07:00
Paperless-ngx Bot [bot]
9e0f56982b New Crowdin updates (#3529)
* New translations messages.xlf (French)
[ci skip]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* New translations messages.xlf (Slovak)
[ci skip]
2023-06-21 08:03:43 -07:00
Trenton H
1c66daf12b Ignore errors when trying to copy the original file's stats 2023-06-21 07:54:27 -07:00
shamoon
59d683849e Merge pull request #3645 from plu/test_3631
Add test for not moving default thumbnail
2023-06-20 16:43:16 -07:00
Johannes Plunien
9946acb1a0 Add test for not moving default thumbnail
See also #3632 and #3631
2023-06-20 20:54:15 +02:00
Johannes Plunien
83a760644d Copy default thumbnail if thumbnail generation fails
Fix #3631
2023-06-20 11:28:46 -07:00
shamoon
25ccff8640 Merge pull request #3641 from paperless-ngx/update-stale-labels
Chore: include 'not a bug' for stale action
2023-06-20 11:00:44 -07:00
Trenton H
5c4c5a7794 Explicitly set some environment for each supervised program, as it is not updated by supervisord 2023-06-20 10:53:33 -07:00
shamoon
cb6af97595 Include 'not a bug' for stale action 2023-06-20 10:53:05 -07:00
Trenton H
c4407dccf6 Updates the default Postgres to 15 for new installs 2023-06-20 10:35:48 -07:00
Trenton H
ecdea4c3c8 Updates the stale timing and close timing 2023-06-20 08:29:19 -07:00
Trenton H
26d6f302cf When starting with an external DB, start it for a bit first to allow its setup to complete 2023-06-20 08:20:44 -07:00
github-actions[bot]
ecf10622ef [Documentation] Add v1.16.2 changelog (#3629)
* Changelog v1.16.2 - GHA

* Update changelog.md

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-06-19 19:52:11 -07:00
Trenton H
0fb553675b Resets -dev version string 2023-06-19 09:39:50 -07:00
Trenton H
2080fde4f9 Bumps the version to v1.16.2 2023-06-19 09:07:11 -07:00
Trenton H
d10e67ce09 Merge remote-tracking branch 'origin/dev' 2023-06-19 09:01:15 -07:00
Trenton H
74fe7c586b Updates the httpx timeout to be 30s for all operations 2023-06-19 08:59:51 -07:00
Trenton Holmes
05188aed6d Bumps our locked pipenv version for CI and Docker image builds 2023-06-18 10:09:38 -07:00
Trenton Holmes
865efb7752 Sets the retention days for all uploaded artifacts to be 7 days after upload 2023-06-18 10:08:36 -07:00
Trenton Holmes
4782b4da07 Adds better error handling/checking around getting content of a document via Tika
Signed-off-by: Trenton Holmes <797416+stumpylog@users.noreply.github.com>
2023-06-18 08:39:17 -07:00
Daniel Dietzler
4693632c7d Feature: separate save / save & close buttons (#3575)
* Add setting to decide whether the edit dialog should automatically close on save

* Add the actual button to the ui

* Revert "Add the actual button to the ui"

This reverts commit e1f5a8bde0.

* Revert "Add setting to decide whether the edit dialog should automatically close on save"

This reverts commit feef3c909b.

* Add button for save without exit

* Correct save button ordering, ensure perms, update translation strings

* fix e2e tests

* Add unit testing for save / save & close button

---------

Update messages.xlf

Update document-detail.component.spec.ts

Co-Authored-By: shamoon <4887959+shamoon@users.noreply.github.com>
2023-06-18 08:06:54 -07:00
shamoon
4c4b571a88 Merge pull request #3597 from paperless-ngx/frontend-unit-testing
Development: frontend unit testing
2023-06-17 21:13:34 -07:00
Trenton H
328c87995b Experiment with a buildx cache mount for pip's directory. Does it increase image size? 2023-06-17 20:13:38 -07:00
shamoon
a1d10e7d4a Update stale.yml 2023-06-17 20:02:44 -07:00
shamoon
77d9a7e9d3 Upload frontend coverage to codecov 2023-06-16 19:49:01 -07:00
shamoon
981b090088 Update frontend testing dev documentation 2023-06-16 19:38:00 -07:00
shamoon
6d1c788ee0 Merge pull request #3608 from paperless-ngx/v1.16.1-changelog
[Documentation] Add v1.16.1 changelog
2023-06-16 12:24:35 -07:00
github-actions
02de773d5b Changelog v1.16.1 - GHA 2023-06-16 17:25:54 +00:00
Trenton H
2a240d83fd Reset -dev version tagging 2023-06-16 10:08:36 -07:00
Trenton H
25cdf7916d Bumps version to 1.16.1 2023-06-16 09:56:40 -07:00
Trenton H
11b5983a0d Merge remote-tracking branch 'origin/dev' 2023-06-16 09:55:32 -07:00
Trenton H
4964987245 Fixes the image cleaner not actually deleting the old images 2023-06-16 09:19:03 -07:00
Trenton H
ed129d6074 Updates to a build of Pillow which builds against libtiff6 instead of expecting libtiff5 2023-06-16 09:14:44 -07:00
shamoon
37e928d869 Run jest tests in ci & upload coverage
update playwright
2023-06-16 07:36:41 -07:00
shamoon
06def8c11e frontend unit tests
toasts component testing

conditional import of angular setup-jest for vscode-jest support

Update jest.config.js

Create open-documents.service.spec.ts

Add unit tests for all REST services

settings service test

Remove component from settings service test

Create permissions.service.spec.ts

upload documents service tests

Update package.json

Create toast.service.spec.ts

Tasks service test

Statistics widget component tests

Update permissions.service.ts

Create app.component.spec.ts

settings component testing

tasks component unit testing

Management list component generic tests

Some management component tests

document notes component unit tests

Create document-list.component.spec.ts

Create save-view-config-dialog.component.spec.ts

Create filter-editor.component.spec.ts

small and large document cards unit testing

Create bulk-editor.component.spec.ts

document detail unit tests

saving work on documentdetail component spec

Create document-asn.component.spec.ts

dashboard & widgets unit testing

Fix ResizeObserver mock

common component unit tests

fix some merge errors

Update app-frame.component.spec.ts

Create page-header.component.spec.ts

input component unit tests

FilterableDropdownComponent unit testing

and found minor errors

update taskservice unit tests

Edit dialogs unit tests

Create date-dropdown.component.spec.ts

Remove selectors from guard tests

confirm dialog component tests

app frame component test

Miscellaneous component tests

Update document-list-view.service.spec.ts

directives unit tests

Remove unused resizeobserver mock

guard unit tests

Update query-params.spec.ts

try to fix flaky playwright

filter rules utils & testing

Interceptor unit tests

Pipes unit testing

Utils unit tests

Update upload-documents.service.spec.ts

consumer status service tests

Update setup-jest.ts

Create document-list-view.service.spec.ts

Update app-routing.module.ts
2023-06-15 23:53:04 -07:00
github-actions[bot]
10571676a4 Changelog v1.16.0 - GHA (#3595)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2023-06-15 13:57:16 -07:00
Trenton H
af5160237d Resets version tag to -dev 2023-06-15 10:30:18 -07:00
325 changed files with 48225 additions and 21093 deletions

View File

@@ -1,3 +1,17 @@
codecov:
require_ci_to_pass: true
# https://docs.codecov.com/docs/flags#recommended-automatic-flag-management
# Require each flag to have 1 upload before notification
flag_management:
default_rules:
after_n_builds: 1
individual_flags:
- name: backend
paths:
- src/
- name: frontend
paths:
- src-ui/
# https://docs.codecov.com/docs/pull-request-comments
# codecov will only comment if coverage changes
comment:

View File

@@ -20,11 +20,16 @@ NOTE: Please check only one box!
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Other (please explain)
- [ ] Other (please explain):
## Checklist:
<!--
NOTE: PRs that do not address the following will not be merged, please do not skip any relevant items.
-->
- [ ] I have read & agree with the [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).

View File

@@ -17,6 +17,21 @@ updates:
# Add reviewers
reviewers:
- "paperless-ngx/frontend"
groups:
frontend-angular-dependencies:
patterns:
- "@angular*"
- "@ng-*"
- "ngx-*"
- "ng2-pdf-viewer"
frontend-jest-dependencies:
patterns:
- "@types/jest"
- "jest"
frontend-eslint-dependencies:
patterns:
- "@typescript-eslint*"
- "eslint"
# Enable version updates for Python
- package-ecosystem: "pip"

23
.github/stale.yml vendored
View File

@@ -1,23 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 30
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: [cant-reproduce]
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
# See https://github.com/marketplace/stale for more info on the app
# and https://github.com/probot/stale for the configuration docs

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.4.20"
DEFAULT_PIP_ENV_VERSION: "2023.7.23"
# This is the default version of Python to use in most steps
# If changing this, change Dockerfile
DEFAULT_PYTHON_VERSION: "3.9"
@@ -77,6 +77,7 @@ jobs:
with:
name: documentation
path: site/
retention-days: 7
documentation-deploy:
name: "Deploy Documentation"
@@ -199,15 +200,36 @@ jobs:
name: Linting checks
run: cd src-ui && npm run lint
-
name: Run Playwright tests
name: Run Jest unit tests
run: cd src-ui && npm run test
-
name: Upload Jest coverage
if: always()
uses: actions/upload-artifact@v3
with:
name: jest-coverage-report
path: src-ui/coverage
retention-days: 7
-
name: Upload frontend coverage to Codecov
if: always()
uses: codecov/codecov-action@v3
with:
# not required for public repos, but intermittently fails otherwise
token: ${{ secrets.CODECOV_TOKEN }}
# future expansion
flags: frontend
-
name: Run Playwright e2e tests
run: cd src-ui && npx playwright test
-
name: Upload test results
name: Upload Playwright test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: src-ui/playwright-report
retention-days: 7
build-docker-image:
name: Build Docker image for ${{ github.ref_name }}
@@ -328,6 +350,7 @@ jobs:
with:
name: frontend-compiled
path: src/documents/static/frontend/
retention-days: 7
build-release:
needs:
@@ -436,6 +459,7 @@ jobs:
with:
name: release
path: dist/paperless-ngx.tar.xz
retention-days: 7
publish-release:
runs-on: ubuntu-22.04

View File

@@ -29,7 +29,7 @@ jobs:
-
name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.1.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.2.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
@@ -38,6 +38,7 @@ jobs:
scheme: "branch"
repo_name: "paperless-ngx"
match_regex: "feature-"
do_delete: "true"
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
@@ -67,9 +68,10 @@ jobs:
-
name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.1.0
uses: stumpylog/image-cleaner-action/untagged@v0.2.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
is_org: "true"
package_name: "${{ matrix.primary-name }}"
do_delete: "true"

View File

@@ -19,9 +19,9 @@ jobs:
steps:
- uses: actions/stale@v8
with:
days-before-stale: 30
days-before-close: 7
only-labels: 'cant-reproduce'
days-before-stale: 7
days-before-close: 14
any-of-labels: 'cant-reproduce,not a bug'
stale-issue-label: stale
stale-pr-label: stale
stale-issue-message: >

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: 'v3.0.0'
hooks:
- id: prettier
types_or:
@@ -37,11 +37,11 @@ repos:
exclude: "(^Pipfile\\.lock$)"
# Python hooks
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.272'
rev: 'v0.0.280'
hooks:
- id: ruff
- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.7.0
hooks:
- id: black
# Dockerfile hooks

View File

@@ -2,3 +2,5 @@
semi: false
# https://prettier.io/docs/en/options.html#quotes
singleQuote: true
# https://prettier.io/docs/en/options.html#trailing-commas
trailingComma: "es5"

View File

@@ -2,7 +2,7 @@
# https://beta.ruff.rs/docs/rules/
extend-select = ["I", "W", "UP", "COM", "DJ", "EXE", "ISC", "ICN", "G201", "INP", "PIE", "RSE", "SIM", "TID", "PLC", "PLE", "RUF"]
# TODO PTH
ignore = ["DJ001", "SIM105"]
ignore = ["DJ001", "SIM105", "RUF012"]
fix = true
line-length = 88
respect-gitignore = true

View File

@@ -45,7 +45,7 @@ Examples of `non-trivial` PRs might include:
- Additional features
- Large changes to many distinct files
- Breaking or depreciation of existing features
- Breaking or deprecation of existing features
Our community review process for `non-trivial` PRs is the following:

View File

@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.4
# syntax=docker/dockerfile:1
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md
# Stage: compile-frontend
@@ -29,7 +29,7 @@ COPY Pipfile* ./
RUN set -eux \
&& echo "Installing pipenv" \
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.4.20 \
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.7.23 \
&& echo "Generating requirement.txt" \
&& pipenv requirements > requirements.txt
@@ -183,7 +183,7 @@ ARG PSYCOPG2_VERSION=2.9.6
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/builder/archive/1f0e6665ba1b144f70fd6dfc8d0e8ba3b7a578ee.tar.gz \
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/builder/archive/58bb061b9b3b63009852d6d875f9a305d9ae6ac9.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
@@ -214,15 +214,18 @@ COPY --from=pipenv-base /usr/src/pipenv/requirements.txt ./
ARG BUILD_PACKAGES="\
build-essential \
git \
default-libmysqlclient-dev"
default-libmysqlclient-dev \
pkg-config"
RUN set -eux \
# hadolint ignore=DL3042
RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
set -eux \
&& echo "Installing build system packages" \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
&& python3 -m pip install --no-cache-dir --upgrade wheel \
&& echo "Installing Python requirements" \
&& python3 -m pip install --default-timeout=1000 --no-cache-dir --requirement requirements.txt \
&& python3 -m pip install --default-timeout=1000 --requirement requirements.txt \
&& echo "Installing NLTK data" \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \

15
Pipfile
View File

@@ -18,7 +18,7 @@ django-celery-results = "*"
django-compression-middleware = "*"
django-guardian = "*"
django-extensions = "*"
django-filter = "~=22.1"
django-filter = "~=23.1"
djangorestframework = "~=3.14"
djangorestframework-guardian = "*"
filelock = "*"
@@ -26,8 +26,6 @@ gunicorn = "*"
imap-tools = "*"
langdetect = "*"
pathvalidate = "*"
pillow = "*"
pikepdf = "*"
python-gnupg = "*"
python-dotenv = "*"
python-dateutil = "*"
@@ -36,9 +34,9 @@ python-ipware = "*"
psycopg2 = "*"
rapidfuzz = "*"
redis = {extras = ["hiredis"], version = "*"}
scikit-learn = "~=1.2"
whitenoise = "~=6.3"
watchdog = "~=2.2"
scikit-learn = "~=1.3"
whitenoise = "~=6.5"
watchdog = "~=3.0"
whoosh="~=2.7"
inotifyrecursive = "~=0.3"
ocrmypdf = "~=14.0"
@@ -64,8 +62,10 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
scipy = "==1.8.1"
# v4 brings in extra dependencies for features not used here
reportlab = "==3.6.12"
# Pin this until piwheels is building a newer version (see https://www.piwheels.org/project/cryptography/)
# Pin these until piwheels is building a newer version (see https://www.piwheels.org/project/{package}/)
cryptography = "==40.0.1"
pikepdf = "==7.2.0"
pillow = "==9.5.0"
[dev-packages]
# Linting
@@ -81,6 +81,7 @@ pytest-httpx = "*"
pytest-env = "*"
pytest-sugar = "*"
pytest-xdist = "*"
pytest-rerunfailures = "*"
"pdfminer.six" = "*"
imagehash = "*"
daphne = "*"

1972
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,7 @@ services:
- redisdata:/data
db:
image: docker.io/library/postgres:13
image: docker.io/library/postgres:15
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data

View File

@@ -39,7 +39,7 @@ services:
- redisdata:/data
db:
image: docker.io/library/postgres:13
image: docker.io/library/postgres:15
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data

View File

@@ -35,7 +35,7 @@ services:
- redisdata:/data
db:
image: docker.io/library/postgres:13
image: docker.io/library/postgres:15
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data

View File

@@ -15,6 +15,7 @@ stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment = HOME="/usr/src/paperless",USER="paperless"
[program:consumer]
command=python3 manage.py document_consumer
@@ -25,6 +26,7 @@ stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment = HOME="/usr/src/paperless",USER="paperless"
[program:celery]
@@ -37,6 +39,7 @@ stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment = HOME="/usr/src/paperless",USER="paperless"
[program:celery-beat]
@@ -48,6 +51,7 @@ stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment = HOME="/usr/src/paperless",USER="paperless"
[program:celery-flower]
command = /usr/local/bin/flower-conditional.sh
@@ -58,3 +62,4 @@ stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment = HOME="/usr/src/paperless",USER="paperless"

View File

@@ -68,23 +68,23 @@ $ docker-compose down
After that, [make a backup](#backup).
1. If you pull the image from the docker hub, all you need to do is:
1. If you pull the image from the docker hub, all you need to do is:
```shell-session
$ docker-compose pull
$ docker-compose up
```
```shell-session
$ docker-compose pull
$ docker-compose up
```
The docker-compose files refer to the `latest` version, which is
always the latest stable release.
The docker-compose files refer to the `latest` version, which is
always the latest stable release.
2. If you built the image yourself, do the following:
1. If you built the image yourself, do the following:
```shell-session
$ git pull
$ docker-compose build
$ docker-compose up
```
```shell-session
$ git pull
$ docker-compose build
$ docker-compose up
```
Running `docker-compose up` will also apply any new database migrations.
If you see everything working, press CTRL+C once to gracefully stop
@@ -167,6 +167,16 @@ following:
This might not actually do anything. Not every new paperless version
comes with new database migrations.
### Database Upgrades
In general, paperless does not require a specific version of PostgreSQL or MariaDB and it is
safe to update them to newer versions. However, you should always take a backup and follow
the instructions from your database's documentation for how to upgrade between major versions.
For PostgreSQL, refer to [Upgrading a PostgreSQL Cluster](https://www.postgresql.org/docs/current/upgrading.html).
For MariaDB, refer to [Upgrading MariaDB](https://mariadb.com/kb/en/upgrading/)
## Downgrading Paperless {#downgrade-paperless}
Downgrades are possible. However, some updates also contain database
@@ -460,7 +470,7 @@ The issues detected by the sanity checker are as follows:
- Inaccessible thumbnails due to improper permissions.
- Documents without any content (warning).
- Orphaned files in the media directory (warning). These are files
that are not referenced by any document im paperless.
that are not referenced by any document in paperless.
```
document_sanity_checker

View File

@@ -1,6 +1,6 @@
# Advanced Topics
Paperless offers a couple features that automate certain tasks and make
Paperless offers a couple of features that automate certain tasks and make
your life easier.
## Matching tags, correspondents, document types, and storage paths {#matching}
@@ -35,9 +35,9 @@ The following algorithms are available:
(i.e. preserve ordering) in the PDF.
- **Regular expression:** Parses the match as a regular expression and
tries to find a match within the document.
- **Fuzzy match:** I don't know. Look at the source.
- **Fuzzy match:** I don't know. Look at [the source](https://github.com/paperless-ngx/paperless-ngx/blob/main/src/documents/matching.py).
- **Auto:** Tries to automatically match new documents. This does not
require you to set a match. See the notes below.
require you to set a match. See the [notes below](#automatic-matching).
When using the _any_ or _all_ matching algorithms, you can search for
terms that consist of multiple words by enclosing them in double quotes.
@@ -92,7 +92,7 @@ when using this feature:
decide when not to assign a certain tag, correspondent, document
type, or storage path. This will usually be the case as you start
filling up paperless with documents. Example: If all your documents
are either from "Webshop" and "Bank", paperless will assign one
are either from "Webshop" or "Bank", paperless will assign one
of these correspondents to ANY new document, if both are set to
automatic matching.
@@ -101,7 +101,7 @@ when using this feature:
Sometimes you may want to do something arbitrary whenever a document is
consumed. Rather than try to predict what you may want to do, Paperless
lets you execute scripts of your own choosing just before or after a
document is consumed using a couple simple hooks.
document is consumed using a couple of simple hooks.
Just write a script, put it somewhere that Paperless can read & execute,
and then put the path to that script in `paperless.conf` or
@@ -197,7 +197,7 @@ The script can be in any language, A simple shell script example:
!!! warning
The post consumption script should not modify the document files
directly
directly.
The script's stdout and stderr will be logged line by line to the
webserver log, along with the exit code of the script.
@@ -311,6 +311,7 @@ Paperless provides the following placeholders within filenames:
- `{added_day}`: Day added only (number 01-31).
- `{owner_username}`: Username of document owner, if any, or "none"
- `{original_name}`: Document original filename, minus the extension, if any, or "none"
- `{doc_pk}`: The paperless identifier (primary key) for the document.
Paperless will try to conserve the information from your database as
much as possible. However, some characters that you can use in document
@@ -528,7 +529,7 @@ For how to enable barcode usage, see [the configuration](/configuration#barcodes
The two settings may be enabled independently, but do have interactions as explained
below.
### Document Splitting
### Document Splitting {#document-splitting}
When enabled, Paperless will look for a barcode with the configured value and create a new document
starting from the next page. The page with the barcode on it will _not_ be retained. It
@@ -543,3 +544,75 @@ If document splitting via barcode is also enabled, documents will be split when
barcode is located. However, differing from the splitting, the page with the
barcode _will_ be retained. This allows application of a barcode to any page, including
one which holds data to keep in the document.
## Automatic collation of double-sided documents {#collate}
!!! note
If your scanner supports double-sided scanning natively, you do not need this feature.
This feature is turned off by default, see [configuration](/configuration#collate) on how to turn it on.
### Summary
If you have a scanner with an automatic document feeder (ADF) that only scans a single side,
this feature makes scanning double-sided documents much more convenient by automatically
collating two separate scans into one document, reordering the pages as necessary.
### Usage example
Suppose you have a double-sided document with 6 pages (3 sheets of paper). First,
put the stack into your ADF as normal, ensuring that page 1 is scanned first. Your ADF
will now scan pages 1, 3, and 5. Then you (or your the scanner, if it supports it) upload
the scan into the correct sub-directory of the consume folder (`double-sided` by default;
keep in mind that Paperless will _not_ automatically create the directory for you.)
Paperless will then process the scan and move it into an internal staging area.
The next step is to turn your stack upside down (without reordering the sheets of paper),
and scan it once again, your ADF will now scan pages 6, 4, and 2, in that order. Once this
scan is copied into the sub-directory, Paperless will collate the previous scan with the
new one, reversing the order of the pages on the second, "even numbered" scan. The
resulting document will have the pages 1-6 in the correct order, and this new file will
then be processed as normal.
!!! tip
When scanning the even numbered pages, you can omit the last empty pages, if there are
any. For example, if page 6 is empty, you only need to scan pages 2 and 4. _Do not_ omit
empty pages in the middle of the document.
### Things that could go wrong
Paperless will notice when the first, "odd numbered" scan has less pages than the second
scan (this can happen when e.g. the ADF skipped a few pages in the first pass). In that
case, Paperless will remove the staging copy as well as the scan, and give you an error
message asking you to restart the process from scratch, by scanning the odd pages again,
followed by the even pages.
It's important that the scan files get consumed in the correct order, and one at a time.
You therefore need to make sure that Paperless is running while you upload the files into
the directory; and if you're using [polling](/configuration#polling), make sure that
`CONSUMER_POLLING` is set to a value lower than it takes for the second scan to appear,
like 5-10 or even lower.
Another thing that might happen is that you start a double sided scan, but then forget
to upload the second file. To avoid collating the wrong documents if you then come back
a day later to scan a new double-sided document, Paperless will only keep an "odd numbered
pages" file for up to 30 minutes. If more time passes, it will consider the next incoming
scan a completely new "odd numbered pages" one. The old staging file will get discarded.
### Interaction with "subdirs as tags"
The collation feature can be used together with the [subdirs as tags](/configuration#consume_config)
feature (but this is not a requirement). Just create a correctly named double-sided subdir
in the hierachy and upload your scans there. For example, both `double-sided/foo/bar` as
well as `foo/bar/double-sided` will cause the collated document to be treated as if it
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
### Interaction with document splitting
You can use the [document splitting](#document-splitting) feature, but if you use a normal
single-sided split marker page, the split document(s) will have an empty page at the front (or
whatever else was on the backside of the split marker page.) You can work around that by having
a split marker page that has the split barcode on _both_ sides. This way, the extra page will
get automatically removed.

View File

@@ -288,10 +288,23 @@ with an optional `set_permissions` parameter which is of the form:
}
```
!!! note
Arrays should contain user or group ID numbers.
If this parameter is supplied the object's permissions will be overwritten,
assuming the authenticated user has permission to do so (the user must be
the object owner or a superuser).
### Retrieving full permissions
By default, the API will return a truncated version of object-level
permissions, returning `user_can_change` indicating whether the current user
can edit the object (either because they are the object owner or have permissions
granted). You can pass the parameter `full_perms=true` to API calls to view the
full permissions of objects in a format that mirrors the `set_permissions`
parameter above.
## API Versioning
The REST API is versioned since Paperless-ngx 1.3.0.

View File

@@ -1,5 +1,286 @@
# Changelog
## paperless-ngx 1.17.1
### Features
- Fix / Enhancement: restrict status messages by owner if set \& improve 404 page [@shamoon](https://github.com/shamoon) ([#3959](https://github.com/paperless-ngx/paperless-ngx/pull/3959))
- Feature: Add Ukrainian translation [@shamoon](https://github.com/shamoon) ([#3941](https://github.com/paperless-ngx/paperless-ngx/pull/3941))
### Bug Fixes
- Fix: handle ASN = 0 on frontend cards [@shamoon](https://github.com/shamoon) ([#3988](https://github.com/paperless-ngx/paperless-ngx/pull/3988))
- Fix: improve light color filled primary button text legibility [@shamoon](https://github.com/shamoon) ([#3980](https://github.com/paperless-ngx/paperless-ngx/pull/3980))
- Fix / Enhancement: restrict status messages by owner if set \& improve 404 page [@shamoon](https://github.com/shamoon) ([#3959](https://github.com/paperless-ngx/paperless-ngx/pull/3959))
- Fix: handle very old date strings in correspondent list [@shamoon](https://github.com/shamoon) ([#3953](https://github.com/paperless-ngx/paperless-ngx/pull/3953))
### Documentation
- docs(bare-metal): add new dependency [@bin101](https://github.com/bin101) ([#3931](https://github.com/paperless-ngx/paperless-ngx/pull/3931))
### Dependencies
- Chore: Loosen Pipfile restriction on some packages and update them [@stumpylog](https://github.com/stumpylog) ([#3972](https://github.com/paperless-ngx/paperless-ngx/pull/3972))
### All App Changes
<details>
<summary>6 changes</summary>
- Fix: handle ASN = 0 on frontend cards [@shamoon](https://github.com/shamoon) ([#3988](https://github.com/paperless-ngx/paperless-ngx/pull/3988))
- Fix: improve light color filled primary button text legibility [@shamoon](https://github.com/shamoon) ([#3980](https://github.com/paperless-ngx/paperless-ngx/pull/3980))
- Fix / Enhancement: restrict status messages by owner if set \& improve 404 page [@shamoon](https://github.com/shamoon) ([#3959](https://github.com/paperless-ngx/paperless-ngx/pull/3959))
- Fix: handle very old date strings in correspondent list [@shamoon](https://github.com/shamoon) ([#3953](https://github.com/paperless-ngx/paperless-ngx/pull/3953))
- Chore: Reduces the 2 mail tests flakiness [@stumpylog](https://github.com/stumpylog) ([#3949](https://github.com/paperless-ngx/paperless-ngx/pull/3949))
- Feature: Add Ukrainian translation [@shamoon](https://github.com/shamoon) ([#3941](https://github.com/paperless-ngx/paperless-ngx/pull/3941))
</details>
## paperless-ngx 1.17.0
### Features
- Add support for additional UK date formats [@brainrecursion](https://github.com/brainrecursion) ([#3887](https://github.com/paperless-ngx/paperless-ngx/pull/3887))
- Add 'doc_pk' to PAPERLESS_FILENAME_FORMAT handling [@mechanarchy](https://github.com/mechanarchy) ([#3861](https://github.com/paperless-ngx/paperless-ngx/pull/3861))
- Feature: hover buttons for saved view widgets [@shamoon](https://github.com/shamoon) ([#3875](https://github.com/paperless-ngx/paperless-ngx/pull/3875))
- Feature: collate two single-sided multipage scans [@brakhane](https://github.com/brakhane) ([#3784](https://github.com/paperless-ngx/paperless-ngx/pull/3784))
- Feature: include global and object-level permissions in export / import [@shamoon](https://github.com/shamoon) ([#3672](https://github.com/paperless-ngx/paperless-ngx/pull/3672))
- Enhancement / Fix: Migrate encrypted png thumbnails to webp [@shamoon](https://github.com/shamoon) ([#3719](https://github.com/paperless-ngx/paperless-ngx/pull/3719))
- Feature: Add Slovak translation [@shamoon](https://github.com/shamoon) ([#3722](https://github.com/paperless-ngx/paperless-ngx/pull/3722))
### Bug Fixes
- Fix: cancel possibly slow queries on doc details [@shamoon](https://github.com/shamoon) ([#3925](https://github.com/paperless-ngx/paperless-ngx/pull/3925))
- Fix: note creation / deletion should respect doc permissions [@shamoon](https://github.com/shamoon) ([#3903](https://github.com/paperless-ngx/paperless-ngx/pull/3903))
- Fix: notes show persistent scrollbars [@shamoon](https://github.com/shamoon) ([#3904](https://github.com/paperless-ngx/paperless-ngx/pull/3904))
- Fix: Provide SSL context to IMAP client [@stumpylog](https://github.com/stumpylog) ([#3886](https://github.com/paperless-ngx/paperless-ngx/pull/3886))
- Fix/enhancement: permissions for mail rules \& accounts [@shamoon](https://github.com/shamoon) ([#3869](https://github.com/paperless-ngx/paperless-ngx/pull/3869))
- Fix: Classifier special case when no items are set to automatic matching [@stumpylog](https://github.com/stumpylog) ([#3858](https://github.com/paperless-ngx/paperless-ngx/pull/3858))
- Fix: issues with copy2 or copystat and SELinux permissions [@stumpylog](https://github.com/stumpylog) ([#3847](https://github.com/paperless-ngx/paperless-ngx/pull/3847))
- Fix: Parsing office document timestamps [@stumpylog](https://github.com/stumpylog) ([#3836](https://github.com/paperless-ngx/paperless-ngx/pull/3836))
- Fix: Add warning to install script need for permissions [@shamoon](https://github.com/shamoon) ([#3835](https://github.com/paperless-ngx/paperless-ngx/pull/3835))
- Fix interaction between API and barcode archive serial number [@stumpylog](https://github.com/stumpylog) ([#3834](https://github.com/paperless-ngx/paperless-ngx/pull/3834))
- Enhancement / Fix: Migrate encrypted png thumbnails to webp [@shamoon](https://github.com/shamoon) ([#3719](https://github.com/paperless-ngx/paperless-ngx/pull/3719))
- Fix: add UI tour step padding [@hakimio](https://github.com/hakimio) ([#3791](https://github.com/paperless-ngx/paperless-ngx/pull/3791))
- Fix: translate file tasks types in footer [@shamoon](https://github.com/shamoon) ([#3749](https://github.com/paperless-ngx/paperless-ngx/pull/3749))
- Fix: limit ng-select size for addition of filter button [@shamoon](https://github.com/shamoon) ([#3731](https://github.com/paperless-ngx/paperless-ngx/pull/3731))
### Documentation
- Documentation: improvements to grammar, spelling, indentation [@mechanarchy](https://github.com/mechanarchy) ([#3844](https://github.com/paperless-ngx/paperless-ngx/pull/3844))
### Maintenance
- Bump stumpylog/image-cleaner-action from 0.1.0 to 0.2.0 [@dependabot](https://github.com/dependabot) ([#3910](https://github.com/paperless-ngx/paperless-ngx/pull/3910))
- Chore: group frontend angular dependabot updates [@shamoon](https://github.com/shamoon) ([#3750](https://github.com/paperless-ngx/paperless-ngx/pull/3750))
### Dependencies
<details>
<summary>17 changes</summary>
- Chore: Bump the frontend-angular-dependencies group in /src-ui with 11 updates [@shamoon](https://github.com/shamoon) ([#3918](https://github.com/paperless-ngx/paperless-ngx/pull/3918))
- Bump stumpylog/image-cleaner-action from 0.1.0 to 0.2.0 [@dependabot](https://github.com/dependabot) ([#3910](https://github.com/paperless-ngx/paperless-ngx/pull/3910))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#3911](https://github.com/paperless-ngx/paperless-ngx/pull/3911))
- Bump tslib from 2.6.0 to 2.6.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3909](https://github.com/paperless-ngx/paperless-ngx/pull/3909))
- Bump jest-environment-jsdom from 29.5.0 to 29.6.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3916](https://github.com/paperless-ngx/paperless-ngx/pull/3916))
- Bump [@<!---->types/node from 20.3.3 to 20.4.5 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.3.3 to 20.4.5 in /src-ui @dependabot) ([#3915](https://github.com/paperless-ngx/paperless-ngx/pull/3915))
- Bump bootstrap from 5.3.0 to 5.3.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3914](https://github.com/paperless-ngx/paperless-ngx/pull/3914))
- Bump [@<!---->playwright/test from 1.36.1 to 1.36.2 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.36.1 to 1.36.2 in /src-ui @dependabot) ([#3912](https://github.com/paperless-ngx/paperless-ngx/pull/3912))
- Bump the frontend-jest-dependencies group in /src-ui with 1 update [@dependabot](https://github.com/dependabot) ([#3906](https://github.com/paperless-ngx/paperless-ngx/pull/3906))
- Chore: Update dependencies [@stumpylog](https://github.com/stumpylog) ([#3883](https://github.com/paperless-ngx/paperless-ngx/pull/3883))
- Chore: Update Python dependencies [@stumpylog](https://github.com/stumpylog) ([#3842](https://github.com/paperless-ngx/paperless-ngx/pull/3842))
- Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#3826](https://github.com/paperless-ngx/paperless-ngx/pull/3826))
- Bump [@<!---->typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui @dependabot) ([#3829](https://github.com/paperless-ngx/paperless-ngx/pull/3829))
- Bump jest and [@<!---->types/jest in /src-ui @dependabot](https://github.com/<!---->types/jest in /src-ui @dependabot) ([#3828](https://github.com/paperless-ngx/paperless-ngx/pull/3828))
- Bump [@<!---->playwright/test from 1.36.0 to 1.36.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.36.0 to 1.36.1 in /src-ui @dependabot) ([#3827](https://github.com/paperless-ngx/paperless-ngx/pull/3827))
- Bump semver from 5.7.1 to 5.7.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3793](https://github.com/paperless-ngx/paperless-ngx/pull/3793))
- Chore: Bump Angular to v16 and other frontend packages [@dependabot](https://github.com/dependabot) ([#3727](https://github.com/paperless-ngx/paperless-ngx/pull/3727))
</details>
### All App Changes
<details>
<summary>35 changes</summary>
- Fix: cancel possibly slow queries on doc details [@shamoon](https://github.com/shamoon) ([#3925](https://github.com/paperless-ngx/paperless-ngx/pull/3925))
- [BUG] Set office document creation date with timezone, if it is naive [@a17t](https://github.com/a17t) ([#3760](https://github.com/paperless-ngx/paperless-ngx/pull/3760))
- Fix: note creation / deletion should respect doc permissions [@shamoon](https://github.com/shamoon) ([#3903](https://github.com/paperless-ngx/paperless-ngx/pull/3903))
- Chore: Bump the frontend-angular-dependencies group in /src-ui with 11 updates [@shamoon](https://github.com/shamoon) ([#3918](https://github.com/paperless-ngx/paperless-ngx/pull/3918))
- Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#3911](https://github.com/paperless-ngx/paperless-ngx/pull/3911))
- Bump tslib from 2.6.0 to 2.6.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3909](https://github.com/paperless-ngx/paperless-ngx/pull/3909))
- Bump jest-environment-jsdom from 29.5.0 to 29.6.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3916](https://github.com/paperless-ngx/paperless-ngx/pull/3916))
- Bump [@<!---->types/node from 20.3.3 to 20.4.5 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.3.3 to 20.4.5 in /src-ui @dependabot) ([#3915](https://github.com/paperless-ngx/paperless-ngx/pull/3915))
- Bump bootstrap from 5.3.0 to 5.3.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#3914](https://github.com/paperless-ngx/paperless-ngx/pull/3914))
- Bump [@<!---->playwright/test from 1.36.1 to 1.36.2 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.36.1 to 1.36.2 in /src-ui @dependabot) ([#3912](https://github.com/paperless-ngx/paperless-ngx/pull/3912))
- Bump the frontend-jest-dependencies group in /src-ui with 1 update [@dependabot](https://github.com/dependabot) ([#3906](https://github.com/paperless-ngx/paperless-ngx/pull/3906))
- Fix: notes show persistent scrollbars [@shamoon](https://github.com/shamoon) ([#3904](https://github.com/paperless-ngx/paperless-ngx/pull/3904))
- Add support for additional UK date formats [@brainrecursion](https://github.com/brainrecursion) ([#3887](https://github.com/paperless-ngx/paperless-ngx/pull/3887))
- Add 'doc_pk' to PAPERLESS_FILENAME_FORMAT handling [@mechanarchy](https://github.com/mechanarchy) ([#3861](https://github.com/paperless-ngx/paperless-ngx/pull/3861))
- Fix: Provide SSL context to IMAP client [@stumpylog](https://github.com/stumpylog) ([#3886](https://github.com/paperless-ngx/paperless-ngx/pull/3886))
- Feature: hover buttons for saved view widgets [@shamoon](https://github.com/shamoon) ([#3875](https://github.com/paperless-ngx/paperless-ngx/pull/3875))
- Fix/enhancement: permissions for mail rules \& accounts [@shamoon](https://github.com/shamoon) ([#3869](https://github.com/paperless-ngx/paperless-ngx/pull/3869))
- Chore: typing improvements [@stumpylog](https://github.com/stumpylog) ([#3860](https://github.com/paperless-ngx/paperless-ngx/pull/3860))
- Fix: Classifier special case when no items are set to automatic matching [@stumpylog](https://github.com/stumpylog) ([#3858](https://github.com/paperless-ngx/paperless-ngx/pull/3858))
- Fix: issues with copy2 or copystat and SELinux permissions [@stumpylog](https://github.com/stumpylog) ([#3847](https://github.com/paperless-ngx/paperless-ngx/pull/3847))
- Chore: Update Python dependencies [@stumpylog](https://github.com/stumpylog) ([#3842](https://github.com/paperless-ngx/paperless-ngx/pull/3842))
- Feature: include global and object-level permissions in export / import [@shamoon](https://github.com/shamoon) ([#3672](https://github.com/paperless-ngx/paperless-ngx/pull/3672))
- Fix: Parsing office document timestamps [@stumpylog](https://github.com/stumpylog) ([#3836](https://github.com/paperless-ngx/paperless-ngx/pull/3836))
- Fix interaction between API and barcode archive serial number [@stumpylog](https://github.com/stumpylog) ([#3834](https://github.com/paperless-ngx/paperless-ngx/pull/3834))
- Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#3826](https://github.com/paperless-ngx/paperless-ngx/pull/3826))
- Enhancement / Fix: Migrate encrypted png thumbnails to webp [@shamoon](https://github.com/shamoon) ([#3719](https://github.com/paperless-ngx/paperless-ngx/pull/3719))
- Bump [@<!---->typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/eslint-plugin from 5.60.1 to 6.1.0 in /src-ui @dependabot) ([#3829](https://github.com/paperless-ngx/paperless-ngx/pull/3829))
- Bump jest and [@<!---->types/jest in /src-ui @dependabot](https://github.com/<!---->types/jest in /src-ui @dependabot) ([#3828](https://github.com/paperless-ngx/paperless-ngx/pull/3828))
- Bump [@<!---->playwright/test from 1.36.0 to 1.36.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.36.0 to 1.36.1 in /src-ui @dependabot) ([#3827](https://github.com/paperless-ngx/paperless-ngx/pull/3827))
- Bump semver from 5.7.1 to 5.7.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3793](https://github.com/paperless-ngx/paperless-ngx/pull/3793))
- Fix: add UI tour step padding [@hakimio](https://github.com/hakimio) ([#3791](https://github.com/paperless-ngx/paperless-ngx/pull/3791))
- Fix: translate file tasks types in footer [@shamoon](https://github.com/shamoon) ([#3749](https://github.com/paperless-ngx/paperless-ngx/pull/3749))
- Feature: Add Slovak translation [@shamoon](https://github.com/shamoon) ([#3722](https://github.com/paperless-ngx/paperless-ngx/pull/3722))
- Fix: limit ng-select size for addition of filter button [@shamoon](https://github.com/shamoon) ([#3731](https://github.com/paperless-ngx/paperless-ngx/pull/3731))
- Chore: Bump Angular to v16 and other frontend packages [@dependabot](https://github.com/dependabot) ([#3727](https://github.com/paperless-ngx/paperless-ngx/pull/3727))
</details>
## paperless-ngx 1.16.5
### Features
- Feature: support barcode upscaling for better detection of small barcodes [@bmachek](https://github.com/bmachek) ([#3655](https://github.com/paperless-ngx/paperless-ngx/pull/3655))
### Bug Fixes
- Fix: owner removed when set_permissions passed on object create [@shamoon](https://github.com/shamoon) ([#3702](https://github.com/paperless-ngx/paperless-ngx/pull/3702))
### All App Changes
<details>
<summary>2 changes</summary>
- Feature: support barcode upscaling for better detection of small barcodes [@bmachek](https://github.com/bmachek) ([#3655](https://github.com/paperless-ngx/paperless-ngx/pull/3655))
- Fix: owner removed when set_permissions passed on object create [@shamoon](https://github.com/shamoon) ([#3702](https://github.com/paperless-ngx/paperless-ngx/pull/3702))
</details>
## paperless-ngx 1.16.4
### Bug Fixes
- Fix: prevent button wrapping when sidebar narrows in MS Edge [@shamoon](https://github.com/shamoon) ([#3682](https://github.com/paperless-ngx/paperless-ngx/pull/3682))
- Fix: Handling for filenames with non-ascii and no content attribute [@stumpylog](https://github.com/stumpylog) ([#3695](https://github.com/paperless-ngx/paperless-ngx/pull/3695))
- Fix: Generation of thumbnails for existing stored emails [@stumpylog](https://github.com/stumpylog) ([#3696](https://github.com/paperless-ngx/paperless-ngx/pull/3696))
- Fix: Use row gap for filter editor [@kleinweby](https://github.com/kleinweby) ([#3662](https://github.com/paperless-ngx/paperless-ngx/pull/3662))
### Documentation
- Documentation: update API docs re permissions [@shamoon](https://github.com/shamoon) ([#3697](https://github.com/paperless-ngx/paperless-ngx/pull/3697))
### Maintenance
- Chore: Updates codecov configuration for the flag settings and notification delay [@stumpylog](https://github.com/stumpylog) ([#3656](https://github.com/paperless-ngx/paperless-ngx/pull/3656))
### All App Changes
<details>
<summary>4 changes</summary>
- Fix: prevent button wrapping when sidebar narrows in MS Edge [@shamoon](https://github.com/shamoon) ([#3682](https://github.com/paperless-ngx/paperless-ngx/pull/3682))
- Fix: Handling for filenames with non-ascii and no content attribute [@stumpylog](https://github.com/stumpylog) ([#3695](https://github.com/paperless-ngx/paperless-ngx/pull/3695))
- Fix: Generation of thumbnails for existing stored emails [@stumpylog](https://github.com/stumpylog) ([#3696](https://github.com/paperless-ngx/paperless-ngx/pull/3696))
- Fix: Use row gap for filter editor [@kleinweby](https://github.com/kleinweby) ([#3662](https://github.com/paperless-ngx/paperless-ngx/pull/3662))
</details>
## paperless-ngx 1.16.3
### Bug Fixes
- Fix: Set user and home environment through supervisord [@stumpylog](https://github.com/stumpylog) ([#3638](https://github.com/paperless-ngx/paperless-ngx/pull/3638))
- Fix: Ignore errors when trying to copy the original file's stats [@stumpylog](https://github.com/stumpylog) ([#3652](https://github.com/paperless-ngx/paperless-ngx/pull/3652))
- Fix: Copy default thumbnail if thumbnail generation fails [@plu](https://github.com/plu) ([#3632](https://github.com/paperless-ngx/paperless-ngx/pull/3632))
- Fix: Set user and home environment through supervisord [@stumpylog](https://github.com/stumpylog) ([#3638](https://github.com/paperless-ngx/paperless-ngx/pull/3638))
- Fix: Fix quick install with external database not being fully ready [@stumpylog](https://github.com/stumpylog) ([#3637](https://github.com/paperless-ngx/paperless-ngx/pull/3637))
### Maintenance
- Chore: Update default Postgres version for new installs [@stumpylog](https://github.com/stumpylog) ([#3640](https://github.com/paperless-ngx/paperless-ngx/pull/3640))
### All App Changes
<details>
<summary>2 changes</summary>
- Fix: Ignore errors when trying to copy the original file's stats [@stumpylog](https://github.com/stumpylog) ([#3652](https://github.com/paperless-ngx/paperless-ngx/pull/3652))
- Fix: Copy default thumbnail if thumbnail generation fails [@plu](https://github.com/plu) ([#3632](https://github.com/paperless-ngx/paperless-ngx/pull/3632))
</details>
## paperless-ngx 1.16.2
### Bug Fixes
- Fix: Increase httpx operation timeouts to 30s [@stumpylog](https://github.com/stumpylog) ([#3627](https://github.com/paperless-ngx/paperless-ngx/pull/3627))
- Fix: Better error handling and checking when parsing documents via Tika [@stumpylog](https://github.com/stumpylog) ([#3617](https://github.com/paperless-ngx/paperless-ngx/pull/3617))
### Development
- Development: frontend unit testing [@shamoon](https://github.com/shamoon) ([#3597](https://github.com/paperless-ngx/paperless-ngx/pull/3597))
### Maintenance
- Chore: Bumps the CI/Docker pipenv version [@stumpylog](https://github.com/stumpylog) ([#3622](https://github.com/paperless-ngx/paperless-ngx/pull/3622))
- Chore: Set CI artifact retention days [@stumpylog](https://github.com/stumpylog) ([#3621](https://github.com/paperless-ngx/paperless-ngx/pull/3621))
### All App Changes
<details>
<summary>3 changes</summary>
- Fix: Increase httpx operation timeouts to 30s [@stumpylog](https://github.com/stumpylog) ([#3627](https://github.com/paperless-ngx/paperless-ngx/pull/3627))
- Fix: Better error handling and checking when parsing documents via Tika [@stumpylog](https://github.com/stumpylog) ([#3617](https://github.com/paperless-ngx/paperless-ngx/pull/3617))
- Development: frontend unit testing [@shamoon](https://github.com/shamoon) ([#3597](https://github.com/paperless-ngx/paperless-ngx/pull/3597))
</details>
## paperless-ngx 1.16.1
### Bug Fixes
- Fix: PIL ImportError on ARM devices with Docker [@stumpylog](https://github.com/stumpylog) ([#3605](https://github.com/paperless-ngx/paperless-ngx/pull/3605))
### Maintenance
- Chore: Enable the image cleanup action [@stumpylog](https://github.com/stumpylog) ([#3606](https://github.com/paperless-ngx/paperless-ngx/pull/3606))
## paperless-ngx 1.16.0
### Notable Changes
- Chore: Update base image to Debian bookworm [@stumpylog](https://github.com/stumpylog) ([#3469](https://github.com/paperless-ngx/paperless-ngx/pull/3469))
### Features
- Feature: Update to a simpler Tika library [@stumpylog](https://github.com/stumpylog) ([#3517](https://github.com/paperless-ngx/paperless-ngx/pull/3517))
- Feature: Allow to filter documents by original filename and checksum [@jayme-github](https://github.com/jayme-github) ([#3485](https://github.com/paperless-ngx/paperless-ngx/pull/3485))
### Bug Fixes
- Fix: return user first / last name from backend [@shamoon](https://github.com/shamoon) ([#3579](https://github.com/paperless-ngx/paperless-ngx/pull/3579))
- Fix use of `PAPERLESS_DB_TIMEOUT` for all db types [@shamoon](https://github.com/shamoon) ([#3576](https://github.com/paperless-ngx/paperless-ngx/pull/3576))
- Fix: handle mail rules with no filters on some imap servers [@shamoon](https://github.com/shamoon) ([#3554](https://github.com/paperless-ngx/paperless-ngx/pull/3554))
### Dependencies
- Chore: Python dependency updates (celery 5.3.0 in particular) [@stumpylog](https://github.com/stumpylog) ([#3584](https://github.com/paperless-ngx/paperless-ngx/pull/3584))
### All App Changes
<details>
<summary>8 changes</summary>
- Chore: Python dependency updates (celery 5.3.0 in particular) [@stumpylog](https://github.com/stumpylog) ([#3584](https://github.com/paperless-ngx/paperless-ngx/pull/3584))
- Fix: return user first / last name from backend [@shamoon](https://github.com/shamoon) ([#3579](https://github.com/paperless-ngx/paperless-ngx/pull/3579))
- Fix use of `PAPERLESS_DB_TIMEOUT` for all db types [@shamoon](https://github.com/shamoon) ([#3576](https://github.com/paperless-ngx/paperless-ngx/pull/3576))
- Fix: handle mail rules with no filters on some imap servers [@shamoon](https://github.com/shamoon) ([#3554](https://github.com/paperless-ngx/paperless-ngx/pull/3554))
- Chore: Copy file stats from original file [@stumpylog](https://github.com/stumpylog) ([#3551](https://github.com/paperless-ngx/paperless-ngx/pull/3551))
- Chore: Adds test for barcode ASN when it already exists [@stumpylog](https://github.com/stumpylog) ([#3550](https://github.com/paperless-ngx/paperless-ngx/pull/3550))
- Feature: Update to a simpler Tika library [@stumpylog](https://github.com/stumpylog) ([#3517](https://github.com/paperless-ngx/paperless-ngx/pull/3517))
- Feature: Allow to filter documents by original filename and checksum [@jayme-github](https://github.com/jayme-github) ([#3485](https://github.com/paperless-ngx/paperless-ngx/pull/3485))
</details>
## paperless-ngx 1.15.1
### Bug Fixes

View File

@@ -35,6 +35,12 @@ matcher.
Defaults to `redis://localhost:6379`.
`PAPERLESS_REDIS_PREFIX=<prefix>`
: Prefix to be used in Redis for keys and channels. Useful for sharing one Redis server among multiple Paperless instances.
Defaults to no prefix.
### Database
`PAPERLESS_DBENGINE=<engine_name>`
@@ -495,6 +501,19 @@ HTTP header/value expected by Django, eg `'["HTTP_X_FORWARDED_PROTO", "https"]'`
Settings this value has security implications. Read the Django documentation
and be sure you understand its usage before setting it.
`PAPERLESS_EMAIL_CERTIFICATE_FILE=<path>`
: Configures an additional SSL certificate file containing a [certificate](https://docs.python.org/3/library/ssl.html#certificates)
or certificate chain which should be trusted for validating SSL connections against mail providers.
This is for use with self-signed certificates against local IMAP servers.
Defaults to None.
!!! warning
Settings this value has security implications for the security of your email.
Understand what it does and be sure you need to before setting.
## OCR settings {#ocr}
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
@@ -524,7 +543,7 @@ parsing documents.
`PAPERLESS_OCR_MODE=<mode>`
: Tell paperless when and how to perform ocr on your documents. Four
: Tell paperless when and how to perform ocr on your documents. Three
modes are available:
- `skip`: Paperless skips all pages and will perform ocr only on
@@ -1095,6 +1114,64 @@ barcode.
Defaults to "ASN"
`PAPERLESS_CONSUMER_BARCODE_UPSCALE=<float>`
: Defines the upscale factor used in barcode detection.
Improves the detection of small barcodes, i.e. with a value of 1.5 by
upscaling the document beforce the detection process. Upscaling will
only take place if value is bigger than 1.0. Otherwise upscaling will
not be performed to save resources. Try using in combination with
PAPERLESS_CONSUMER_BARCODE_DPI set to a value higher than default.
Defaults to 0.0
`PAPERLESS_CONSUMER_BARCODE_DPI=<int>`
: During barcode detection every page from a PDF document needs
to be converted to an image. A dpi value can be specified in the
conversion process. Default is 300. If the detection of small barcodes
fails a bigger dpi value i.e. 600 can fix the issue. Try using in
combination with PAPERLESS_CONSUMER_BARCODE_UPSCALE bigger than 1.0.
Defaults to "300"
## Collate Double-Sided Documents {#collate}
`PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=<bool>`
: Enables automatic collation of two single-sided scans into a double-sided
document.
This is useful if you have an automatic document feeder that only supports
single-sided scans, but you need to scan a double-sided document. If your
ADF supports double-sided scans natively, you do not need this feature.
`PAPERLESS_CONSUMER_RECURSIVE` must be enabled for this to work.
For more information, read the [corresponding section in the advanced
documentation](/advanced_usage#collate).
Defaults to false.
`PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME=<str>`
: The name of the subdirectory that the collate feature expects documents to
arrive.
This only has an effect if `PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED`
has been enabled. Note that Paperless will not automatically create the
directory.
Defaults to "double-sided".
`PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT=<bool>`
: Whether TIFF image files should be supported when collating documents.
This will automatically convert any TIFF image(s) to pdfs for later
processing. This only has an effect if
`PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED` has been enabled.
Defaults to false.
## Binaries
There are a few external software packages that Paperless expects to
@@ -1102,7 +1179,7 @@ find on your system when it starts up. Unless you've done something
creative with their installation, you probably won't need to edit any
of these. However, if you've installed these programs somewhere where
simply typing the name of the program doesn't automatically execute it
(ie. the program isn't in your \$PATH), then you'll need to specify
(ie. the program isn't in your $PATH), then you'll need to specify
the literal path for that program.
`PAPERLESS_CONVERT_BINARY=<path>`
@@ -1186,7 +1263,7 @@ actual group ID on the host system, which you can get by executing
with English, German, Italian, Spanish and French. If your language
is not in this list, install additional languages with this
configuration option. You will need to [find the right LangCodes](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html)
but note that (tesseract-ocr-\* package names)[https://packages.debian.org/bullseye/graphics/]
but note that [tesseract-ocr-\* package names](https://packages.debian.org/bullseye/graphics/)
do not always correspond with the language codes e.g. "chi_tra" should be
specified as "chi-tra".

View File

@@ -58,7 +58,7 @@ first-time setup.
!!! note
Every command is executed directly from the root folder of the project unless specified otherwise.
Every command is executed directly from the root folder of the project unless specified otherwise.
1. Install prerequisites + pipenv as mentioned in
[Bare metal route](/setup#bare_metal).
@@ -177,69 +177,69 @@ The front end is built using AngularJS. In order to get started, you need Node.j
The following commands are all performed in the `src-ui`-directory. You will need a running back end (including an active session) to connect to the back end API. To spin it up refer to the commands under the section [above](#back-end-development).
1. Install the Angular CLI. You might need sudo privileges
to perform this command:
1. Install the Angular CLI. You might need sudo privileges to perform this command:
```bash
$ npm install -g @angular/cli
```
```bash
$ npm install -g @angular/cli
```
2. Make sure that it's on your path.
2. Make sure that it's on your path.
3. Install all necessary modules:
3. Install all necessary modules:
```bash
$ npm install
```
```bash
$ npm install
```
4. You can launch a development server by running:
4. You can launch a development server by running:
```bash
$ ng serve
```
```bash
$ ng serve
```
This will automatically update whenever you save. However, in-place
compilation might fail on syntax errors, in which case you need to
restart it.
This will automatically update whenever you save. However, in-place
compilation might fail on syntax errors, in which case you need to
restart it.
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
By default, the development server is available on `http://localhost:4200/` and is configured to access the API at
`http://localhost:8000/api/`, which is the default of the backend. If you enabled `DEBUG` on the back end, several security overrides for allowed hosts, CORS and X-Frame-Options are in place so that the front end behaves exactly as in production.
### Testing and code style
- The front end code (.ts, .html, .scss) use `prettier` for code
formatting via the Git `pre-commit` hooks which run automatically on
commit. See [above](#code-formatting-with-pre-commit-hooks) for installation instructions. You can also run this via the CLI with a
command such as
The front end code (.ts, .html, .scss) use `prettier` for code
formatting via the Git `pre-commit` hooks which run automatically on
commit. See [above](#code-formatting-with-pre-commit-hooks) for installation instructions. You can also run this via the CLI with a
command such as
```bash
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
```
```bash
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
```
- Front end testing uses jest and cypress. There is currently a need
for significantly more front end tests. Unit tests and e2e tests,
respectively, can be run non-interactively with:
Front end testing uses Jest and Playwright. Unit tests and e2e tests,
respectively, can be run non-interactively with:
```bash
$ ng test
$ npm run e2e:ci
```
```bash
$ ng test
$ npx playwright test
```
- Cypress also includes a UI which can be run with:
Playwright also includes a UI which can be run with:
```bash
$ ./node_modules/.bin/cypress open
```
```bash
$ npx playwright test --ui
```
- In order to build the front end and serve it as part of Django, execute:
### Building the frontend
```bash
$ ng build --configuration production
```
In order to build the front end and serve it as part of Django, execute:
This will build the front end and put it in a location from which the
Django server will serve it as static content. This way, you can verify
that authentication is working.
```bash
$ ng build --configuration production
```
This will build the front end and put it in a location from which the
Django server will serve it as static content. This way, you can verify
that authentication is working.
## Localization

View File

@@ -3,10 +3,11 @@
## _What's the general plan for Paperless-ngx?_
**A:** While Paperless-ngx is already considered largely
"feature-complete" it is a community-driven project and development
will be guided in this way. New features can be submitted via GitHub
discussions and "up-voted" by the community but this is not a
guarantee the feature will be implemented. This project will always be
"feature-complete", it is a community-driven project and development
will be guided in this way. New features can be submitted via
[GitHub discussions](https://github.com/paperless-ngx/paperless-ngx/discussions)
and "up-voted" by the community, but this is not a
guarantee that the feature will be implemented. This project will always be
open to collaboration in the form of PRs, ideas etc.
## _I'm using docker. Where are my documents?_
@@ -58,7 +59,7 @@ elsewhere. Here are a couple notes about that.
WebP images are processed with OCR and converted into PDF documents.
- Plain text documents are supported as well and are added verbatim to
paperless.
- With the optional Tika integration enabled (see [Tika configuration](/configuration#tika),
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
Paperless also supports various Office documents (.docx, .doc, odt,
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
@@ -82,7 +83,7 @@ has to do much less work to serve the data.
## _How do I install paperless-ngx on Raspberry Pi?_
**A:** Docker images are available for armv7 and arm64 hardware, so just
follow the docker-compose instructions. Apart from more required disk
follow the [docker-compose instructions](https://docs.paperless-ngx.com/setup/#installation). Apart from more required disk
space compared to a bare metal installation, docker comes with close to
zero overhead, even on Raspberry Pi.

View File

@@ -259,6 +259,7 @@ supported.
- `python3-pip`
- `python3-dev`
- `default-libmysqlclient-dev` for MariaDB
- `pkg-config` for mysqlclient (python dependency)
- `fonts-liberation` for generating thumbnails for plain text
files
- `imagemagick` >= 6 for PDF conversion
@@ -273,7 +274,7 @@ supported.
Use this list for your preferred package management:
```
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev libmagic-dev mime-support libzbar0 poppler-utils
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev pkg-config libmagic-dev mime-support libzbar0 poppler-utils
```
These dependencies are required for OCRmyPDF, which is used for text

View File

@@ -221,9 +221,18 @@ As of version 1.14.0 Paperless-ngx added core support for user / group permissio
based around 'global' permissions as well as 'object-level' permissions. Global permissions designate
which parts of the application a user can access (e.g. Documents, Tags, Settings) and object-level
determine which objects are visible or editable. All objects have an 'owner' and 'view' and 'edit'
permissions which can be granted to other users or groups.
permissions which can be granted to other users or groups. The paperless-ngx permissions system uses
the built-in user model of the backend framework, Django.
Permissions uses the built-in user model of the backend framework, Django.
!!! tip
Object-level permissions only apply to the object itself. In other words, setting permissions
for a Tag will _not_ affect the permissions of documents that have the Tag.
Permissions can be set using the new "Permissions" tab when editing documents, or bulk-applied
in the UI by selecting documents and choosing the "Permissions" button. Owner can also optionally
be set for documents uploaded via the API. Documents consumed via the consumption dir currently
do not have an owner set.
!!! note
@@ -238,11 +247,6 @@ Permissions uses the built-in user model of the backend framework, Django.
Note that superusers have access to all objects.
Permissions can be set using the new "Permissions" tab when editing documents, or bulk-applied
in the UI by selecting documents and choosing the "Permissions" button. Owner can also optionally
be set for documents uploaded via the API. Documents consumed via the consumption dir currently
do not have an owner set.
### Users and Groups
Paperless-ngx versions after 1.14.0 allow creating and editing users and groups via the 'frontend' UI.

View File

@@ -72,7 +72,7 @@ fi
if ! docker stats --no-stream &> /dev/null ; then
echo ""
echo "WARN: It look like the current user does not have Docker permissions."
echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user."
echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user (may require restarting shell)."
echo ""
sleep 3
fi
@@ -384,6 +384,14 @@ fi
${DOCKER_COMPOSE_CMD} pull
if [ "$DATABASE_BACKEND" == "postgres" ] || [ "$DATABASE_BACKEND" == "mariadb" ] ; then
echo "Starting DB first for initilzation"
${DOCKER_COMPOSE_CMD} up --detach db
# hopefully enough time for even the slower systems
sleep 15
${DOCKER_COMPOSE_CMD} stop
fi
${DOCKER_COMPOSE_CMD} run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
${DOCKER_COMPOSE_CMD} up --detach

View File

@@ -66,6 +66,11 @@
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
#PAPERLESS_CONSUMER_ENABLE_BARCODES=false
#PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT
#PAPERLESS_CONSUMER_BARCODE_UPSCALE=0.0
#PAPERLESS_CONSUMER_BARCODE_DPI=300
#PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=false
#PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME=double-sided
#PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT=false
#PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
#PAPERLESS_FILENAME_DATE_ORDER=YMD

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:15
docker run -d -p 6379:6379 redis:latest
docker run -p 3000:3000 -d gotenberg/gotenberg:7.8 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest

View File

@@ -34,10 +34,12 @@
"pt-PT": "src/locale/messages.pt_PT.xlf",
"ro-RO": "src/locale/messages.ro_RO.xlf",
"ru-RU": "src/locale/messages.ru_RU.xlf",
"sk-SK": "src/locale/messages.sk_SK.xlf",
"sl-SI": "src/locale/messages.sl_SI.xlf",
"sr-CS": "src/locale/messages.sr_CS.xlf",
"sv-SE": "src/locale/messages.sv_SE.xlf",
"tr-TR": "src/locale/messages.tr_TR.xlf",
"uk-UA": "src/locale/messages.uk_UA.xlf",
"zh-CN": "src/locale/messages.zh_CN.xlf"
}
},
@@ -159,7 +161,6 @@
}
}
},
"defaultProject": "paperless-ui",
"cli": {
"schematicCollections": [
"@angular-eslint/schematics"

View File

@@ -12,9 +12,13 @@ test('should activate / deactivate save button when changes are saved', async ({
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
/\w+/
)
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Save', exact: true })
).toBeDisabled()
await page.getByTitle('Storage path').getByTitle('Clear all').click()
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
await expect(
page.getByRole('button', { name: 'Save', exact: true })
).toBeEnabled()
})
test('should warn on unsaved changes', async ({ page }) => {
@@ -23,13 +27,17 @@ test('should warn on unsaved changes', async ({ page }) => {
await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText(
/\w+/
)
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
await expect(
page.getByRole('button', { name: 'Save', exact: true })
).toBeDisabled()
await page
.getByTitle('Storage path', { exact: true })
.getByTitle('Clear all')
.click()
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
await page.getByRole('button', { name: 'Close' }).click()
await expect(
page.getByRole('button', { name: 'Save', exact: true })
).toBeEnabled()
await page.getByRole('button', { name: 'Close', exact: true }).click()
await expect(page.getByRole('dialog')).toHaveText(/unsaved changes/)
await page.getByRole('button', { name: 'Cancel' }).click()
await page.getByRole('link', { name: 'Close all' }).click()
@@ -86,51 +94,6 @@ test('should show a list of notes', async ({ page }) => {
).toHaveCount(4)
})
test('should support note deletion', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/documents/175/notes')
await expect(page.locator('app-document-notes')).toBeVisible()
const deletePromise = page.waitForRequest(
(request) =>
request.method() === 'DELETE' &&
request.url().includes('/api/documents/175/notes/')
)
await page
.getByRole('button', { name: /delete note/i, includeHidden: true })
.first()
.click()
await deletePromise
})
test('should support note insertion', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/documents/175/notes')
await expect(page.locator('app-document-notes')).toBeVisible()
await expect(
await page.getByRole('button', {
name: /delete note/i,
includeHidden: true,
})
).toHaveCount(4)
await page.getByPlaceholder('Enter note').fill('This is a new note')
const addPromise = page.waitForRequest((request) => {
if (!request.url().includes('/notes/')) {
// ignore other requests
return true
} else {
const data = request.postDataJSON()
const isValid = data['note'] === 'This is a new note'
return (
isValid &&
request.method() === 'POST' &&
request.url().includes('/notes/')
)
}
})
await page.getByRole('button', { name: 'Add note' }).click()
await addPromise
})
test('should support quick filters', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
await page.goto('/documents/175/details')

View File

@@ -1,58 +0,0 @@
import { test, expect } from '@playwright/test'
const REQUESTS_HAR1 = 'e2e/manage/requests/api-manage1.har'
const REQUESTS_HAR2 = 'e2e/manage/requests/api-manage2.har'
test('should show a list of tags with bottom pagination as well', async ({
page,
}) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
await page.goto('/tags')
await expect(page.getByRole('main')).toHaveText(/26 total tags/i)
await expect(await page.locator('ngb-pagination')).toHaveCount(2)
})
test('should show a list of correspondents without bottom pagination', async ({
page,
}) => {
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
await page.goto('/correspondents')
await expect(page.getByRole('main')).toHaveText(/4 total correspondents/i)
await expect(await page.locator('ngb-pagination')).toHaveCount(1)
})
test('should support quick filter Documents button', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
await page.goto('/tags')
await page
.getByRole('row', { name: 'Inbox' })
.getByRole('button', { name: 'Documents' })
.click()
await expect(page).toHaveURL(/tags__id__all=9/)
})
test('should support item editing', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
await page.goto('/tags')
await page
.getByRole('row', { name: 'Inbox' })
.getByRole('button', { name: 'Edit' })
.click()
await expect(page.getByRole('dialog')).toBeVisible()
await expect(page.getByLabel('Name')).toHaveValue('Inbox')
await page.getByTitle('Color').getByRole('button').click()
const color = await page.getByLabel('Color').inputValue()
const updatePromise = page.waitForRequest((request) => {
const data = request.postDataJSON()
const isValid = data['color'] === color
return (
isValid &&
request.method() === 'PUT' &&
request.url().includes('/api/tags/9/')
)
})
await page.getByRole('button', { name: 'Save' }).click()
await updatePromise
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,6 @@
import { test, expect } from '@playwright/test'
const REQUESTS_HAR = 'e2e/settings/requests/api-settings.har'
const REQUESTS_HAR2 = 'e2e/settings/requests/api-settings2.har'
const REQUESTS_HAR3 = 'e2e/settings/requests/api-settings3.har'
test('should post settings on save', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
@@ -101,65 +99,3 @@ test('should support tab direct navigation', async ({ page }) => {
page.getByRole('tab', { name: 'Users & Groups' })
).toHaveAttribute('aria-selected', 'true')
})
test('should show a list of mail accounts & support creation', async ({
page,
}) => {
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
await page.goto('/settings/mail')
await expect(
page.getByRole('listitem').filter({ hasText: 'imap.gmail.com' })
).toHaveCount(1)
await expect(
page.getByRole('listitem').filter({ hasText: 'imap.domain.com' })
).toHaveCount(1)
await page.getByRole('button', { name: /Add Account/ }).click()
await expect(page.getByRole('dialog')).toHaveCount(1)
await page.getByLabel('Name', { exact: true }).fill('Test Account')
await page.getByLabel('IMAP Server', { exact: true }).fill('imap.server.com')
await page.getByLabel('IMAP Port', { exact: true }).fill('993')
await page.getByLabel('Username', { exact: true }).fill('username')
await page.getByLabel('Password', { exact: true }).fill('password')
const createPromise = page.waitForRequest((request) => {
const data = request.postDataJSON()
const isValid = data['imap_server'] === 'imap.server.com'
return (
isValid &&
request.method() === 'POST' &&
request.url().includes('/api/mail_accounts/')
)
})
await page.getByRole('button', { name: 'Save' }).click()
await createPromise
})
test('should show a list of mail rules & support creation', async ({
page,
}) => {
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
await page.goto('/settings/mail')
await expect(
page.getByRole('listitem').filter({ hasText: 'domain' })
).toHaveCount(2)
await expect(
page.getByRole('listitem').filter({ hasText: 'gmail' })
).toHaveCount(2)
await page.getByRole('button', { name: /Add Rule/ }).click()
await expect(page.getByRole('dialog')).toHaveCount(1)
await page.getByLabel('Name', { exact: true }).fill('Test Rule')
await page.getByTitle('Account').locator('span').first().click()
await page.getByRole('option', { name: 'gmail' }).click()
await page.getByLabel('Maximum age (days)').fill('0')
const createPromise = page.waitForRequest((request) => {
const data = request.postDataJSON()
const isValid = data['name'] === 'Test Rule'
return (
isValid &&
request.method() === 'POST' &&
request.url().includes('/api/mail_rules/')
)
})
await page.getByRole('button', { name: 'Save' }).scrollIntoViewIfNeeded()
await page.getByRole('button', { name: 'Save' }).click()
await createPromise
})

File diff suppressed because one or more lines are too long

View File

@@ -1,71 +0,0 @@
import { test, expect } from '@playwright/test'
const REQUESTS_HAR = 'e2e/tasks/requests/api-tasks.har'
test('should show a list of dismissable tasks in tabs', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/tasks')
await expect(page.getByRole('tab', { name: /Failed/ })).toHaveText(/1/)
await expect(
page.getByRole('cell').filter({ hasText: 'Dismiss' })
).toHaveCount(1)
await expect(page.getByRole('tab', { name: /Complete/ })).toHaveText(/8/)
await page.getByRole('tab', { name: /Complete/ }).click()
await expect(
page.getByRole('cell').filter({ hasText: 'Dismiss' })
).toHaveCount(8)
await page.getByRole('tab', { name: /Started/ }).click()
await expect(
page.getByRole('cell').filter({ hasText: 'Dismiss' })
).toHaveCount(0)
await page.getByRole('tab', { name: /Queued/ }).click()
await expect(
page.getByRole('cell').filter({ hasText: 'Dismiss' })
).toHaveCount(0)
})
test('should support dismissing tasks', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/tasks')
await page.getByRole('tab', { name: /Failed/ }).click()
const dismissPromise = page.waitForRequest((request) => {
const data = request.postDataJSON()
const isValid = Array.isArray(data['tasks']) && data['tasks'].includes(255)
return (
isValid &&
request.method() === 'POST' &&
request.url().includes('/api/acknowledge_tasks/')
)
})
await page
.getByRole('button', { name: 'Dismiss', exact: true })
.first()
.click()
await dismissPromise
})
test('should support dismiss all tasks', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/tasks')
await expect(page.getByRole('button', { name: 'Dismiss all' })).toBeEnabled()
await page.getByRole('button', { name: 'Dismiss all' }).click()
const dismissPromise = page.waitForRequest((request) => {
const data = request.postDataJSON()
const isValid = Array.isArray(data['tasks'])
return (
isValid &&
request.method() === 'POST' &&
request.url().includes('/api/acknowledge_tasks/')
)
})
await page.getByRole('button', { name: /Dismiss/ }).click()
await dismissPromise
})
test('should warn on dismiss all tasks', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/tasks')
await expect(page.getByRole('button', { name: 'Dismiss all' })).toBeEnabled()
await page.getByRole('button', { name: 'Dismiss all' }).click()
await expect(page.getByRole('dialog')).toHaveCount(1)
})

View File

@@ -1,8 +1,14 @@
module.exports = {
moduleNameMapper: {
'@core/(.*)': '<rootDir>/src/app/core/$1',
},
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
testPathIgnorePatterns: ['/node_modules/', '/cypress/'],
testPathIgnorePatterns: [
'/node_modules/',
'/e2e/',
'abstract-name-filter-service',
'abstract-paperless-service',
],
transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
moduleNameMapper: {
'^src/(.*)': '<rootDir>/src/$1',
},
}

File diff suppressed because it is too large Load Diff

8944
src-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,58 +5,59 @@
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"test": "ng test --no-watch --coverage",
"lint": "ng lint"
},
"private": true,
"dependencies": {
"@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.2.0",
"@ng-select/ng-select": "^10.0.4",
"@angular/common": "~16.1.7",
"@angular/compiler": "~16.1.7",
"@angular/core": "~16.1.7",
"@angular/forms": "~16.1.7",
"@angular/localize": "~16.1.7",
"@angular/platform-browser": "~16.1.7",
"@angular/platform-browser-dynamic": "~16.1.7",
"@angular/router": "~16.1.7",
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
"@ng-select/ng-select": "^11.1.1",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.0",
"bootstrap": "^5.3.1",
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
"ng2-pdf-viewer": "^9.1.5",
"ngx-color": "^8.0.3",
"ngx-cookie-service": "^15.0.0",
"ngx-file-drop": "^15.0.0",
"ngx-ui-tour-ng-bootstrap": "^12.6.0",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.3",
"rxjs": "^7.8.1",
"tslib": "^2.5.2",
"tslib": "^2.6.1",
"uuid": "^9.0.0",
"zone.js": "^0.13.0"
},
"devDependencies": {
"@angular-builders/jest": "15.0.0",
"@angular-devkit/build-angular": "~15.2.6",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@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.7",
"@angular/compiler-cli": "~15.2.8",
"@playwright/test": "^1.34.3",
"@types/jest": "^29.5.0",
"@types/node": "^20.2.5",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"@angular-builders/jest": "16.0.0",
"@angular-devkit/build-angular": "~16.1.6",
"@angular-eslint/builder": "16.1.0",
"@angular-eslint/eslint-plugin": "16.1.0",
"@angular-eslint/eslint-plugin-template": "16.1.0",
"@angular-eslint/schematics": "16.1.0",
"@angular-eslint/template-parser": "16.1.0",
"@angular/cli": "~16.1.6",
"@angular/compiler-cli": "~16.1.3",
"@playwright/test": "^1.36.2",
"@types/jest": "^29.5.3",
"@types/node": "^20.4.5",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"concurrently": "^8.1.0",
"eslint": "^8.41.0",
"jest": "28.1.3",
"jest-environment-jsdom": "^29.5.0",
"jest-preset-angular": "^12.2.6",
"eslint": "^8.46.0",
"jest": "29.6.2",
"jest-environment-jsdom": "^29.6.2",
"jest-preset-angular": "^13.1.1",
"jest-websocket-mock": "^2.4.0",
"ts-node": "~10.9.1",
"typescript": "~4.9.5",
"typescript": "^5.1.6",
"wait-on": "^7.0.1"
}
}

View File

@@ -1,4 +1,63 @@
import { jest } from '@jest/globals'
if (process.env.NODE_ENV === 'test') {
require('jest-preset-angular/setup-jest')
}
import '@angular/localize/init'
import { TextEncoder, TextDecoder } from 'util'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
import { registerLocaleData } from '@angular/common'
import localeAr from '@angular/common/locales/ar'
import localeBe from '@angular/common/locales/be'
import localeCa from '@angular/common/locales/ca'
import localeCs from '@angular/common/locales/cs'
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'
import localeNl from '@angular/common/locales/nl'
import localePl from '@angular/common/locales/pl'
import localePt from '@angular/common/locales/pt'
import localeRo from '@angular/common/locales/ro'
import localeRu from '@angular/common/locales/ru'
import localeSk from '@angular/common/locales/sk'
import localeSl from '@angular/common/locales/sl'
import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr'
import localeUk from '@angular/common/locales/uk'
import localeZh from '@angular/common/locales/zh'
registerLocaleData(localeAr)
registerLocaleData(localeBe)
registerLocaleData(localeCa)
registerLocaleData(localeCs)
registerLocaleData(localeDa)
registerLocaleData(localeDe)
registerLocaleData(localeEnGb)
registerLocaleData(localeEs)
registerLocaleData(localeFi)
registerLocaleData(localeFr)
registerLocaleData(localeIt)
registerLocaleData(localeLb)
registerLocaleData(localeNl)
registerLocaleData(localePl)
registerLocaleData(localePt, 'pt-BR')
registerLocaleData(localePt, 'pt-PT')
registerLocaleData(localeRo)
registerLocaleData(localeRu)
registerLocaleData(localeSk)
registerLocaleData(localeSl)
registerLocaleData(localeSr)
registerLocaleData(localeSv)
registerLocaleData(localeTr)
registerLocaleData(localeUk)
registerLocaleData(localeZh)
/* global mocks for jsdom */
const mock = () => {
@@ -17,14 +76,7 @@ Object.defineProperty(window, 'getComputedStyle', {
value: () => ['-webkit-appearance'],
})
Object.defineProperty(document.body.style, 'transform', {
value: () => {
return {
enumerable: true,
configurable: true,
}
},
})
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext

View File

@@ -22,7 +22,7 @@ import {
PermissionType,
} from './services/permissions.service'
const routes: Routes = [
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{
path: '',

View File

@@ -0,0 +1,182 @@
import { HttpClientTestingModule } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
tick,
} from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TourService, TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { Subject } from 'rxjs'
import { routes } from './app-routing.module'
import { AppComponent } from './app.component'
import { ToastsComponent } from './components/common/toasts/toasts.component'
import {
ConsumerStatusService,
FileStatus,
} from './services/consumer-status.service'
import { PermissionsService } from './services/permissions.service'
import { ToastService, Toast } from './services/toast.service'
import { UploadDocumentsService } from './services/upload-documents.service'
import { SettingsService } from './services/settings.service'
describe('AppComponent', () => {
let component: AppComponent
let fixture: ComponentFixture<AppComponent>
let tourService: TourService
let consumerStatusService: ConsumerStatusService
let permissionsService: PermissionsService
let toastService: ToastService
let router: Router
let settingsService: SettingsService
let uploadDocumentsService: UploadDocumentsService
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [AppComponent, ToastsComponent],
providers: [],
imports: [
HttpClientTestingModule,
TourNgBootstrapModule,
RouterTestingModule.withRoutes(routes),
NgxFileDropModule,
],
}).compileComponents()
tourService = TestBed.inject(TourService)
consumerStatusService = TestBed.inject(ConsumerStatusService)
permissionsService = TestBed.inject(PermissionsService)
settingsService = TestBed.inject(SettingsService)
toastService = TestBed.inject(ToastService)
router = TestBed.inject(Router)
uploadDocumentsService = TestBed.inject(UploadDocumentsService)
fixture = TestBed.createComponent(AppComponent)
component = fixture.componentInstance
})
it('should initialize the tour service & toggle class on body for styling', fakeAsync(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
fixture.detectChanges()
const tourSpy = jest.spyOn(tourService, 'initialize')
component.ngOnInit()
expect(tourSpy).toHaveBeenCalled()
tourService.start()
expect(document.body.classList).toContain('tour-active')
tourService.end()
tick(500)
expect(document.body.classList).not.toContain('tour-active')
}))
it('should display toast on document consumed with link if user has access', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
let toast: Toast
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
const toastSpy = jest.spyOn(toastService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).toHaveBeenCalled()
expect(toast.action).not.toBeUndefined()
})
it('should display toast on document consumed without link if user does not have access', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
let toast: Toast
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
const toastSpy = jest.spyOn(toastService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).toHaveBeenCalled()
expect(toast.action).toBeUndefined()
})
it('should display toast on document added', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const toastSpy = jest.spyOn(toastService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(consumerStatusService, 'onDocumentDetected')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).toHaveBeenCalled()
})
it('should suppress dashboard notifications if set', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest.spyOn(settingsService, 'get').mockReturnValue(true)
jest.spyOn(router, 'url', 'get').mockReturnValue('/dashboard')
const toastSpy = jest.spyOn(toastService, 'show')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(consumerStatusService, 'onDocumentDetected')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).not.toHaveBeenCalled()
})
it('should display toast on document failed', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const toastSpy = jest.spyOn(toastService, 'showError')
const fileStatusSubject = new Subject<FileStatus>()
jest
.spyOn(consumerStatusService, 'onDocumentConsumptionFailed')
.mockReturnValue(fileStatusSubject)
component.ngOnInit()
fileStatusSubject.next(new FileStatus())
expect(toastSpy).toHaveBeenCalled()
})
it('should disable drag-drop if on dashboard', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest.spyOn(router, 'url', 'get').mockReturnValueOnce('/dashboard')
expect(component.dragDropEnabled).toBeFalsy()
jest.spyOn(router, 'url', 'get').mockReturnValueOnce('/documents')
expect(component.dragDropEnabled).toBeTruthy()
})
it('should enable drag-drop if user has permissions', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.dragDropEnabled).toBeTruthy()
})
it('should disable drag-drop if user does not have permissions', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
expect(component.dragDropEnabled).toBeFalsy()
})
it('should support drag drop', fakeAsync(() => {
expect(component.fileIsOver).toBeFalsy()
component.fileOver()
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
expect(dropzone).not.toBeNull()
component.fileLeave()
tick(700)
fixture.detectChanges()
expect(dropzone.classes['hide']).toBeTruthy()
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
component.dropped([])
tick(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
}))
})

View File

@@ -139,104 +139,88 @@ export class AppComponent implements OnInit, OnDestroy {
const nextBtnTitle = $localize`Next`
const endBtnTitle = $localize`End`
this.tourService.initialize([
this.tourService.initialize(
[
{
anchorId: 'tour.dashboard',
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some.`,
route: '/dashboard',
delayAfterNavigation: 500,
isOptional: false,
},
{
anchorId: 'tour.upload-widget',
content: $localize`Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.`,
route: '/dashboard',
},
{
anchorId: 'tour.documents',
content: $localize`The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.`,
route: '/documents?sort=created&reverse=1&page=1',
delayAfterNavigation: 500,
placement: 'bottom',
},
{
anchorId: 'tour.documents-filter-editor',
content: $localize`The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.`,
route: '/documents?sort=created&reverse=1&page=1',
placement: 'bottom',
},
{
anchorId: 'tour.documents-views',
content: $localize`Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.`,
route: '/documents?sort=created&reverse=1&page=1',
},
{
anchorId: 'tour.tags',
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
route: '/tags',
backdropConfig: {
offset: 0,
},
},
{
anchorId: 'tour.file-tasks',
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
route: '/tasks',
backdropConfig: {
offset: 0,
},
},
{
anchorId: 'tour.settings',
content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`,
route: '/settings',
backdropConfig: {
offset: 0,
},
},
{
anchorId: 'tour.outro',
title: $localize`Thank you! 🙏`,
content:
$localize`There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.` +
'<br/><br/>' +
$localize`Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!`,
route: '/dashboard',
isOptional: false,
backdropConfig: {
offset: 0,
},
},
],
{
anchorId: 'tour.dashboard',
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some.`,
route: '/dashboard',
enableBackdrop: true,
delayAfterNavigation: 500,
backdropConfig: {
offset: 10,
},
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
{
anchorId: 'tour.upload-widget',
content: $localize`Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms.`,
route: '/dashboard',
enableBackdrop: true,
isOptional: true,
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
{
anchorId: 'tour.documents',
content: $localize`The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.`,
route: '/documents?sort=created&reverse=1&page=1',
delayAfterNavigation: 500,
placement: 'bottom',
enableBackdrop: true,
disableScrollToAnchor: true,
isOptional: true,
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
{
anchorId: 'tour.documents-filter-editor',
content: $localize`The filtering tools allow you to quickly find documents using various searches, dates, tags, etc.`,
route: '/documents?sort=created&reverse=1&page=1',
placement: 'bottom',
enableBackdrop: true,
isOptional: true,
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
{
anchorId: 'tour.documents-views',
content: $localize`Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar.`,
route: '/documents?sort=created&reverse=1&page=1',
enableBackdrop: true,
isOptional: true,
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
{
anchorId: 'tour.tags',
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
route: '/tags',
enableBackdrop: true,
isOptional: true,
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
{
anchorId: 'tour.file-tasks',
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
route: '/tasks',
enableBackdrop: true,
isOptional: true,
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
{
anchorId: 'tour.settings',
content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`,
route: '/settings',
enableBackdrop: true,
isOptional: true,
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
{
anchorId: 'tour.outro',
title: $localize`Thank you! 🙏`,
content:
$localize`There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues.` +
'<br/><br/>' +
$localize`Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx!`,
route: '/dashboard',
prevBtnTitle,
nextBtnTitle,
endBtnTitle,
},
])
useLegacyTitle: true,
}
)
this.tourService.start$.subscribe(() => {
this.renderer.addClass(document.body, 'tour-active')

View File

@@ -92,6 +92,8 @@ import { PermissionsDialogComponent } from './components/common/permissions-dial
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 { LogoComponent } from './components/common/logo/logo.component'
import { IsNumberPipe } from './pipes/is-number.pipe'
import localeAr from '@angular/common/locales/ar'
import localeBe from '@angular/common/locales/be'
@@ -110,10 +112,12 @@ import localePl from '@angular/common/locales/pl'
import localePt from '@angular/common/locales/pt'
import localeRo from '@angular/common/locales/ro'
import localeRu from '@angular/common/locales/ru'
import localeSk from '@angular/common/locales/sk'
import localeSl from '@angular/common/locales/sl'
import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr'
import localeUk from '@angular/common/locales/uk'
import localeZh from '@angular/common/locales/zh'
registerLocaleData(localeAr)
@@ -134,10 +138,12 @@ registerLocaleData(localePt, 'pt-BR')
registerLocaleData(localePt, 'pt-PT')
registerLocaleData(localeRo)
registerLocaleData(localeRu)
registerLocaleData(localeSk)
registerLocaleData(localeSl)
registerLocaleData(localeSr)
registerLocaleData(localeSv)
registerLocaleData(localeTr)
registerLocaleData(localeUk)
registerLocaleData(localeZh)
function initializeApp(settings: SettingsService) {
@@ -217,6 +223,8 @@ function initializeApp(settings: SettingsService) {
PermissionsFormComponent,
PermissionsFilterDropdownComponent,
UsernamePipe,
LogoComponent,
IsNumberPipe,
],
imports: [
BrowserModule,

View File

@@ -137,6 +137,7 @@ main {
.sidebar .nav-link {
font-weight: 500;
white-space: nowrap;
&:hover, &.active, &:focus {
color: var(--bs-primary);

View File

@@ -0,0 +1,272 @@
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { AppFrameComponent } from './app-frame.component'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { BrowserModule } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing'
import { SettingsService } from 'src/app/services/settings.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { of } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { ActivatedRoute, Router } from '@angular/router'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
import { SearchService } from 'src/app/services/rest/search.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
import { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
const document = { id: 2, title: 'Hello world' }
describe('AppFrameComponent', () => {
let component: AppFrameComponent
let fixture: ComponentFixture<AppFrameComponent>
let httpTestingController: HttpTestingController
let settingsService: SettingsService
let permissionsService: PermissionsService
let remoteVersionService: RemoteVersionService
let toastService: ToastService
let openDocumentsService: OpenDocumentsService
let searchService: SearchService
let documentListViewService: DocumentListViewService
let router: Router
let savedViewSpy
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [AppFrameComponent, IfPermissionsDirective],
imports: [
HttpClientTestingModule,
BrowserModule,
RouterTestingModule.withRoutes(routes),
NgbModule,
FormsModule,
ReactiveFormsModule,
],
providers: [
SettingsService,
SavedViewService,
PermissionsService,
RemoteVersionService,
IfPermissionsDirective,
ToastService,
OpenDocumentsService,
SearchService,
{
provide: ActivatedRoute,
useValue: {
firstChild: {
component: DocumentDetailComponent,
},
snapshot: {
firstChild: {
component: DocumentDetailComponent,
params: {
id: document.id,
},
},
},
},
},
PermissionsGuard,
],
}).compileComponents()
settingsService = TestBed.inject(SettingsService)
const savedViewService = TestBed.inject(SavedViewService)
permissionsService = TestBed.inject(PermissionsService)
remoteVersionService = TestBed.inject(RemoteVersionService)
toastService = TestBed.inject(ToastService)
openDocumentsService = TestBed.inject(OpenDocumentsService)
searchService = TestBed.inject(SearchService)
documentListViewService = TestBed.inject(DocumentListViewService)
router = TestBed.inject(Router)
jest
.spyOn(settingsService, 'displayName', 'get')
.mockReturnValue('Hello World')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
savedViewSpy = jest.spyOn(savedViewService, 'initialize')
fixture = TestBed.createComponent(AppFrameComponent)
component = fixture.componentInstance
httpTestingController = TestBed.inject(HttpTestingController)
fixture.detectChanges()
})
it('should initialize the saved view service', () => {
expect(savedViewSpy).toHaveBeenCalled()
})
it('should check for update if enabled', () => {
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
updateCheckSpy.mockImplementation(() => {
return of({
version: 'v100.0',
update_available: true,
})
})
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, true)
component.ngOnInit()
expect(updateCheckSpy).toHaveBeenCalled()
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Update available')
})
it('should check not for update if disabled', () => {
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
component.ngOnInit()
fixture.detectChanges()
expect(updateCheckSpy).not.toHaveBeenCalled()
expect(fixture.nativeElement.textContent).not.toContain('Update available')
})
it('should check for update if was disabled and then enabled', () => {
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
component.setUpdateChecking(true)
fixture.detectChanges()
expect(updateCheckSpy).toHaveBeenCalled()
})
it('should show error on toggle update checking if store settings fails', () => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
const toastSpy = jest.spyOn(toastService, 'showError')
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
component.setUpdateChecking(true)
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush('error', {
status: 500,
statusText: 'error',
})
expect(toastSpy).toHaveBeenCalled()
})
it('should support toggling slim sidebar and saving', fakeAsync(() => {
const saveSettingSpy = jest.spyOn(settingsService, 'set')
expect(component.slimSidebarEnabled).toBeFalsy()
expect(component.slimSidebarAnimating).toBeFalsy()
component.toggleSlimSidebar()
expect(component.slimSidebarAnimating).toBeTruthy()
tick(200)
expect(component.slimSidebarAnimating).toBeFalsy()
expect(component.slimSidebarEnabled).toBeTruthy()
expect(saveSettingSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.SLIM_SIDEBAR,
true
)
}))
it('should show error on toggle slim sidebar if store settings fails', () => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
const toastSpy = jest.spyOn(toastService, 'showError')
component.toggleSlimSidebar()
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush('error', {
status: 500,
statusText: 'error',
})
expect(toastSpy).toHaveBeenCalled()
})
it('should support collapsable menu', () => {
const button: HTMLButtonElement = (
fixture.nativeElement as HTMLDivElement
).querySelector('button[data-toggle=collapse]')
button.dispatchEvent(new MouseEvent('click'))
expect(component.isMenuCollapsed).toBeFalsy()
component.closeMenu()
expect(component.isMenuCollapsed).toBeTruthy()
})
it('should support close document & navigate on close current doc', () => {
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
closeSpy.mockReturnValue(of(true))
const routerSpy = jest.spyOn(router, 'navigate')
component.closeDocument(document)
expect(closeSpy).toHaveBeenCalledWith(document)
expect(routerSpy).toHaveBeenCalled()
})
it('should support close all documents & navigate on close current doc', () => {
const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
closeAllSpy.mockReturnValue(of(true))
const routerSpy = jest.spyOn(router, 'navigate')
component.closeAll()
expect(closeAllSpy).toHaveBeenCalled()
expect(routerSpy).toHaveBeenCalled()
})
it('should close all documents on logout', () => {
const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
component.onLogout()
expect(closeAllSpy).toHaveBeenCalled()
})
it('should warn before close if dirty documents', () => {
jest.spyOn(openDocumentsService, 'hasDirty').mockReturnValue(true)
expect(component.canDeactivate()).toBeFalsy()
})
it('should call autocomplete endpoint on input', fakeAsync(() => {
const autocompleteSpy = jest.spyOn(searchService, 'autocomplete')
component.searchAutoComplete(of('hello')).subscribe()
tick(250)
expect(autocompleteSpy).toHaveBeenCalled()
component.searchAutoComplete(of('hello world 1')).subscribe()
tick(250)
expect(autocompleteSpy).toHaveBeenCalled()
}))
it('should support reset search field', () => {
const resetSpy = jest.spyOn(component, 'resetSearchField')
const input = (fixture.nativeElement as HTMLDivElement).querySelector(
'input'
) as HTMLInputElement
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }))
expect(resetSpy).toHaveBeenCalled()
})
it('should support choosing a search item', () => {
expect(component.searchField.value).toEqual('')
component.itemSelected({ item: 'hello', preventDefault: () => true })
expect(component.searchField.value).toEqual('hello ')
component.itemSelected({ item: 'world', preventDefault: () => true })
expect(component.searchField.value).toEqual('hello world ')
})
it('should navigate via quickFilter on search', () => {
const str = 'hello world '
component.searchField.patchValue(str)
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
component.search()
expect(qfSpy).toHaveBeenCalledWith([
{
rule_type: FILTER_FULLTEXT_QUERY,
value: str.trim(),
},
])
})
})

View File

@@ -53,7 +53,7 @@ export class AppFrameComponent
public settingsService: SettingsService,
public tasksService: TasksService,
private readonly toastService: ToastService,
private permissionsService: PermissionsService
permissionsService: PermissionsService
) {
super()
@@ -75,7 +75,7 @@ export class AppFrameComponent
}
versionString = `${environment.appTitle} ${environment.version}`
appRemoteVersion
appRemoteVersion: AppRemoteVersion
isMenuCollapsed: boolean = true
@@ -103,7 +103,7 @@ export class AppFrameComponent
this.toastService.showError(
$localize`An error occurred while saving settings.`
)
console.log(error)
console.warn(error)
},
})
}
@@ -236,7 +236,7 @@ export class AppFrameComponent
this.toastService.showError(
$localize`An error occurred while saving update checking settings.`
)
console.log(error)
console.warn(error)
},
})
if (enable) {

View File

@@ -0,0 +1,43 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ClearableBadgeComponent } from './clearable-badge.component'
describe('ClearableBadgeComponent', () => {
let component: ClearableBadgeComponent
let fixture: ComponentFixture<ClearableBadgeComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [ClearableBadgeComponent],
}).compileComponents()
fixture = TestBed.createComponent(ClearableBadgeComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support selected', () => {
component.selected = true
expect(component.active).toBeTruthy()
})
it('should support numbered', () => {
component.number = 3
fixture.detectChanges()
expect(component.active).toBeTruthy()
expect((fixture.nativeElement as HTMLDivElement).textContent).toContain('3')
})
it('should support selected', () => {
let clearedResult
component.selected = true
fixture.detectChanges()
component.cleared.subscribe((clear) => {
clearedResult = clear
})
fixture.nativeElement
.querySelectorAll('button')[0]
.dispatchEvent(new MouseEvent('click'))
expect(clearedResult).toBeTruthy()
})
})

View File

@@ -0,0 +1,99 @@
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ConfirmDialogComponent } from './confirm-dialog.component'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { Subject } from 'rxjs'
describe('ConfirmDialogComponent', () => {
let component: ConfirmDialogComponent
let modal: NgbActiveModal
let fixture: ComponentFixture<ConfirmDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [ConfirmDialogComponent, SafeHtmlPipe],
providers: [NgbActiveModal, SafeHtmlPipe],
imports: [],
}).compileComponents()
modal = TestBed.inject(NgbActiveModal)
fixture = TestBed.createComponent(ConfirmDialogComponent)
component = fixture.componentInstance
component.title = 'Confirm delete'
component.messageBold = 'Do you really want to delete document file.pdf?'
component.message =
'The files for this document will be deleted permanently. This operation cannot be undone.'
component.btnClass = 'btn-danger'
component.btnCaption = 'Delete document'
fixture.detectChanges()
})
it('should support alternative', () => {
let alternativeClickedResult
let alternativeSubjectResult
component.alternativeClicked.subscribe((result) => {
alternativeClickedResult = true
})
component.alternative()
// with subject
const subject = new Subject<boolean>()
component.alternativeSubject = subject
subject.asObservable().subscribe((result) => {
alternativeSubjectResult = result
})
component.alternative()
expect(alternativeClickedResult).toBeTruthy()
expect(alternativeSubjectResult).toBeTruthy()
})
it('should support confirm', () => {
let confirmClickedResult
let confirmSubjectResult
component.confirmClicked.subscribe((result) => {
confirmClickedResult = true
})
component.confirm()
// with subject
const subject = new Subject<boolean>()
component.confirmSubject = subject
subject.asObservable().subscribe((result) => {
confirmSubjectResult = result
})
component.confirm()
expect(confirmClickedResult).toBeTruthy()
expect(confirmSubjectResult).toBeTruthy()
})
it('should support cancel & close modal', () => {
let confirmSubjectResult
const closeModalSpy = jest.spyOn(modal, 'close')
component.cancel()
const subject = new Subject<boolean>()
component.confirmSubject = subject
subject.asObservable().subscribe((result) => {
confirmSubjectResult = result
})
component.cancel()
// with subject
expect(closeModalSpy).toHaveBeenCalled()
expect(confirmSubjectResult).toBeFalsy()
})
it('should support delay confirm', fakeAsync(() => {
component.confirmButtonEnabled = false
component.delayConfirm(1)
expect(component.confirmButtonEnabled).toBeFalsy()
tick(1500)
fixture.detectChanges()
expect(component.confirmButtonEnabled).toBeTruthy()
discardPeriodicTasks()
}))
})

View File

@@ -0,0 +1,141 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
let fixture: ComponentFixture<DateDropdownComponent>
import {
DateDropdownComponent,
DateSelection,
RelativeDate,
} from './date-dropdown.component'
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { SettingsService } from 'src/app/services/settings.service'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DatePipe } from '@angular/common'
describe('DateDropdownComponent', () => {
let component: DateDropdownComponent
let httpTestingController: HttpTestingController
let settingsService: SettingsService
let settingsSpy
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
DateDropdownComponent,
ClearableBadgeComponent,
CustomDatePipe,
],
providers: [SettingsService, CustomDatePipe, DatePipe],
imports: [
HttpClientTestingModule,
NgbModule,
FormsModule,
ReactiveFormsModule,
],
}).compileComponents()
httpTestingController = TestBed.inject(HttpTestingController)
settingsService = TestBed.inject(SettingsService)
settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')
fixture = TestBed.createComponent(DateDropdownComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should use a localized date placeholder', () => {
expect(component.datePlaceHolder).toEqual('mm/dd/yyyy')
expect(settingsSpy).toHaveBeenCalled()
})
it('should support date input, emit change', fakeAsync(() => {
let result: string
component.dateAfterChange.subscribe((date) => (result = date))
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
input.value = '5/30/2023'
input.dispatchEvent(new Event('change'))
tick(500)
expect(result).not.toBeNull()
}))
it('should support date select, emit datesSet change', fakeAsync(() => {
let result: DateSelection
component.datesSet.subscribe((date) => (result = date))
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
input.value = '5/30/2023'
input.dispatchEvent(new Event('dateSelect'))
tick(500)
expect(result).not.toBeNull()
}))
it('should support relative dates', fakeAsync(() => {
let result: DateSelection
component.datesSet.subscribe((date) => (result = date))
component.setRelativeDate(null)
component.setRelativeDate(RelativeDate.LAST_7_DAYS)
tick(500)
expect(result).toEqual({
after: null,
before: null,
relativeDateID: RelativeDate.LAST_7_DAYS,
})
}))
it('should support report if active', () => {
component.relativeDate = RelativeDate.LAST_7_DAYS
expect(component.isActive).toBeTruthy()
component.relativeDate = null
component.dateAfter = '2023-05-30'
expect(component.isActive).toBeTruthy()
component.dateAfter = null
component.dateBefore = '2023-05-30'
expect(component.isActive).toBeTruthy()
component.dateBefore = null
expect(component.isActive).toBeFalsy()
})
it('should support reset', () => {
component.dateAfter = '2023-05-30'
component.reset()
expect(component.dateAfter).toBeNull()
})
it('should support clearAfter', () => {
component.dateAfter = '2023-05-30'
component.clearAfter()
expect(component.dateAfter).toBeNull()
})
it('should support clearBefore', () => {
component.dateBefore = '2023-05-30'
component.clearBefore()
expect(component.dateBefore).toBeNull()
})
it('should limit keyboard events', () => {
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
let event: KeyboardEvent = new KeyboardEvent('keypress', {
key: '9',
})
let eventSpy = jest.spyOn(event, 'preventDefault')
input.dispatchEvent(event)
expect(eventSpy).not.toHaveBeenCalled()
event = new KeyboardEvent('keypress', {
key: '{',
})
eventSpy = jest.spyOn(event, 'preventDefault')
input.dispatchEvent(event)
expect(eventSpy).toHaveBeenCalled()
})
})

View File

@@ -1,4 +1,3 @@
import { formatDate } from '@angular/common'
import {
Component,
EventEmitter,

View File

@@ -0,0 +1,55 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { EditDialogMode } from '../edit-dialog.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SelectComponent } from '../../input/select/select.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TextComponent } from '../../input/text/text.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
describe('CorrespondentEditDialogComponent', () => {
let component: CorrespondentEditDialogComponent
let fixture: ComponentFixture<CorrespondentEditDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
CorrespondentEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
PermissionsFormComponent,
],
providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(CorrespondentEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,55 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { EditDialogMode } from '../edit-dialog.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SelectComponent } from '../../input/select/select.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TextComponent } from '../../input/text/text.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
describe('DocumentTypeEditDialogComponent', () => {
let component: DocumentTypeEditDialogComponent
let fixture: ComponentFixture<DocumentTypeEditDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
DocumentTypeEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
PermissionsFormComponent,
],
providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(DocumentTypeEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,234 @@
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { Component } from '@angular/core'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import {
FormGroup,
FormControl,
FormsModule,
ReactiveFormsModule,
} from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
import {
DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL,
MATCH_AUTO,
MATCH_NONE,
} from 'src/app/data/matching-model'
import { of } from 'rxjs'
import { environment } from 'src/environments/environment'
@Component({
template: `
<div>
<h4 class="modal-title" id="modal-basic-title">{{ getTitle() }}</h4>
</div>
`,
})
class TestComponent extends EditDialogComponent<PaperlessTag> {
constructor(
service: TagService,
activeModal: NgbActiveModal,
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
}
getForm(): FormGroup<any> {
return new FormGroup({
name: new FormControl(''),
color: new FormControl(''),
is_inbox_tag: new FormControl(false),
permissions_form: new FormControl(null),
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
})
}
}
const currentUser = {
id: 99,
username: 'user99',
}
const permissions = {
view: {
users: [11],
groups: [],
},
change: {
users: [],
groups: [2],
},
}
const tag = {
id: 1,
name: 'Tag 1',
color: '#fff000',
is_inbox_tag: false,
matching_algorithm: MATCH_AUTO,
owner: 10,
permissions,
}
describe('EditDialogComponent', () => {
let component: TestComponent
let fixture: ComponentFixture<TestComponent>
let tagService: TagService
let activeModal: NgbActiveModal
let httpTestingController: HttpTestingController
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [TestComponent],
providers: [
NgbActiveModal,
{
provide: UserService,
useValue: {
listAll: () =>
of({
results: [
{
id: 13,
username: 'user1',
},
],
}),
},
},
{
provide: SettingsService,
useValue: {
currentUser,
},
},
TagService,
],
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
}).compileComponents()
tagService = TestBed.inject(TagService)
activeModal = TestBed.inject(NgbActiveModal)
httpTestingController = TestBed.inject(HttpTestingController)
fixture = TestBed.createComponent(TestComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should interpolate object permissions', () => {
component.object = tag
component.dialogMode = EditDialogMode.EDIT
component.ngOnInit()
expect(component.objectForm.get('permissions_form').value).toEqual({
owner: tag.owner,
set_permissions: permissions,
})
})
it('should delay close enabled', fakeAsync(() => {
expect(component.closeEnabled).toBeFalsy()
component.ngOnInit()
tick(100)
expect(component.closeEnabled).toBeTruthy()
}))
it('should set default owner when in create mode', () => {
component.dialogMode = EditDialogMode.CREATE
component.ngOnInit()
expect(component.objectForm.get('permissions_form').value.owner).toEqual(
currentUser.id
)
// cover optional chaining
component.objectForm.removeControl('permissions_form')
component.ngOnInit()
})
it('should detect if pattern required', () => {
expect(component.patternRequired).toBeFalsy()
component.objectForm.get('matching_algorithm').setValue(MATCH_AUTO)
expect(component.patternRequired).toBeFalsy()
component.objectForm.get('matching_algorithm').setValue(MATCH_NONE)
expect(component.patternRequired).toBeFalsy()
component.objectForm.get('matching_algorithm').setValue(MATCH_ALL)
expect(component.patternRequired).toBeTruthy()
// coverage
component.objectForm = null
expect(component.patternRequired).toBeTruthy()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
// coverage
component.dialogMode = null
fixture.detectChanges()
})
it('should close on cancel', () => {
const closeSpy = jest.spyOn(activeModal, 'close')
component.cancel()
expect(closeSpy).toHaveBeenCalled()
})
it('should update an object on save in edit mode', () => {
const updateSpy = jest.spyOn(tagService, 'update')
component.dialogMode = EditDialogMode.EDIT
component.save()
expect(updateSpy).toHaveBeenCalled()
})
it('should create an object on save in edit mode', () => {
const createSpy = jest.spyOn(tagService, 'create')
component.dialogMode = EditDialogMode.CREATE
component.save()
expect(createSpy).toHaveBeenCalled()
})
it('should close on successful save', () => {
const closeSpy = jest.spyOn(activeModal, 'close')
const successSpy = jest.spyOn(component.succeeded, 'emit')
component.save()
httpTestingController.expectOne(`${environment.apiBaseUrl}tags/`).flush({})
expect(closeSpy).toHaveBeenCalled()
expect(successSpy).toHaveBeenCalled()
})
it('should not close on failed save', () => {
const closeSpy = jest.spyOn(activeModal, 'close')
const failedSpy = jest.spyOn(component.failed, 'next')
component.save()
httpTestingController
.expectOne(`${environment.apiBaseUrl}tags/`)
.flush('error', {
status: 500,
statusText: 'error',
})
expect(closeSpy).not.toHaveBeenCalled()
expect(failedSpy).toHaveBeenCalled()
expect(component.error).toEqual('error')
})
})

View File

@@ -15,9 +15,14 @@ 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'
export enum EditDialogMode {
CREATE = 0,
EDIT = 1,
}
@Directive()
export abstract class EditDialogComponent<
T extends ObjectWithPermissions | ObjectWithId
T extends ObjectWithPermissions | ObjectWithId,
> implements OnInit
{
constructor(
@@ -30,7 +35,7 @@ export abstract class EditDialogComponent<
users: PaperlessUser[]
@Input()
dialogMode: string = 'create'
dialogMode: EditDialogMode = EditDialogMode.CREATE
@Input()
object: T
@@ -71,7 +76,7 @@ export abstract class EditDialogComponent<
this.userService.listAll().subscribe((r) => {
this.users = r.results
if (this.dialogMode === 'create') {
if (this.dialogMode === EditDialogMode.CREATE) {
this.objectForm.get('permissions_form')?.setValue({
owner: this.settingsService.currentUser.id,
})
@@ -87,15 +92,11 @@ export abstract class EditDialogComponent<
return $localize`Edit item`
}
getSaveErrorMessage(error: string) {
return $localize`Could not save element: ${error}`
}
getTitle() {
switch (this.dialogMode) {
case 'create':
case EditDialogMode.CREATE:
return this.getCreateTitle()
case 'edit':
case EditDialogMode.EDIT:
return this.getEditTitle()
default:
break
@@ -127,10 +128,10 @@ export abstract class EditDialogComponent<
var newObject = Object.assign(Object.assign({}, this.object), formValues)
var serverResponse: Observable<T>
switch (this.dialogMode) {
case 'create':
case EditDialogMode.CREATE:
serverResponse = this.service.create(newObject)
break
case 'edit':
case EditDialogMode.EDIT:
serverResponse = this.service.update(newObject)
default:
break

View File

@@ -0,0 +1,57 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { EditDialogMode } from '../edit-dialog.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SelectComponent } from '../../input/select/select.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TextComponent } from '../../input/text/text.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { GroupEditDialogComponent } from './group-edit-dialog.component'
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
describe('GroupEditDialogComponent', () => {
let component: GroupEditDialogComponent
let fixture: ComponentFixture<GroupEditDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
GroupEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
PermissionsFormComponent,
PermissionsSelectComponent,
],
providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(GroupEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,117 @@
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
tick,
} from '@angular/core/testing'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { EditDialogMode } from '../edit-dialog.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SelectComponent } from '../../input/select/select.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TextComponent } from '../../input/text/text.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { MailAccountEditDialogComponent } from './mail-account-edit-dialog.component'
import { PasswordComponent } from '../../input/password/password.component'
import { CheckComponent } from '../../input/check/check.component'
import { IMAPSecurity } from 'src/app/data/paperless-mail-account'
import { environment } from 'src/environments/environment'
describe('MailAccountEditDialogComponent', () => {
let component: MailAccountEditDialogComponent
let fixture: ComponentFixture<MailAccountEditDialogComponent>
let httpController: HttpTestingController
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
MailAccountEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
CheckComponent,
PermissionsFormComponent,
PasswordComponent,
],
providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
httpController = TestBed.inject(HttpTestingController)
fixture = TestBed.createComponent(MailAccountEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
it('should support test mail account and show appropriate expiring alert', fakeAsync(() => {
component.object = {
name: 'example',
imap_server: 'imap.example.com',
username: 'user',
password: 'pass',
imap_port: 443,
imap_security: IMAPSecurity.SSL,
is_token: false,
}
// success
component.test()
httpController
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
.flush({ success: true })
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain(
'Successfully connected'
)
tick(6000)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).not.toContain(
'Successfully connected'
)
// not success
component.test()
httpController
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
.flush({ success: false })
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Unable to connect')
// error
component.test()
httpController
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
.flush({}, { status: 500, statusText: 'error' })
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Unable to connect')
tick(6000)
}))
})

View File

@@ -0,0 +1,113 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { EditDialogMode } from '../edit-dialog.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SelectComponent } from '../../input/select/select.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TextComponent } from '../../input/text/text.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { MailRuleEditDialogComponent } from './mail-rule-edit-dialog.component'
import { NumberComponent } from '../../input/number/number.component'
import { TagsComponent } from '../../input/tags/tags.component'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { of } from 'rxjs'
import {
MailAction,
MailMetadataCorrespondentOption,
} from 'src/app/data/paperless-mail-rule'
describe('MailRuleEditDialogComponent', () => {
let component: MailRuleEditDialogComponent
let fixture: ComponentFixture<MailRuleEditDialogComponent>
let accountService: MailAccountService
let correspondentService: CorrespondentService
let documentTypeService: DocumentTypeService
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
MailRuleEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
PermissionsFormComponent,
NumberComponent,
TagsComponent,
SafeHtmlPipe,
],
providers: [
NgbActiveModal,
{
provide: MailAccountService,
useValue: {
listAll: () => of([]),
},
},
{
provide: CorrespondentService,
useValue: {
listAll: () => of([]),
},
},
{
provide: DocumentTypeService,
useValue: {
listAll: () => of([]),
},
},
],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(MailRuleEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
it('should support optional fields', () => {
expect(component.showCorrespondentField).toBeFalsy()
component.objectForm
.get('assign_correspondent_from')
.setValue(MailMetadataCorrespondentOption.FromCustom)
expect(component.showCorrespondentField).toBeTruthy()
expect(component.showActionParamField).toBeFalsy()
component.objectForm.get('action').setValue(MailAction.Move)
expect(component.showActionParamField).toBeTruthy()
component.objectForm.get('action').setValue('')
expect(component.showActionParamField).toBeFalsy()
component.objectForm.get('action').setValue(MailAction.Tag)
expect(component.showActionParamField).toBeTruthy()
// coverage of optional chaining
component.objectForm = null
expect(component.showCorrespondentField).toBeFalsy()
expect(component.showActionParamField).toBeFalsy()
})
})

View File

@@ -0,0 +1,57 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { EditDialogMode } from '../edit-dialog.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SelectComponent } from '../../input/select/select.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TextComponent } from '../../input/text/text.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
describe('StoragePathEditDialogComponent', () => {
let component: StoragePathEditDialogComponent
let fixture: ComponentFixture<StoragePathEditDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
StoragePathEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
PermissionsFormComponent,
SafeHtmlPipe,
],
providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(StoragePathEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,59 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { EditDialogMode } from '../edit-dialog.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SelectComponent } from '../../input/select/select.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TextComponent } from '../../input/text/text.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { TagEditDialogComponent } from './tag-edit-dialog.component'
import { ColorComponent } from '../../input/color/color.component'
import { CheckComponent } from '../../input/check/check.component'
describe('TagEditDialogComponent', () => {
let component: TagEditDialogComponent
let fixture: ComponentFixture<TagEditDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
TagEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
PermissionsFormComponent,
ColorComponent,
CheckComponent,
],
providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(TagEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,115 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { EditDialogMode } from '../edit-dialog.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SelectComponent } from '../../input/select/select.component'
import {
AbstractControl,
FormsModule,
ReactiveFormsModule,
} from '@angular/forms'
import { TextComponent } from '../../input/text/text.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { UserEditDialogComponent } from './user-edit-dialog.component'
import { PasswordComponent } from '../../input/password/password.component'
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
import { GroupService } from 'src/app/services/rest/group.service'
import { of } from 'rxjs'
describe('UserEditDialogComponent', () => {
let component: UserEditDialogComponent
let fixture: ComponentFixture<UserEditDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
UserEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
PasswordComponent,
PermissionsFormComponent,
PermissionsSelectComponent,
],
providers: [
NgbActiveModal,
{
provide: GroupService,
useValue: {
listAll: () =>
of({
results: [
{
id: 1,
permissions: ['dummy_perms'],
},
],
}),
},
},
],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(UserEditDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
it('should disable user permissions select on toggle superuser', () => {
const control: AbstractControl =
component.objectForm.get('user_permissions')
expect(control.disabled).toBeFalsy()
component.objectForm.get('is_superuser').setValue(true)
component.onToggleSuperUser()
expect(control.disabled).toBeTruthy()
})
it('should update inherited permissions', () => {
component.objectForm.get('groups').setValue(null)
expect(component.inheritedPermissions).toEqual([])
component.objectForm.get('groups').setValue([1])
expect(component.inheritedPermissions).toEqual(['dummy_perms'])
component.objectForm.get('groups').setValue([2])
expect(component.inheritedPermissions).toEqual([])
})
it('should detect whether password was changed in form on save', () => {
component.objectForm.get('password').setValue(null)
component.save()
expect(component.passwordIsSet).toBeFalsy()
// unchanged pw
component.objectForm.get('password').setValue('*******')
component.save()
expect(component.passwordIsSet).toBeFalsy()
// unchanged pw
component.objectForm.get('password').setValue('helloworld')
component.save()
expect(component.passwordIsSet).toBeTruthy()
})
})

View File

@@ -34,7 +34,7 @@
<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">
*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 - 1)" [disabled]="disabled">
</app-toggleable-dropdown-button>
</ng-container>
</div>

View File

@@ -0,0 +1,487 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import {
ChangedItems,
FilterableDropdownComponent,
FilterableDropdownSelectionModel,
Intersection,
LogicalOperator,
} from './filterable-dropdown.component'
import { FilterPipe } from 'src/app/pipes/filter.pipe'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import {
DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL,
} from 'src/app/data/matching-model'
import {
ToggleableDropdownButtonComponent,
ToggleableItemState,
} from './toggleable-dropdown-button/toggleable-dropdown-button.component'
import { TagComponent } from '../tag/tag.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
const items: PaperlessTag[] = [
{
id: 1,
name: 'Tag1',
is_inbox_tag: false,
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
},
{
id: 2,
name: 'Tag2',
is_inbox_tag: true,
matching_algorithm: MATCH_ALL,
match: 'str',
},
]
const nullItem = {
id: null,
name: 'Not assigned',
}
let selectionModel: FilterableDropdownSelectionModel
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
let component: FilterableDropdownComponent
let fixture: ComponentFixture<FilterableDropdownComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
FilterableDropdownComponent,
FilterPipe,
ToggleableDropdownButtonComponent,
TagComponent,
ClearableBadgeComponent,
],
providers: [FilterPipe],
imports: [NgbModule, FormsModule, ReactiveFormsModule],
}).compileComponents()
fixture = TestBed.createComponent(FilterableDropdownComponent)
component = fixture.componentInstance
selectionModel = new FilterableDropdownSelectionModel()
})
it('should sanitize title', () => {
expect(component.name).toBeNull()
component.title = 'Foo Bar'
expect(component.name).toEqual('foo_bar')
})
it('should support reset', () => {
component.items = items
component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.Selected)
expect(selectionModel.getSelectedItems()).toHaveLength(1)
expect(selectionModel.isDirty()).toBeTruthy()
component.reset()
expect(selectionModel.getSelectedItems()).toHaveLength(0)
expect(selectionModel.isDirty()).toBeFalsy()
})
it('should report document counts', () => {
component.documentCounts = [
{
id: items[0].id,
document_count: 12,
},
]
expect(component.getUpdatedDocumentCount(items[0].id)).toEqual(12)
expect(component.getUpdatedDocumentCount(items[1].id)).toBeUndefined() // coverate of optional chaining
})
it('should emit change when items selected', () => {
component.items = items
component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
expect(newModel).toBeUndefined()
selectionModel.set(items[0].id, ToggleableItemState.Selected)
expect(selectionModel.isDirty()).toBeTruthy()
expect(newModel.getSelectedItems()).toEqual([items[0]])
expect(newModel.getExcludedItems()).toEqual([])
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
expect(newModel.getSelectedItems()).toEqual([])
expect(component.items).toEqual([nullItem, ...items])
})
it('should emit change when items excluded', () => {
component.items = items
component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
expect(newModel).toBeUndefined()
selectionModel.toggle(items[0].id)
expect(newModel.getSelectedItems()).toEqual([items[0]])
})
it('should emit change when items excluded', () => {
component.items = items
component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
selectionModel.set(items[0].id, ToggleableItemState.Excluded)
expect(newModel.getSelectedItems()).toEqual([])
expect(newModel.getExcludedItems()).toEqual([items[0]])
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
expect(newModel.getSelectedItems()).toEqual([])
expect(newModel.getExcludedItems()).toEqual([])
})
it('should exclude items when excluded and not editing', () => {
component.items = items
component.manyToOne = true
component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.Selected)
component.excludeClicked(items[0].id)
expect(selectionModel.getSelectedItems()).toEqual([])
expect(selectionModel.getExcludedItems()).toEqual([items[0]])
})
it('should toggle when items excluded and editing', () => {
component.items = items
component.manyToOne = true
component.editing = true
component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
component.excludeClicked(items[0].id)
expect(selectionModel.getSelectedItems()).toEqual([items[0]])
expect(selectionModel.getExcludedItems()).toEqual([])
})
it('should hide count for item if adding will increase size of set', () => {
component.items = items
component.manyToOne = true
component.selectionModel = selectionModel
expect(component.hideCount(items[0])).toBeFalsy()
selectionModel.logicalOperator = LogicalOperator.Or
expect(component.hideCount(items[0])).toBeTruthy()
})
it('should enforce single select when editing', () => {
component.editing = true
component.items = items
component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
expect(selectionModel.singleSelect).toEqual(true)
selectionModel.toggle(items[0].id)
selectionModel.toggle(items[1].id)
expect(newModel.getSelectedItems()).toEqual([items[1]])
})
it('should support manyToOne selecting', () => {
component.items = items
selectionModel.manyToOne = false
component.selectionModel = selectionModel
component.manyToOne = true
expect(component.manyToOne).toBeTruthy()
let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model))
expect(selectionModel.singleSelect).toEqual(false)
selectionModel.toggle(items[0].id)
selectionModel.toggle(items[1].id)
expect(newModel.getSelectedItems()).toEqual([items[0], items[1]])
})
it('should dynamically enable / disable modifier toggle', () => {
component.items = items
component.selectionModel = selectionModel
expect(component.modifierToggleEnabled).toBeTruthy()
selectionModel.toggle(null)
expect(component.modifierToggleEnabled).toBeFalsy()
component.manyToOne = true
expect(component.modifierToggleEnabled).toBeFalsy()
selectionModel.toggle(items[0].id)
selectionModel.toggle(items[1].id)
expect(component.modifierToggleEnabled).toBeTruthy()
})
it('should apply changes and close when apply button clicked', () => {
component.items = items
component.editing = true
component.selectionModel = selectionModel
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
selectionModel.toggle(items[0].id)
fixture.detectChanges()
expect(component.modelIsDirty).toBeTruthy()
let applyResult: ChangedItems
const closeSpy = jest.spyOn(component.dropdown, 'close')
component.apply.subscribe((result) => (applyResult = result))
const applyButton = Array.from(
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
).find((b) => b.textContent.includes('Apply'))
applyButton.dispatchEvent(new MouseEvent('click'))
expect(closeSpy).toHaveBeenCalled()
expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
})
it('should apply on close if enabled', () => {
component.items = items
component.editing = true
component.applyOnClose = true
component.selectionModel = selectionModel
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
selectionModel.toggle(items[0].id)
fixture.detectChanges()
expect(component.modelIsDirty).toBeTruthy()
let applyResult: ChangedItems
component.apply.subscribe((result) => (applyResult = result))
component.dropdown.close()
expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
})
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
component.items = items
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
expect(document.activeElement).toEqual(
component.listFilterTextInput.nativeElement
)
expect(
Array.from(
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
).filter((b) => b.textContent.includes('Tag'))
).toHaveLength(2)
component.filterText = 'Tag2'
fixture.detectChanges()
expect(
Array.from(
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
).filter((b) => b.textContent.includes('Tag'))
).toHaveLength(1)
component.dropdown.close()
expect(component.filterText).toHaveLength(0)
}))
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
component.items = items
expect(component.selectionModel.getSelectedItems()).toEqual([])
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
component.filterText = 'Tag2'
fixture.detectChanges()
const closeSpy = jest.spyOn(component.dropdown, 'close')
component.listFilterTextInput.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Enter' })
)
expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
tick(300)
expect(closeSpy).toHaveBeenCalled()
}))
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
component.items = items
component.editing = true
let applyResult: ChangedItems
component.apply.subscribe((result) => (applyResult = result))
expect(component.selectionModel.getSelectedItems()).toEqual([])
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
component.filterText = 'Tag2'
fixture.detectChanges()
component.listFilterTextInput.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Enter' })
)
expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
tick(300)
expect(applyResult).toEqual({ itemsToAdd: [items[1]], itemsToRemove: [] })
}))
it('should support arrow keyboard navigation', fakeAsync(() => {
component.items = items
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
const filterInputEl: HTMLInputElement =
component.listFilterTextInput.nativeElement
expect(document.activeElement).toEqual(filterInputEl)
const itemButtons = Array.from(
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
).filter((b) => b.textContent.includes('Tag'))
filterInputEl.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[0])
itemButtons[0].dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[1])
itemButtons[1].dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[0])
itemButtons[0].dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
)
expect(document.activeElement).toEqual(filterInputEl)
filterInputEl.value = 'foo'
component.filterText = 'foo'
// dont move focus if we're traversing the field
filterInputEl.selectionStart = 1
expect(document.activeElement).toEqual(filterInputEl)
// now we're at end, so move focus
filterInputEl.selectionStart = 3
filterInputEl.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[0])
}))
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
component.items = items
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
const filterInputEl: HTMLInputElement =
component.listFilterTextInput.nativeElement
expect(document.activeElement).toEqual(filterInputEl)
const itemButtons = Array.from(
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
).filter((b) => b.textContent.includes('Tag'))
filterInputEl.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
)
itemButtons[0].focus() // normally handled by browser
itemButtons[0].dispatchEvent(
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
)
itemButtons[1].focus() // normally handled by browser
itemButtons[1].dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Tab',
shiftKey: true,
bubbles: true,
})
)
itemButtons[0].focus() // normally handled by browser
itemButtons[0].dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[1])
}))
it('should support arrow keyboard navigation after click', fakeAsync(() => {
component.items = items
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
const filterInputEl: HTMLInputElement =
component.listFilterTextInput.nativeElement
expect(document.activeElement).toEqual(filterInputEl)
const itemButtons = Array.from(
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
).filter((b) => b.textContent.includes('Tag'))
fixture.nativeElement
.querySelector('app-toggleable-dropdown-button')
.dispatchEvent(new MouseEvent('click'))
itemButtons[0].focus() // normally handled by browser
expect(document.activeElement).toEqual(itemButtons[0])
itemButtons[0].dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
)
expect(document.activeElement).toEqual(itemButtons[1])
}))
it('should toggle logical operator', fakeAsync(() => {
component.items = items
component.manyToOne = true
selectionModel.set(items[0].id, ToggleableItemState.Selected)
selectionModel.set(items[1].id, ToggleableItemState.Selected)
component.selectionModel = selectionModel
let changedResult: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe(
(result) => (changedResult = result)
)
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
expect(component.modifierToggleEnabled).toBeTruthy()
const operatorButtons: HTMLInputElement[] = Array.from(
(fixture.nativeElement as HTMLDivElement).querySelectorAll('input')
).filter((b) => ['and', 'or'].includes(b.value))
expect(operatorButtons[0].checked).toBeTruthy()
operatorButtons[1].dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
expect(selectionModel.logicalOperator).toEqual(LogicalOperator.Or)
expect(changedResult.logicalOperator).toEqual(LogicalOperator.Or)
}))
it('should toggle intersection include / exclude', fakeAsync(() => {
component.items = items
selectionModel.set(items[0].id, ToggleableItemState.Selected)
selectionModel.set(items[1].id, ToggleableItemState.Selected)
component.selectionModel = selectionModel
let changedResult: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe(
(result) => (changedResult = result)
)
fixture.nativeElement
.querySelector('button')
.dispatchEvent(new MouseEvent('click')) // open
fixture.detectChanges()
tick(100)
expect(component.modifierToggleEnabled).toBeTruthy()
const intersectionButtons: HTMLInputElement[] = Array.from(
(fixture.nativeElement as HTMLDivElement).querySelectorAll('input')
).filter((b) => ['include', 'exclude'].includes(b.value))
expect(intersectionButtons[0].checked).toBeTruthy()
intersectionButtons[1].dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
expect(selectionModel.intersection).toEqual(Intersection.Exclude)
expect(changedResult.intersection).toEqual(Intersection.Exclude)
expect(changedResult.getSelectedItems()).toEqual([])
expect(changedResult.getExcludedItems()).toEqual(items)
}))
it('FilterableDropdownSelectionModel should sort items by state', () => {
component.items = items
component.selectionModel = selectionModel
selectionModel.toggle(items[1].id)
selectionModel.apply()
expect(selectionModel.itemsSorted).toEqual([nullItem, items[1], items[0]])
})
})

View File

@@ -96,7 +96,7 @@ export class FilterableDropdownSelectionModel {
toggle(id: number, fireEvent = true) {
let state = this.temporarySelectionStates.get(id)
if (
state == null ||
state == undefined ||
(state != ToggleableItemState.Selected &&
state != ToggleableItemState.Excluded)
) {

View File

@@ -0,0 +1,79 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
ToggleableDropdownButtonComponent,
ToggleableItemState,
} from './toggleable-dropdown-button.component'
import { TagComponent } from '../../tag/tag.component'
import { PaperlessTag } from 'src/app/data/paperless-tag'
describe('ToggleableDropdownButtonComponent', () => {
let component: ToggleableDropdownButtonComponent
let fixture: ComponentFixture<ToggleableDropdownButtonComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [ToggleableDropdownButtonComponent, TagComponent],
providers: [],
imports: [],
}).compileComponents()
fixture = TestBed.createComponent(ToggleableDropdownButtonComponent)
component = fixture.componentInstance
})
it('should recognize a tag', () => {
component.item = {
id: 1,
name: 'Test Tag',
is_inbox_tag: false,
} as PaperlessTag
fixture.detectChanges()
expect(component.isTag).toBeTruthy()
})
it('should report toggled state', () => {
expect(component.isChecked()).toBeFalsy()
expect(component.isPartiallyChecked()).toBeFalsy()
expect(component.isExcluded()).toBeFalsy()
component.state = ToggleableItemState.Selected
expect(component.isChecked()).toBeTruthy()
expect(component.isPartiallyChecked()).toBeFalsy()
expect(component.isExcluded()).toBeFalsy()
component.state = ToggleableItemState.PartiallySelected
expect(component.isPartiallyChecked()).toBeTruthy()
expect(component.isChecked()).toBeFalsy()
expect(component.isExcluded()).toBeFalsy()
component.state = ToggleableItemState.Excluded
expect(component.isExcluded()).toBeTruthy()
expect(component.isChecked()).toBeFalsy()
expect(component.isPartiallyChecked()).toBeFalsy()
})
it('should emit exclude event when selected and then toggled', () => {
let excludeResult
let toggleResult
component.state = ToggleableItemState.Selected
component.exclude.subscribe(() => (excludeResult = true))
component.toggle.subscribe(() => (toggleResult = true))
const button = fixture.nativeElement.querySelector('button')
button.dispatchEvent(new MouseEvent('click'))
expect(excludeResult).toBeTruthy()
expect(toggleResult).toBeFalsy()
})
it('should emit toggle event when not selected and then toggled', () => {
let excludeResult
let toggleResult
component.state = ToggleableItemState.Excluded
component.exclude.subscribe(() => (excludeResult = true))
component.toggle.subscribe(() => (toggleResult = true))
const button = fixture.nativeElement.querySelector('button')
button.dispatchEvent(new MouseEvent('click'))
expect(excludeResult).toBeFalsy()
expect(toggleResult).toBeTruthy()
})
})

View File

@@ -0,0 +1,55 @@
import { Component } from '@angular/core'
import { AbstractInputComponent } from './abstract-input'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@Component({
template: `
<div>
<input
#inputField
type="text"
class="form-control"
[class.is-invalid]="error"
[id]="inputId"
[(ngModel)]="value"
(change)="onChange(value)"
[disabled]="disabled"
/>
</div>
`,
})
class TestComponent extends AbstractInputComponent<string> {
constructor() {
super()
}
}
describe(`AbstractInputComponent`, () => {
let component: TestComponent
let fixture: ComponentFixture<TestComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [TestComponent],
providers: [],
imports: [FormsModule, ReactiveFormsModule],
}).compileComponents()
fixture = TestBed.createComponent(TestComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should assign uuid', () => {
component.ngOnInit()
expect(component.inputId).not.toBeUndefined()
})
it('should support focus', () => {
const focusSpy = jest.spyOn(component.inputField.nativeElement, 'focus')
component.focus()
expect(focusSpy).toHaveBeenCalled()
})
})

View File

@@ -1,5 +1,5 @@
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
<label class="form-check-label" [for]="inputId">{{title}}</label>
<div *ngIf="hint" class="form-text text-muted">{{hint}}</div>
</div>

View File

@@ -0,0 +1,39 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { CheckComponent } from './check.component'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
describe('CheckComponent', () => {
let component: CheckComponent
let fixture: ComponentFixture<CheckComponent>
let input: HTMLInputElement
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [CheckComponent],
providers: [],
imports: [FormsModule, ReactiveFormsModule],
}).compileComponents()
fixture = TestBed.createComponent(CheckComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
input = component.inputField.nativeElement
})
it('should support use of checkbox', () => {
input.checked = true
input.dispatchEvent(new Event('change'))
fixture.detectChanges()
expect(component.value).toBeTruthy()
input.checked = false
input.dispatchEvent(new Event('change'))
fixture.detectChanges()
expect(component.value).toBeFalsy()
})
})

View File

@@ -1,6 +1,5 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { v4 as uuidv4 } from 'uuid'
import { Component, forwardRef } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@Component({

View File

@@ -11,7 +11,7 @@
</ng-template>
<input class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
<input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
<button class="btn btn-outline-secondary" type="button" (click)="randomize()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16">

View File

@@ -0,0 +1,72 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { ColorComponent } from './color.component'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { ColorSliderModule } from 'ngx-color/slider'
describe('ColorComponent', () => {
let component: ColorComponent
let fixture: ComponentFixture<ColorComponent>
let input: HTMLInputElement
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [ColorComponent],
providers: [],
imports: [
FormsModule,
ReactiveFormsModule,
NgbPopoverModule,
ColorSliderModule,
],
}).compileComponents()
fixture = TestBed.createComponent(ColorComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
input = component.inputField.nativeElement
})
it('should support use of input', () => {
input.value = '#ff0000'
component.colorChanged(input.value)
fixture.detectChanges()
expect(component.value).toEqual('#ff0000')
})
it('should set swatch color', () => {
const swatch: HTMLSpanElement = fixture.nativeElement.querySelector(
'span.input-group-text'
)
expect(swatch.style.backgroundColor).toEqual('')
component.value = '#ff0000'
fixture.detectChanges()
expect(swatch.style.backgroundColor).toEqual('rgb(255, 0, 0)')
})
it('should show color slider popover', () => {
component.value = '#ff0000'
input.dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
expect(
fixture.nativeElement.querySelector('ngb-popover-window')
).not.toBeUndefined()
expect(
fixture.nativeElement.querySelector('color-slider')
).not.toBeUndefined()
fixture.nativeElement
.querySelector('color-slider')
.dispatchEvent(new Event('change'))
})
it('should allow randomize color and update value', () => {
expect(component.value).toBeUndefined()
component.randomize()
expect(component.value).not.toBeUndefined()
})
})

View File

@@ -1,7 +1,7 @@
<div class="mb-3">
<label class="form-label" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error">
<input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
<input #inputField class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
@@ -9,7 +9,7 @@
<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>
</button>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" i18n-title title="Filter documents with this {{title}}">
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ fitlerButtonTitle }}">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>

View File

@@ -0,0 +1,103 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { DateComponent } from './date.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import {
NgbDateParserFormatter,
NgbDatepickerModule,
} from '@ng-bootstrap/ng-bootstrap'
import { RouterTestingModule } from '@angular/router/testing'
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
describe('DateComponent', () => {
let component: DateComponent
let fixture: ComponentFixture<DateComponent>
let input: HTMLInputElement
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [DateComponent],
providers: [
{
provide: NgbDateParserFormatter,
useClass: LocalizedDateParserFormatter,
},
],
imports: [
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule,
NgbDatepickerModule,
RouterTestingModule,
],
}).compileComponents()
fixture = TestBed.createComponent(DateComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
input = component.inputField.nativeElement
})
it('should support use of input field', () => {
input.value = '5/14/20'
input.dispatchEvent(new Event('change'))
fixture.detectChanges()
expect(component.value).toEqual({ day: 14, month: 5, year: 2020 })
})
it('should use localzed placeholder from settings', () => {
component.ngOnInit()
expect(component.placeholder).toEqual('mm/dd/yyyy')
})
it('should support suggestions', () => {
expect(component.value).toBeUndefined()
component.suggestions = ['2023-05-31', '2014-05-14']
fixture.detectChanges()
const suggestionAnchor: HTMLAnchorElement =
fixture.nativeElement.querySelector('a')
suggestionAnchor.click()
expect(component.value).toEqual({ day: 31, month: 5, year: 2023 })
})
it('should limit keyboard events', () => {
let event: KeyboardEvent = new KeyboardEvent('keypress', {
key: '9',
})
let eventSpy = jest.spyOn(event, 'preventDefault')
input.dispatchEvent(event)
expect(eventSpy).not.toHaveBeenCalled()
event = new KeyboardEvent('keypress', {
key: '{',
})
eventSpy = jest.spyOn(event, 'preventDefault')
input.dispatchEvent(event)
expect(eventSpy).toHaveBeenCalled()
})
it('should support paste', () => {
expect(component.value).toBeUndefined()
const date = '5/4/20'
const clipboardData = {
dropEffect: null,
effectAllowed: null,
files: null,
items: null,
types: null,
clearData: null,
getData: () => date,
setData: null,
setDragImage: null,
}
const event = new Event('paste')
event['clipboardData'] = clipboardData
input.dispatchEvent(event)
expect(component.value).toEqual({ day: 4, month: 5, year: 2020 })
})
})

View File

@@ -98,4 +98,8 @@ export class DateComponent
onFilterDocuments() {
this.filterDocuments.emit([this.ngbDateParserFormatter.parse(this.value)])
}
get filterButtonTitle() {
return $localize`Filter documents with this ${this.title}`
}
}

View File

@@ -1,7 +1,7 @@
<div class="mb-3">
<label class="form-label" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error">
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
<input #inputField type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
</div>
<div class="invalid-feedback">

View File

@@ -0,0 +1,79 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { NumberComponent } from './number.component'
import { DocumentService } from 'src/app/services/rest/document.service'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { of } from 'rxjs'
describe('NumberComponent', () => {
let component: NumberComponent
let fixture: ComponentFixture<NumberComponent>
let input: HTMLInputElement
let documentService: DocumentService
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [NumberComponent],
providers: [DocumentService],
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
}).compileComponents()
fixture = TestBed.createComponent(NumberComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
documentService = TestBed.inject(DocumentService)
fixture.detectChanges()
input = component.inputField.nativeElement
})
// TODO: why doesnt this work?
// it('should support use of input field', () => {
// expect(component.value).toBeUndefined()
// input.stepUp()
// console.log(input.value);
// input.dispatchEvent(new Event('change'))
// fixture.detectChanges()
// expect(component.value).toEqual('3')
// })
it('should support +1 ASN', () => {
const listAllSpy = jest.spyOn(documentService, 'listFiltered')
listAllSpy
.mockReturnValueOnce(
of({
count: 1,
all: [1],
results: [
{
id: 1,
archive_serial_number: 1000,
},
],
})
)
.mockReturnValueOnce(
of({
count: 0,
all: [],
results: [],
})
)
expect(component.value).toBeUndefined()
component.nextAsn()
expect(component.value).toEqual(1001)
// this time results are empty
component.value = undefined
component.nextAsn()
expect(component.value).toEqual(1)
component.value = 1002
component.nextAsn()
expect(component.value).toEqual(1002)
})
})

View File

@@ -0,0 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
ReactiveFormsModule,
NG_VALUE_ACCESSOR,
} from '@angular/forms'
import { PasswordComponent } from './password.component'
describe('PasswordComponent', () => {
let component: PasswordComponent
let fixture: ComponentFixture<PasswordComponent>
let input: HTMLInputElement
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [PasswordComponent],
providers: [],
imports: [FormsModule, ReactiveFormsModule],
}).compileComponents()
fixture = TestBed.createComponent(PasswordComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
input = component.inputField.nativeElement
})
it('should support use of input field', () => {
expect(component.value).toBeUndefined()
// TODO: why doesnt this work?
// input.value = 'foo'
// input.dispatchEvent(new Event('change'))
// fixture.detectChanges()
// expect(component.value).toEqual('foo')
})
})

View File

@@ -0,0 +1,66 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
ReactiveFormsModule,
NG_VALUE_ACCESSOR,
} from '@angular/forms'
import { PermissionsFormComponent } from './permissions-form.component'
import { SelectComponent } from '../../select/select.component'
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
import { PermissionsGroupComponent } from '../permissions-group/permissions-group.component'
import { PermissionsUserComponent } from '../permissions-user/permissions-user.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgSelectModule } from '@ng-select/ng-select'
describe('PermissionsFormComponent', () => {
let component: PermissionsFormComponent
let fixture: ComponentFixture<PermissionsFormComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
PermissionsFormComponent,
SelectComponent,
PermissionsGroupComponent,
PermissionsUserComponent,
],
providers: [],
imports: [
FormsModule,
ReactiveFormsModule,
NgbAccordionModule,
HttpClientTestingModule,
NgSelectModule,
],
}).compileComponents()
fixture = TestBed.createComponent(PermissionsFormComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support use of select for owner', () => {
const changeSpy = jest.spyOn(component, 'onChange')
component.ngOnInit()
component.users = [
{
id: 2,
username: 'foo',
},
{
id: 3,
username: 'bar',
},
]
component.form.get('owner').patchValue(2)
fixture.detectChanges()
expect(changeSpy).toHaveBeenCalledWith({
owner: 2,
set_permissions: {
view: { users: [], groups: [] },
change: { users: [], groups: [] },
},
})
})
})

View File

@@ -0,0 +1,59 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { PermissionsGroupComponent } from './permissions-group.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgSelectModule } from '@ng-select/ng-select'
import { GroupService } from 'src/app/services/rest/group.service'
import { of } from 'rxjs'
describe('PermissionsGroupComponent', () => {
let component: PermissionsGroupComponent
let fixture: ComponentFixture<PermissionsGroupComponent>
let groupService: GroupService
let groupServiceSpy
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [PermissionsGroupComponent],
providers: [GroupService],
imports: [
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule,
NgSelectModule,
],
}).compileComponents()
groupService = TestBed.inject(GroupService)
groupServiceSpy = jest.spyOn(groupService, 'listAll').mockReturnValue(
of({
count: 2,
all: [2, 3],
results: [
{
id: 2,
name: 'Group 2',
},
{
id: 3,
name: 'Group 3',
},
],
})
)
fixture = TestBed.createComponent(PermissionsGroupComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should get groups, support use of select', () => {
component.writeValue({ id: 2, name: 'Group 2' })
expect(component.value).toEqual({ id: 2, name: 'Group 2' })
expect(groupServiceSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,60 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { PermissionsUserComponent } from './permissions-user.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgSelectModule } from '@ng-select/ng-select'
import { GroupService } from 'src/app/services/rest/group.service'
import { of } from 'rxjs'
import { UserService } from 'src/app/services/rest/user.service'
describe('PermissionsUserComponent', () => {
let component: PermissionsUserComponent
let fixture: ComponentFixture<PermissionsUserComponent>
let userService: UserService
let userServiceSpy
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [PermissionsUserComponent],
providers: [UserService],
imports: [
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule,
NgSelectModule,
],
}).compileComponents()
userService = TestBed.inject(UserService)
userServiceSpy = jest.spyOn(userService, 'listAll').mockReturnValue(
of({
count: 2,
all: [2, 3],
results: [
{
id: 2,
name: 'User 2',
},
{
id: 3,
name: 'User 3',
},
],
})
)
fixture = TestBed.createComponent(PermissionsUserComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should get users, support use of select', () => {
component.writeValue({ id: 2, name: 'User 2' })
expect(component.value).toEqual({ id: 2, name: 'User 2' })
expect(userServiceSpy).toHaveBeenCalled()
})
})

View File

@@ -26,7 +26,7 @@
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" i18n-title title="Filter documents with this {{title}}">
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>

View File

@@ -0,0 +1,121 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import {
FormsModule,
ReactiveFormsModule,
NG_VALUE_ACCESSOR,
} from '@angular/forms'
import { SelectComponent } from './select.component'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import {
DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL,
} from 'src/app/data/matching-model'
import { NgSelectModule } from '@ng-select/ng-select'
import { RouterTestingModule } from '@angular/router/testing'
const items: PaperlessTag[] = [
{
id: 1,
name: 'Tag1',
is_inbox_tag: false,
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
},
{
id: 2,
name: 'Tag2',
is_inbox_tag: true,
matching_algorithm: MATCH_ALL,
match: 'str',
},
{
id: 10,
name: 'Tag10',
is_inbox_tag: false,
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
},
]
describe('SelectComponent', () => {
let component: SelectComponent
let fixture: ComponentFixture<SelectComponent>
let input: HTMLInputElement
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [SelectComponent],
providers: [],
imports: [
FormsModule,
ReactiveFormsModule,
NgSelectModule,
RouterTestingModule,
],
}).compileComponents()
fixture = TestBed.createComponent(SelectComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support private items', () => {
component.value = 3
component.items = items
expect(component.items).toContainEqual({
id: 3,
name: 'Private',
private: true,
})
component.checkForPrivateItems([4, 5])
expect(component.items).toContainEqual({
id: 4,
name: 'Private',
private: true,
})
expect(component.items).toContainEqual({
id: 5,
name: 'Private',
private: true,
})
})
it('should support suggestions', () => {
expect(component.value).toBeUndefined()
component.items = items
component.suggestions = [1, 2]
fixture.detectChanges()
const suggestionAnchor: HTMLAnchorElement =
fixture.nativeElement.querySelector('a')
suggestionAnchor.click()
expect(component.value).toEqual(1)
})
it('should support create new and emit the value', () => {
expect(component.allowCreateNew).toBeFalsy()
component.items = items
let createNewVal
component.createNew.subscribe((v) => (createNewVal = v))
expect(component.allowCreateNew).toBeTruthy()
component.onSearch({ term: 'foo' })
component.addItem(undefined)
expect(createNewVal).toEqual('foo')
component.addItem('bar')
expect(createNewVal).toEqual('bar')
component.onSearch({ term: 'baz' })
component.clickNew()
expect(createNewVal).toEqual('baz')
})
it('should clear search term on blur after delay', fakeAsync(() => {
const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
component.onBlur()
tick(3000)
expect(clearSpy).toHaveBeenCalled()
}))
})

View File

@@ -144,4 +144,8 @@ export class SelectComponent extends AbstractInputComponent<number> {
onFilterDocuments() {
this.filterDocuments.emit([this.items.find((i) => i.id === this.value)])
}
get filterButtonTitle() {
return $localize`Filter documents with this ${this.title}`
}
}

View File

@@ -2,7 +2,7 @@
<label class="form-label" for="tags" i18n>Tags</label>
<div class="input-group flex-nowrap">
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
<ng-select #tagSelect name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
[disabled]="disabled"
[multiple]="true"
[closeOnSelect]="false"
@@ -11,11 +11,7 @@
[addTag]="allowCreate ? createTagRef : false"
addTagText="Add tag"
i18n-addTagText
(change)="onChange(value)"
(search)="onSearch($event)"
(focus)="clearLastSearchTerm()"
(clear)="clearLastSearchTerm()"
(blur)="onBlur()">
(change)="onChange(value)">
<ng-template ng-label-tmp let-item="item">
<span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">

View File

@@ -0,0 +1,175 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import {
FormsModule,
ReactiveFormsModule,
NG_VALUE_ACCESSOR,
} from '@angular/forms'
import { TagsComponent } from './tags.component'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import {
DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL,
} from 'src/app/data/matching-model'
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
import { RouterTestingModule } from '@angular/router/testing'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { of } from 'rxjs'
import { TagService } from 'src/app/services/rest/tag.service'
import {
NgbAccordionModule,
NgbModal,
NgbModalModule,
NgbModalRef,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { CheckComponent } from '../check/check.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { TextComponent } from '../text/text.component'
import { ColorComponent } from '../color/color.component'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
import { SelectComponent } from '../select/select.component'
import { ColorSliderModule } from 'ngx-color/slider'
import { By } from '@angular/platform-browser'
const tags: PaperlessTag[] = [
{
id: 1,
name: 'Tag1',
is_inbox_tag: false,
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
},
{
id: 2,
name: 'Tag2',
is_inbox_tag: true,
matching_algorithm: MATCH_ALL,
match: 'str',
},
{
id: 10,
name: 'Tag10',
is_inbox_tag: false,
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
},
]
describe('TagsComponent', () => {
let component: TagsComponent
let fixture: ComponentFixture<TagsComponent>
let input: HTMLInputElement
let modalService: NgbModal
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
TagsComponent,
TagEditDialogComponent,
TextComponent,
ColorComponent,
IfOwnerDirective,
SelectComponent,
TextComponent,
PermissionsFormComponent,
ColorComponent,
CheckComponent,
],
providers: [
{
provide: TagService,
useValue: {
listAll: () =>
of({
results: tags,
}),
create: () =>
of({
name: 'bar',
id: 99,
color: '#fff000',
}),
},
},
],
imports: [
FormsModule,
ReactiveFormsModule,
NgSelectModule,
RouterTestingModule,
HttpClientTestingModule,
NgbModalModule,
NgbAccordionModule,
NgbPopoverModule,
],
}).compileComponents()
modalService = TestBed.inject(NgbModal)
fixture = TestBed.createComponent(TagsComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
window.PointerEvent = MouseEvent as any
})
it('should support suggestions', () => {
expect(component.value).toHaveLength(0)
component.value = []
component.tags = tags
component.suggestions = [1, 2]
fixture.detectChanges()
const suggestionAnchor: HTMLAnchorElement =
fixture.nativeElement.querySelector('a')
suggestionAnchor.click()
expect(component.value).toEqual([1])
})
it('should support create new and open a modal', () => {
let activeInstances: NgbModalRef[]
modalService.activeInstances.subscribe((v) => (activeInstances = v))
component.createTag('foo')
expect(modalService.hasOpenModals()).toBeTruthy()
expect(activeInstances[0].componentInstance.object.name).toEqual('foo')
})
it('should support create new using last search term and open a modal', () => {
let activeInstances: NgbModalRef[]
modalService.activeInstances.subscribe((v) => (activeInstances = v))
component.select.searchTerm = 'foobar'
component.createTag()
expect(modalService.hasOpenModals()).toBeTruthy()
expect(activeInstances[0].componentInstance.object.name).toEqual('foobar')
const editDialog = activeInstances[0]
.componentInstance as TagEditDialogComponent
editDialog.save() // create is mocked
fixture.detectChanges()
fixture.whenStable().then(() => {
expect(fixture.debugElement.nativeElement.textContent).toContain('foobar')
})
})
it('support remove tags', () => {
component.tags = tags
component.value = [1, 2]
component.removeTag(new PointerEvent('point'), 2)
expect(component.value).toEqual([1])
component.disabled = true
component.removeTag(new PointerEvent('point'), 1)
expect(component.value).toEqual([1])
})
it('should get tags', () => {
component.tags = null
expect(component.getTag(2)).toBeNull()
component.tags = tags
expect(component.getTag(2)).toEqual(tags[1])
expect(component.getTag(4)).toBeUndefined()
})
})

View File

@@ -5,12 +5,16 @@ import {
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { TagService } from 'src/app/services/rest/tag.service'
import { EditDialogMode } from '../../edit-dialog/edit-dialog.component'
import { first, firstValueFrom, tap } from 'rxjs'
import { NgSelectComponent } from '@ng-select/ng-select'
@Component({
providers: [
@@ -25,7 +29,10 @@ import { TagService } from 'src/app/services/rest/tag.service'
styleUrls: ['./tags.component.scss'],
})
export class TagsComponent implements OnInit, ControlValueAccessor {
constructor(private tagService: TagService, private modalService: NgbModal) {
constructor(
private tagService: TagService,
private modalService: NgbModal
) {
this.createTagRef = this.createTag.bind(this)
}
@@ -70,14 +77,14 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
@Output()
filterDocuments = new EventEmitter<PaperlessTag[]>()
value: number[]
@ViewChild('tagSelect') select: NgSelectComponent
tags: PaperlessTag[]
value: number[] = []
tags: PaperlessTag[] = []
public createTagRef: (name) => void
private _lastSearchTerm: string
getTag(id: number) {
if (this.tags) {
return this.tags.find((tag) => tag.id == id)
@@ -105,17 +112,22 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
var modal = this.modalService.open(TagEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = 'create'
modal.componentInstance.dialogMode = EditDialogMode.CREATE
if (name) modal.componentInstance.object = { name: name }
else if (this._lastSearchTerm)
modal.componentInstance.object = { name: this._lastSearchTerm }
modal.componentInstance.succeeded.subscribe((newTag) => {
this.tagService.listAll().subscribe((tags) => {
this.tags = tags.results
this.value = [...this.value, newTag.id]
this.onChange(this.value)
})
})
else if (this.select.searchTerm)
modal.componentInstance.object = { name: this.select.searchTerm }
this.select.searchTerm = null
this.select.detectChanges()
return firstValueFrom(
(modal.componentInstance as TagEditDialogComponent).succeeded.pipe(
first(),
tap(() => {
this.tagService.listAll().subscribe((tags) => {
this.tags = tags.results
})
})
)
)
}
getSuggestions() {
@@ -133,20 +145,6 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
this.onChange(this.value)
}
clearLastSearchTerm() {
this._lastSearchTerm = null
}
onSearch($event) {
this._lastSearchTerm = $event.term
}
onBlur() {
setTimeout(() => {
this.clearLastSearchTerm()
}, 3000)
}
get hasPrivate(): boolean {
return this.value.some(
(t) => this.tags?.find((t2) => t2.id === t) === undefined

View File

@@ -0,0 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import {
FormsModule,
ReactiveFormsModule,
NG_VALUE_ACCESSOR,
} from '@angular/forms'
import { TextComponent } from './text.component'
describe('TextComponent', () => {
let component: TextComponent
let fixture: ComponentFixture<TextComponent>
let input: HTMLInputElement
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [TextComponent],
providers: [],
imports: [FormsModule, ReactiveFormsModule],
}).compileComponents()
fixture = TestBed.createComponent(TextComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
input = component.inputField.nativeElement
})
it('should support use of input field', () => {
expect(component.value).toBeUndefined()
// TODO: why doesnt this work?
// input.value = 'foo'
// input.dispatchEvent(new Event('change'))
// fixture.detectChanges()
// expect(component.value).toEqual('foo')
})
})

View File

@@ -0,0 +1,18 @@
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.height]="height">
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
<g class="text" style="fill:#000">
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/>
<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/>
<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" transform="translate(0)"/>
<rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/>
<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/>
<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/>
<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/>
<rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/>
<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/>
<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/>
<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5 2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 " transform="translate(0)"/>
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { LogoComponent } from './logo.component'
import { By } from '@angular/platform-browser'
describe('LogoComponent', () => {
let component: LogoComponent
let fixture: ComponentFixture<LogoComponent>
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [LogoComponent],
})
fixture = TestBed.createComponent(LogoComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support extra classes', () => {
expect(fixture.debugElement.queryAll(By.css('.foo'))).toHaveLength(0)
component.extra_classes = 'foo'
fixture.detectChanges()
expect(fixture.debugElement.queryAll(By.css('.foo'))).toHaveLength(1)
})
it('should support setting height', () => {
expect(fixture.debugElement.query(By.css('svg')).attributes.height).toEqual(
'6em'
)
component.height = '10em'
fixture.detectChanges()
expect(fixture.debugElement.query(By.css('svg')).attributes.height).toEqual(
'10em'
)
})
})

View File

@@ -0,0 +1,18 @@
import { Component, Input } from '@angular/core'
@Component({
selector: 'app-logo',
templateUrl: './logo.component.html',
styleUrls: ['./logo.component.scss'],
})
export class LogoComponent {
@Input()
extra_classes: string
@Input()
height = '6em'
getClasses() {
return ['logo'].concat(this.extra_classes).join(' ')
}
}

View File

@@ -0,0 +1,36 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Title } from '@angular/platform-browser'
import { PageHeaderComponent } from './page-header.component'
import { environment } from 'src/environments/environment'
describe('PageHeaderComponent', () => {
let component: PageHeaderComponent
let fixture: ComponentFixture<PageHeaderComponent>
let titleService: Title
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [PageHeaderComponent],
providers: [],
imports: [],
}).compileComponents()
titleService = TestBed.inject(Title)
fixture = TestBed.createComponent(PageHeaderComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should display title + subtitle', () => {
component.title = 'Foo'
component.subTitle = 'Bar'
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('FooBar')
})
it('should set html title', () => {
const titleSpy = jest.spyOn(titleService, 'setTitle')
component.title = 'Foo Bar'
expect(titleSpy).toHaveBeenCalledWith(`Foo Bar - ${environment.appTitle}`)
})
})

View File

@@ -0,0 +1,90 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { PermissionsDialogComponent } from './permissions-dialog.component'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { UserService } from 'src/app/services/rest/user.service'
import { of } from 'rxjs'
import { PermissionsFormComponent } from '../input/permissions/permissions-form/permissions-form.component'
import { SelectComponent } from '../input/select/select.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
const set_permissions = {
owner: 10,
set_permissions: {
view: {
users: [1],
groups: [],
},
edit: {
users: [1],
groups: [],
},
},
}
describe('PermissionsDialogComponent', () => {
let component: PermissionsDialogComponent
let fixture: ComponentFixture<PermissionsDialogComponent>
let modal: NgbActiveModal
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
PermissionsDialogComponent,
SafeHtmlPipe,
SelectComponent,
PermissionsFormComponent,
PermissionsUserComponent,
PermissionsGroupComponent,
],
providers: [
NgbActiveModal,
{
provide: UserService,
useValue: {
listAll: () =>
of({
results: [
{
id: 1,
username: 'user1',
},
{
id: 10,
username: 'user10',
},
],
}),
},
},
],
imports: [
HttpClientTestingModule,
NgSelectModule,
FormsModule,
ReactiveFormsModule,
NgbModule,
],
}).compileComponents()
modal = TestBed.inject(NgbActiveModal)
fixture = TestBed.createComponent(PermissionsDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should return permissions', () => {
component.form.get('permissions_form').setValue(set_permissions)
expect(component.permissions).toEqual(set_permissions)
})
it('should close modal on cancel', () => {
const closeSpy = jest.spyOn(modal, 'close')
component.cancelClicked()
expect(closeSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,157 @@
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { of } from 'rxjs'
import { PermissionsService } from 'src/app/services/permissions.service'
import { UserService } from 'src/app/services/rest/user.service'
import {
OwnerFilterType,
PermissionsFilterDropdownComponent,
PermissionsSelectionModel,
} from './permissions-filter-dropdown.component'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
import { SettingsService } from 'src/app/services/settings.service'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
const currentUserID = 13
describe('PermissionsFilterDropdownComponent', () => {
let component: PermissionsFilterDropdownComponent
let fixture: ComponentFixture<PermissionsFilterDropdownComponent>
let ownerFilterSetResult: PermissionsSelectionModel
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [
PermissionsFilterDropdownComponent,
ClearableBadgeComponent,
IfPermissionsDirective,
],
providers: [
{
provide: UserService,
useValue: {
listAll: () =>
of({
results: [
{
id: 1,
username: 'user1',
},
{
id: 10,
username: 'user10',
},
],
}),
},
},
{
provide: PermissionsService,
useValue: {
currentUserCan: () => true,
},
},
{
provide: SettingsService,
useValue: {
currentUser: {
id: currentUserID,
},
},
},
],
imports: [
HttpClientTestingModule,
NgSelectModule,
FormsModule,
ReactiveFormsModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(PermissionsFilterDropdownComponent)
component = fixture.componentInstance
component.ownerFilterSet.subscribe(
(model) => (ownerFilterSetResult = model)
)
component.selectionModel = new PermissionsSelectionModel()
fixture.detectChanges()
})
it('should report is active', () => {
component.setFilter(OwnerFilterType.NONE)
expect(component.isActive).toBeFalsy()
component.setFilter(OwnerFilterType.OTHERS)
expect(component.isActive).toBeTruthy()
component.setFilter(OwnerFilterType.NONE)
component.selectionModel.hideUnowned = true
expect(component.isActive).toBeTruthy()
})
it('should support reset', () => {
component.setFilter(OwnerFilterType.OTHERS)
expect(component.selectionModel.ownerFilter).not.toEqual(
OwnerFilterType.NONE
)
component.reset()
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
})
it('should toggle owner filter type when users selected', () => {
component.selectionModel.ownerFilter = OwnerFilterType.NONE
// this would normally be done by select component
component.selectionModel.includeUsers = [12]
component.onUserSelect()
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.OTHERS)
// this would normally be done by select component
component.selectionModel.includeUsers = null
component.onUserSelect()
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
})
it('should emit a selection model depending on the type of owner filter set', () => {
component.selectionModel.ownerFilter = OwnerFilterType.NONE
component.setFilter(OwnerFilterType.SELF)
expect(ownerFilterSetResult).toEqual({
excludeUsers: [],
hideUnowned: false,
includeUsers: [],
ownerFilter: OwnerFilterType.SELF,
userID: currentUserID,
})
component.setFilter(OwnerFilterType.NOT_SELF)
expect(ownerFilterSetResult).toEqual({
excludeUsers: [currentUserID],
hideUnowned: false,
includeUsers: [],
ownerFilter: OwnerFilterType.NOT_SELF,
userID: null,
})
component.setFilter(OwnerFilterType.NONE)
expect(ownerFilterSetResult).toEqual({
excludeUsers: [],
hideUnowned: false,
includeUsers: [],
ownerFilter: OwnerFilterType.NONE,
userID: null,
})
component.setFilter(OwnerFilterType.UNOWNED)
expect(ownerFilterSetResult).toEqual({
excludeUsers: [],
hideUnowned: false,
includeUsers: [],
ownerFilter: OwnerFilterType.UNOWNED,
userID: null,
})
})
})

View File

@@ -0,0 +1,96 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { PermissionsSelectComponent } from './permissions-select.component'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import {
PermissionAction,
PermissionType,
} from 'src/app/services/permissions.service'
import { By } from '@angular/platform-browser'
const permissions = [
'add_document',
'view_document',
'change_document',
'delete_document',
'change_tag',
'view_documenttype',
]
const inheritedPermissions = ['change_tag', 'view_documenttype']
describe('PermissionsSelectComponent', () => {
let component: PermissionsSelectComponent
let fixture: ComponentFixture<PermissionsSelectComponent>
let permissionsChangeResult: Permissions
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [PermissionsSelectComponent],
providers: [],
imports: [FormsModule, ReactiveFormsModule, NgbModule],
}).compileComponents()
fixture = TestBed.createComponent(PermissionsSelectComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
component.registerOnChange((r) => (permissionsChangeResult = r))
fixture.detectChanges()
})
it('should create controls for all PermissionType and PermissionAction', () => {
expect(Object.values(component.form.controls)).toHaveLength(
Object.keys(PermissionType).length
)
for (var type in component.form.controls) {
expect(
Object.values(component.form.controls[type].controls)
).toHaveLength(Object.keys(PermissionAction).length)
}
// coverage
component.registerOnTouched(() => {})
component.setDisabledState(true)
})
it('should allow toggle all on / off', () => {
component.ngOnInit()
expect(component.typesWithAllActions.values).toHaveLength(0)
component.toggleAll({ target: { checked: true } }, 'Tag')
expect(component.typesWithAllActions).toContain('Tag')
component.toggleAll({ target: { checked: false } }, 'Tag')
expect(component.typesWithAllActions.values).toHaveLength(0)
})
it('should update on permissions set', () => {
component.ngOnInit()
component.writeValue(permissions)
expect(permissionsChangeResult).toEqual(permissions)
expect(component.typesWithAllActions).toContain('Document')
})
it('should update checkboxes on permissions set', () => {
component.ngOnInit()
component.writeValue(permissions)
fixture.detectChanges()
const input1 = fixture.debugElement.query(By.css('input#Document_Add'))
expect(input1.nativeElement.checked).toBeTruthy()
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
expect(input2.nativeElement.checked).toBeTruthy()
})
it('disable checkboxes when permissions are inherited', () => {
component.ngOnInit()
component.inheritedPermissions = inheritedPermissions
expect(component.isInherited('Document', 'Add')).toBeFalsy()
expect(component.isInherited('Document')).toBeFalsy()
expect(component.isInherited('Tag', 'Change')).toBeTruthy()
const input1 = fixture.debugElement.query(By.css('input#Document_Add'))
expect(input1.nativeElement.disabled).toBeFalsy()
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
expect(input2.nativeElement.disabled).toBeTruthy()
})
})

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