Compare commits

...

106 Commits

Author SHA1 Message Date
shamoon
f02e8e0dc3 Bump version to 2.4.0 2024-01-18 17:43:49 -08:00
github-actions[bot]
c1ed87a44f New Crowdin translations by GitHub Action (#5349)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-18 17:42:21 -08:00
shamoon
16169ca331 Chore: Close outdated support / general discussions (#5443) 2024-01-18 19:46:12 +00:00
shamoon
26900e0766 Fix: doc link removal before assigning value (#5451) 2024-01-18 06:58:41 -08:00
JigSaw
aa798604b3 Fix typo in bug report template (#5450) 2024-01-18 14:28:33 +00:00
shamoon
bb98fc5f65 Chore: better bootstrap icons (#5403) 2024-01-18 00:27:38 +00:00
shamoon
dc1918ad10 Fix: dont lose permissions ui if owner changed from null (#5433) 2024-01-17 17:44:04 +00:00
shamoon
ea632d0417 Fix missing frontend test imports 2024-01-16 23:01:50 -08:00
shamoon
648dc709fd Fix: tweak how auto-scrolling of logs works 2024-01-16 23:00:18 -08:00
shamoon
1a84f6a20e Fix: change auto-refresh click target 2024-01-16 22:58:41 -08:00
shamoon
96af953e6f Fix: save button layout with long button translation text 2024-01-16 21:44:41 -08:00
shamoon
6db9e292ba Enhancement: support remote user auth directly against API (DRF) (#5386) 2024-01-16 23:26:05 +00:00
shamoon
2e2362e2df Fix: outdated confirm dialog confirm 2024-01-16 15:18:26 -08:00
Trenton H
51dd95be3d Fix: Getting next ASN when no documents have an ASN (#5431)
* Fixes the next ASN logic to account for no ASNs yet being assigned

* Updates so the ASN will start at 1

* Do the same calculation without the branch
2024-01-16 23:08:37 +00:00
Trenton H
e16645b146 Feature: Add additional caching support to suggestions and metadata (#5414)
* Adds ETag and Last-Modified headers to suggestions, metadata and previews

* Slight update to the suggestions etag

* Small user message for why classifier didn't train again
2024-01-16 17:01:07 +00:00
dependabot[bot]
0068f091bb Chore(deps): Bump the small-changes group with 2 updates (#5413)
Bumps the small-changes group with 2 updates: [channels-redis](https://github.com/django/channels_redis) and [gotenberg-client](https://github.com/stumpylog/gotenberg-client).


Updates `channels-redis` from 4.1.0 to 4.2.0
- [Changelog](https://github.com/django/channels_redis/blob/main/CHANGELOG.txt)
- [Commits](https://github.com/django/channels_redis/commits)

Updates `gotenberg-client` from 0.4.1 to 0.5.0
- [Release notes](https://github.com/stumpylog/gotenberg-client/releases)
- [Changelog](https://github.com/stumpylog/gotenberg-client/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/gotenberg-client/compare/0.4.1...0.5.0)

---
updated-dependencies:
- dependency-name: channels-redis
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
- dependency-name: gotenberg-client
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 16:14:45 +00:00
dependabot[bot]
ad6efd2898 Chore(deps-dev): Bump the development group with 2 updates (#5412)
Bumps the development group with 2 updates: [ruff](https://github.com/astral-sh/ruff) and [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


Updates `ruff` from 0.1.11 to 0.1.13
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.11...v0.1.13)

Updates `mkdocs-material` from 9.5.3 to 9.5.4
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.3...9.5.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: mkdocs-material
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 16:02:34 +00:00
shamoon
86e380bb1c Fix signin username floating label (#5424) 2024-01-16 07:32:07 -08:00
shamoon
58aacd4814 Feature: help tooltips (#5383) 2024-01-15 15:40:36 -08:00
shamoon
ad07791bac Enhancement / QoL: show selected tasks count (#5379) 2024-01-15 22:15:30 +00:00
shamoon
783090c2cd Fix shared by me filter with multiple users / groups in postgres (#5396) 2024-01-15 22:06:59 +00:00
Trenton H
41a3c7c89b Fix: Catch new warning when loading the classifier (#5395) 2024-01-14 13:21:17 -08:00
shamoon
16cc7415c1 Fix missed migration for app_logo 2024-01-13 16:23:44 -08:00
shamoon
98c5cf89ef Fix: doc detail component fixes (#5373) 2024-01-13 21:40:22 +00:00
shamoon
53e04e66cf Enhancement: warn when outdated doc detected (#5372)
* Update modified property for target docs w bidirectional links

* Warn on doc change detected
2024-01-13 20:28:10 +00:00
shamoon
2a6e79acc8 Feature: app branding (#5357) 2024-01-13 19:57:25 +00:00
Trenton H
2da5e46386 Refactor file consumption task to allow beginnings of a plugin system (#5367) 2024-01-13 16:11:14 +00:00
Trenton H
4dbf8d7969 Reapply #5304 fix 2024-01-12 13:19:24 -08:00
shamoon
4a52fc27d4 Reset dev version string 2024-01-11 21:13:51 -08:00
shamoon
05cd34c8af Merge branch 'main' into dev 2024-01-11 21:13:44 -08:00
dependabot[bot]
8a622181fc Chore(deps-dev): Bump jinja2 from 3.1.2 to 3.1.3 (#5352)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-11 13:26:54 -08:00
github-actions[bot]
13f38bf3a1 [Documentation] Add v2.3.3 changelog (#5346)
* Changelog v2.3.3 - GHA

* Fix PR categorization, as usual

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-11 07:17:26 -08:00
shamoon
16acc2d6ad Merge branch 'dev' 2024-01-10 15:59:50 -08:00
github-actions[bot]
c2c9a953d3 New Crowdin translations by GitHub Action (#5314)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-10 15:58:17 -08:00
shamoon
530f4a8b28 Bump version to 2.3.3 2024-01-10 15:55:09 -08:00
shamoon
8eb1dc4f62 Merge branch 'dev' 2024-01-10 15:54:47 -08:00
shamoon
a2b87fe012 Trigger crowdin action on changes to frontend strings file 2024-01-10 15:54:02 -08:00
shamoon
3dcb973adb Enhancement: Explain behavior of unset app config boolean to user (#5345) 2024-01-10 15:34:20 -08:00
shamoon
8e8810cbaa Fix: correct OCR_MAX_IMAGE_PIXELS doc link 2024-01-10 13:22:05 -08:00
shamoon
b0aeec4c43 Fix: Coerce language app config field to None if empty 2024-01-10 13:21:51 -08:00
shamoon
1ac298f6ff Fix empty assign_title validation 2024-01-10 12:53:35 -08:00
shamoon
6d5f4e92cc Enhancement: title assignment placeholder error handling, fallback (#5282) 2024-01-10 10:18:55 -08:00
shamoon
416ad13aaf Update config page text 2024-01-09 11:37:39 -08:00
Trenton H
a7e1299194 Updates all backend, hooks and configures codespell in a slightly easier way (#5336) 2024-01-09 10:30:33 -08:00
Trenton H
a12e1fae72 Fix: Don't require the JSON user arguments field (#5320)
* Allows new user args field to be null

* Coerce empty string to None for user_args JSONField

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-08 13:14:36 -08:00
shamoon
f525ac0af6 Chore: add pre-commit hook for codespell (#5324) 2024-01-08 13:03:05 -08:00
luzpaz
58bf9c552b Documentation: Fix typos with automated tool (#5319)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-08 16:58:41 +00:00
github-actions[bot]
22d257cd1f Changelog v2.3.2 - GHA (#5310)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-07 23:32:11 -08:00
Trenton Holmes
f1bf1ddc54 Resets version string 2024-01-07 17:03:46 -08:00
Trenton Holmes
6015cc0e4a Bumps version to 2.3.2 2024-01-07 17:02:36 -08:00
Trenton Holmes
f9926d77d5 Merge remote-tracking branch 'origin/dev' 2024-01-07 16:47:52 -08:00
Colin Hebert
4f85dcecfc Deployment: Use the default Docker healthcheck from the Dockerfile (Part 2) (#5224)
* Set default healthcheck

* Rely on default healthcheck
2024-01-07 22:49:29 +00:00
github-actions[bot]
30c31a3d4c New Crowdin translations by GitHub Action (#5309)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-07 14:37:33 -08:00
shamoon
c64667d396 Fix: workflow assignment of customfield fails if field exists in v2.3.1 (#5302) 2024-01-07 22:27:57 +00:00
github-actions[bot]
9f6613fe05 New Crowdin translations by GitHub Action (#5268)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-07 14:19:57 -08:00
Trenton H
ea47af7034 Fixes the user arguments json field decoding (#5307) 2024-01-07 14:17:51 -08:00
shamoon
d46abeff01 Fix: empty match field cannot be saved (#5301) 2024-01-07 22:10:26 +00:00
Trenton H
2b39697ffb Fixes usages of UTC datetime instead of local datetime (#5304) 2024-01-07 13:57:40 -08:00
shamoon
4b00a72ff5 Fix: path fnmatch case note 2024-01-07 10:48:04 -08:00
shamoon
e590b2482e Update frontend strings 2024-01-07 08:46:32 -08:00
github-actions[bot]
eb7dd80410 Changelog v2.3.1 - GHA (#5283)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-07 08:44:12 -08:00
shamoon
86338465fb Fix: workflow edit form loses unsaved changes in v2.3.1 (#5299) 2024-01-07 08:16:58 -08:00
shamoon
a41dbdd12c Reset dev version string 2024-01-06 22:43:25 -08:00
shamoon
1e10a438cd Bump version to 2.3.1 2024-01-06 22:42:35 -08:00
shamoon
ab34ea724d Merge branch 'dev' 2024-01-06 22:42:16 -08:00
shamoon
fd8bfe1a80 Fix: edit workflow form not displaying trigger settings in v2.3.0 (#5276) 2024-01-06 17:27:49 +00:00
Trenton H
9043f45350 Adds more documentation for OCR_PAGES and prevents using 0 for actual OCR (#5275) 2024-01-06 09:06:41 -08:00
github-actions[bot]
5921e6d13e [Documentation] Add v2.3.0 changelog (#5263)
* Changelog v2.3.0 - GHA

* Re-categorize some PRs

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-06 09:06:23 -08:00
shamoon
ee2bfe2350 Reset dev version string 2024-01-05 22:12:29 -08:00
shamoon
0957a7ca8e Bump version to 2.3.0 2024-01-05 21:36:48 -08:00
shamoon
f4e75c7fb7 Merge branch 'dev' 2024-01-05 21:36:01 -08:00
shamoon
fae0e3b405 Update api version to v4 2024-01-05 21:35:40 -08:00
github-actions[bot]
ef335517ce New Crowdin translations by GitHub Action (#5146)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-01-05 21:28:32 -08:00
shamoon
e2be166e67 Update django.po 2024-01-05 21:26:30 -08:00
Trenton H
37e34d92de Replaces deprecated Django with standard library (#5262) 2024-01-05 21:20:38 -08:00
Trenton H
bd35030c59 Fix: Crash in barcode ASN reading when the file type isn't supported (#5261)
* Fixes a random crash in the barcode ASN reading so it doesn't try to access a not created temp dir

* Don't parse the barcodes twice, store the result instead
2024-01-06 05:08:24 +00:00
Trenton H
a82e3771ae Fix: Allows pre-consume scripts to modify the working path again (#5260)
* Allows pre-consume scripts to modify the working path again and generally cleans up some confusion about working copy vs original
2024-01-05 21:01:57 -08:00
shamoon
3115106dc1 Enhancement: add basic filters for listing custom fields (#5257) 2024-01-06 03:04:31 +00:00
shamoon
d623af9c41 Change: Use fnmatch for workflow path matching (#5250) 2024-01-05 19:15:14 +00:00
Thomas Falkenberg
355a434a07 Enhancement: fetch mails in bulk (#5249)
Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
2024-01-05 10:43:22 -08:00
Trenton H
8da2535a65 Fix: zip exports not respecting the --delete option (#5245) 2024-01-04 19:58:58 +00:00
ChrisRBe
5963dfe41b Fix: correctly format tip admonition (#5229)
format for material admonition requires a line break and another
level of indentation which was not correctly applied on this line.
2024-01-03 14:34:59 -08:00
shamoon
c6dcaa0472 Documentation: update screenshot / features list for workflows 2024-01-03 08:35:09 -08:00
shamoon
21063a5c22 Fix: workflow trigger / action ID color legibility 2024-01-03 08:33:19 -08:00
shamoon
ba2f51bed1 Fix: consistent workflow action name display 2024-01-03 00:45:22 -08:00
Bevan Kay
bbf64b7e93 Enhancement: add storage_path parameter to post_document API (#5217)
* Feature: add `storage_path` parameter to post_document API

* Complete coverage for validate_storage_path

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-03 08:31:56 +00:00
shamoon
3b6ce16f1c Feature: Workflows (#5121) 2024-01-03 08:19:19 +00:00
Colin Hebert
46e6be319f Set Docker default healthcheck in the Dockerfile (#5199) 2024-01-02 09:08:10 -08:00
shamoon
e6d6f21d33 Fix: fix filename format remove none when part of directory (#5210) 2024-01-02 15:17:18 +00:00
dependabot[bot]
77b9b79a9e Chore(deps): Bump the actions group with 5 updates (#5203)
* Chore(deps): Bump the actions group with 5 updates

Bumps the actions group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-python](https://github.com/actions/setup-python) | `4` | `5` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `3` | `4` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `3` | `4` |
| [github/codeql-action](https://github.com/github/codeql-action) | `2` | `3` |
| [actions/stale](https://github.com/actions/stale) | `8` | `9` |


Updates `actions/setup-python` from 4 to 5
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

Updates `actions/upload-artifact` from 3 to 4
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

Updates `actions/download-artifact` from 3 to 4
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

Updates `github/codeql-action` from 2 to 3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

Updates `actions/stale` from 8 to 9
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* See if this fixes broken frontend coverage actions/upload & download

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-01-02 03:29:48 +00:00
dependabot[bot]
f0016ad70c Chore(deps): Bump the frontend-angular-dependencies group (#5204)
Bumps the frontend-angular-dependencies group in /src-ui with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `17.0.7` | `17.0.8` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `17.0.7` | `17.0.8` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `17.0.7` | `17.0.8` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `17.0.7` | `17.0.8` |
| [@angular/localize](https://github.com/angular/angular) | `17.0.7` | `17.0.8` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `17.0.7` | `17.0.8` |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `17.0.7` | `17.0.8` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `17.0.7` | `17.0.8` |
| [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | `17.0.7` | `17.0.8` |
| [@angular/cli](https://github.com/angular/angular-cli) | `17.0.7` | `17.0.8` |


Updates `@angular/common` from 17.0.7 to 17.0.8
- [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/17.0.8/packages/common)

Updates `@angular/compiler` from 17.0.7 to 17.0.8
- [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/17.0.8/packages/compiler)

Updates `@angular/core` from 17.0.7 to 17.0.8
- [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/17.0.8/packages/core)

Updates `@angular/forms` from 17.0.7 to 17.0.8
- [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/17.0.8/packages/forms)

Updates `@angular/localize` from 17.0.7 to 17.0.8
- [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/17.0.7...17.0.8)

Updates `@angular/platform-browser` from 17.0.7 to 17.0.8
- [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/17.0.8/packages/platform-browser)

Updates `@angular/platform-browser-dynamic` from 17.0.7 to 17.0.8
- [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/17.0.8/packages/platform-browser-dynamic)

Updates `@angular/router` from 17.0.7 to 17.0.8
- [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/17.0.8/packages/router)

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

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

---
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: "@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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 21:47:37 +00:00
dependabot[bot]
054468ffc2 Chore(deps-dev): Bump @types/node from 20.10.4 to 20.10.6 in /src-ui (#5207)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.10.4 to 20.10.6.
- [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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 21:30:31 +00:00
dependabot[bot]
607c1282e3 Chore(deps-dev): Bump the frontend-eslint-dependencies group (#5205)
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.14.0 to 6.17.0
- [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.17.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 6.14.0 to 6.17.0
- [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.17.0/packages/parser)

Updates `eslint` from 8.55.0 to 8.56.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.55.0...v8.56.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 13:22:28 -08:00
Christoph Loy
3f4f4444f7 Document invalid options in rootless containers (#5122)
Specifying `PAPERLESS_OCR_LANGUAGES` in rootless containers is not possible since the extra languages are currently installed with apt which requires root.

Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
2024-01-01 08:00:05 -08:00
Antoine Libert
54372b5618 Fix: improve performance of listing result IDs (#5195) 2024-01-01 07:58:43 -08:00
Johannes Schneider
670a3f6c7f fix command for Docker version check (#5196) 2023-12-31 14:21:40 -08:00
shamoon
35a4d3fb54 Disable custom field remove button if user does not have permissions (#5194) 2023-12-31 10:55:25 -08:00
shamoon
fb81612ed1 Fix overlap of focus highlight on login screen (#5193) 2023-12-31 10:01:45 -08:00
shamoon
c5d622279c Fix symmetric doc links with target value None (#5187) 2023-12-31 07:56:26 -08:00
amo13
b93f655039 Create parent dirs of data/media/consume if necessary (#5176)
* Create parent dirs of data/media/consume if necessary
* long --parents instead of short -p
2023-12-30 19:25:34 +00:00
shamoon
428ffb4729 Fix: setting empty doc link with docs to be removed (#5174) 2023-12-30 07:43:29 -08:00
Trenton H
061f33fb05 Feature: Allow setting backend configuration settings via the UI (#5126)
* Saving some start on this

* At least partially working for the tesseract parser

* Problems with migration testing need to figure out

* Work around that error

* Fixes max m_pixels

* Moving the settings to main paperless application

* Starting some consumer options

* More fixes and work

* Fixes these last tests

* Fix max_length on OcrSettings.mode field

* Fix all fields on Common & Ocr settings serializers

* Umbrellla config view

* Revert "Umbrellla config view"

This reverts commit fbaf9f4be30f89afeb509099180158a3406416a5.

* Updates to use a single configuration object for all settings

* Squashed commit of the following:

commit 8a0a49dd57
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 23:02:47 2023 -0800

    Fix formatting

commit 66b2d90c50
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 22:36:35 2023 -0800

    Refactor frontend data models

commit 5723bd8dd8
Author: Adam Bogdał <adam@bogdal.pl>
Date:   Wed Dec 20 01:17:43 2023 +0100

    Fix: speed up admin panel for installs with a large number of documents (#5052)

commit 9b08ce1761
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 15:18:51 2023 -0800

    Update PULL_REQUEST_TEMPLATE.md

commit a6248bec2d
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 15:02:05 2023 -0800

    Chore: Update Angular to v17 (#4980)

commit b1f6f52486
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 13:53:56 2023 -0800

    Fix: Dont allow null custom_fields property via API (#5063)

commit 638d9970fd
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 13:43:50 2023 -0800

    Enhancement: symmetric document links (#4907)

commit 5e8de4c1da
Author: shamoon <4887959+shamoon@users.noreply.github.com>
Date:   Tue Dec 19 12:45:04 2023 -0800

    Enhancement: shared icon & shared by me filter (#4859)

commit 088bad9030
Author: Trenton H <797416+stumpylog@users.noreply.github.com>
Date:   Tue Dec 19 12:04:03 2023 -0800

    Bulk updates all the backend libraries (#5061)

* Saving some work on frontend config

* Very basic but dynamically-generated config form

* Saving work on slightly less ugly frontend config

* JSON validation for user_args field

* Fully dynamic config form

* Adds in some additional validators for a nicer error message

* Cleaning up the testing and coverage more

* Reverts unintentional change

* Adds documentation about the settings and the precedence

* Couple more commenting and style fixes

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2023-12-29 15:42:56 -08:00
shamoon
da058b915b Enhancement: improve validation of custom field values (#5166)
* Support all URI schemes

* Reworks custom field value validation to check and return a 400 error code in more cases and support more URL looking items, not just some basic schemes

* Fixes a spelling error in the message

---------

Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
2023-12-29 14:45:29 -08:00
shamoon
cf869b1356 Fix: type casting of db values for shared by me filter (#5155) 2023-12-29 17:19:45 +00:00
shamoon
05e294fc81 Fix URL validation of empty string 2023-12-29 01:26:24 -08:00
github-actions[bot]
bd904d9e6b Changelog v2.2.1 - GHA (#5147)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2023-12-28 23:57:12 -08:00
314 changed files with 114594 additions and 77490 deletions

3
.codespellrc Normal file
View File

@@ -0,0 +1,3 @@
[codespell]
write-changes = True
ignore-words-list = criterias,afterall,valeu,ureue,equest,ure

View File

@@ -20,7 +20,7 @@ body:
- [The troubleshooting documentation](https://docs.paperless-ngx.com/troubleshooting/).
- [The installation instructions](https://docs.paperless-ngx.com/setup/#installation).
- [Existing issues and discussions](https://github.com/paperless-ngx/paperless-ngx/search?q=&type=issues).
- Disable any customer container initialization scripts, if using
- Disable any custom container initialization scripts, if using
If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support).
- type: textarea

View File

@@ -37,7 +37,7 @@ jobs:
uses: actions/checkout@v4
-
name: Install python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
-
@@ -56,7 +56,7 @@ jobs:
-
name: Set up Python
id: setup-python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
cache: "pipenv"
@@ -87,7 +87,7 @@ jobs:
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs gh-deploy --force --no-history
-
name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: documentation
path: site/
@@ -114,7 +114,7 @@ jobs:
-
name: Set up Python
id: setup-python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "${{ matrix.python-version }}"
cache: "pipenv"
@@ -155,7 +155,7 @@ jobs:
-
name: Upload coverage
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: backend-coverage-report
path: src/coverage.xml
@@ -169,7 +169,7 @@ jobs:
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
install-frontend-depedendencies:
name: "Install Frontend Dependendencies"
name: "Install Frontend Dependencies"
runs-on: ubuntu-22.04
needs:
- pre-commit
@@ -182,7 +182,7 @@ jobs:
node-version: 20.x
cache: 'npm'
cache-dependency-path: 'src-ui/package-lock.json'
- name: Cache frontend depdendencies
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v3
with:
@@ -219,7 +219,7 @@ jobs:
node-version: 20.x
cache: 'npm'
cache-dependency-path: 'src-ui/package-lock.json'
- name: Cache frontend depdendencies
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v3
with:
@@ -238,7 +238,7 @@ jobs:
-
name: Upload Jest coverage
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: jest-coverage-report-${{ matrix.shard-index }}
path: |
@@ -253,9 +253,9 @@ jobs:
-
name: Upload Playwright test results
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: playwright-report
name: playwright-report-${{ matrix.shard-index }}
path: src-ui/playwright-report
retention-days: 7
@@ -269,10 +269,18 @@ jobs:
-
uses: actions/checkout@v4
-
name: Download frontend coverage
uses: actions/download-artifact@v3
name: Download frontend jest coverage
uses: actions/download-artifact@v4
with:
path: src-ui/coverage/
pattern: jest-coverage-report-*
-
name: Download frontend playwright coverage
uses: actions/download-artifact@v4
with:
path: src-ui/coverage/
pattern: playwright-report-*
merge-multiple: true
-
name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v3
@@ -285,7 +293,7 @@ jobs:
files: '!coverage.xml'
-
name: Download backend coverage
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: backend-coverage-report
path: src/
@@ -416,7 +424,7 @@ jobs:
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
-
name: Upload frontend artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: frontend-compiled
path: src/documents/static/frontend/
@@ -435,7 +443,7 @@ jobs:
-
name: Set up Python
id: setup-python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
cache: "pipenv"
@@ -461,13 +469,13 @@ jobs:
sudo apt-get install -qq --no-install-recommends gettext liblept5
-
name: Download frontend artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: frontend-compiled
path: src/documents/static/frontend/
-
name: Download documentation artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: documentation
path: docs/_build/html/
@@ -533,7 +541,7 @@ jobs:
tar -cJf paperless-ngx.tar.xz paperless-ngx/
-
name: Upload release artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: release
path: dist/paperless-ngx.tar.xz
@@ -552,7 +560,7 @@ jobs:
steps:
-
name: Download release artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: release
path: ./
@@ -603,7 +611,7 @@ jobs:
ref: main
-
name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
cache: "pipenv"

View File

@@ -42,7 +42,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,4 +51,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View File

@@ -7,6 +7,7 @@ on:
push:
paths: [
'src/locale/**',
'src-ui/messages.xlf',
'src-ui/src/locale/**'
]
branches: [ dev ]

View File

@@ -18,7 +18,7 @@ jobs:
name: 'Stale'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
- uses: actions/stale@v9
with:
days-before-stale: 7
days-before-close: 14
@@ -81,7 +81,7 @@ jobs:
console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`)
for (const discussion of result.repository.discussions.nodes) {
console.log(`Closing dicussion #${discussion.number} (${discussion.id})`)
console.log(`Closing discussion #${discussion.number} (${discussion.id})`)
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
@@ -107,3 +107,94 @@ jobs:
await sleep(1000)
}
close-outdated-discussions:
name: 'Close Outdated Discussions'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const CUTOFF_DAYS = 180;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - CUTOFF_DAYS);
const query = `query(
$owner:String!,
$name:String!,
$supportCategory:ID!,
$generalCategory:ID!,
) {
supportDiscussions: repository(owner:$owner, name:$name){
discussions(
categoryId:$supportCategory,
last:50,
answered:false,
states:[OPEN],
) {
nodes {
id,
number,
updatedAt
}
},
},
generalDiscussions: repository(owner:$owner, name:$name){
discussions(
categoryId:$generalCategory,
last:50,
states:[OPEN],
) {
nodes {
id,
number,
updatedAt
}
}
}
}`;
const variables = {
owner: context.repo.owner,
name: context.repo.repo,
supportCategory: "DIC_kwDOG1Zs184CBKWK",
generalCategory: "DIC_kwDOG1Zs184CBKWJ"
}
const result = await github.graphql(query, variables);
const combinedDiscussions = [
...result.supportDiscussions.discussions.nodes,
...result.generalDiscussions.discussions.nodes,
]
console.log(`Checking ${combinedDiscussions.length} open discussions`);
for (const discussion of combinedDiscussions) {
if (new Date(discussion.updatedAt) < cutoff) {
console.log(`Closing outdated discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt}`);
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
clientMutationId
}
}`;
const commentVariables = {
discussion: discussion.id,
body: 'This discussion has been automatically closed due to inactivity.',
}
await github.graphql(addCommentMutation, commentVariables);
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
clientMutationId
}
}`;
const closeVariables = {
discussion: discussion.id,
reason: "OUTDATED",
}
await github.graphql(closeDiscussionMutation, closeVariables);
await sleep(1000);
}
}

View File

@@ -11,6 +11,8 @@ repos:
- id: check-json
exclude: "tsconfig.*json"
- id: check-yaml
args:
- "--unsafe"
- id: check-toml
- id: check-executables-have-shebangs
- id: end-of-file-fixer
@@ -26,6 +28,14 @@ repos:
- svg
- id: check-case-conflict
- id: detect-private-key
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
exclude_types:
- pofile
- json
- repo: https://github.com/pre-commit/mirrors-prettier
rev: 'v3.1.0'
hooks:
@@ -37,11 +47,11 @@ repos:
exclude: "(^Pipfile\\.lock$)"
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.1.5'
rev: 'v0.1.11'
hooks:
- id: ruff
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.11.0
rev: 23.12.1
hooks:
- id: black
# Dockerfile hooks

View File

@@ -189,7 +189,7 @@ RUN set -eux \
&& chmod 755 /usr/local/bin/paperless_cmd.sh \
&& mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \
&& chmod 755 /usr/local/bin/flower-conditional.sh \
&& echo "Installing managment commands" \
&& echo "Installing management commands" \
&& chmod +x install_management_commands.sh \
&& ./install_management_commands.sh
@@ -270,3 +270,5 @@ ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
EXPOSE 8000
CMD ["/usr/local/bin/paperless_cmd.sh"]
HEALTHCHECK --interval=30s --timeout=10s --retries=5 CMD [ "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000" ]

View File

@@ -7,7 +7,7 @@ name = "pypi"
dateparser = "~=1.2"
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
django = "~=4.2.8"
django = "~=4.2.9"
django-auditlog = "*"
django-celery-results = "*"
django-compression-middleware = "*"
@@ -57,7 +57,7 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
[dev-packages]
# Linting
black = "==23.11.0"
black = "*"
pre-commit = "*"
ruff = "*"
# Testing

1620
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# Docker Compose file for running paperless testing with actual gotenberg
# and Tika containers for a more end to end test of the Tika related functionality
# Can be used locally or by the CI to start the nessecary containers with the
# Can be used locally or by the CI to start the necessary containers with the
# correct networking for the tests
version: "3.7"

View File

@@ -60,11 +60,6 @@ services:
- tika
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media

View File

@@ -54,11 +54,6 @@ services:
- broker
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
@@ -73,7 +68,6 @@ services:
PAPERLESS_DBPASS: paperless # only needed if non-default password
PAPERLESS_DBPORT: 3306
volumes:
data:
media:

View File

@@ -54,11 +54,6 @@ services:
- broker
ports:
- "8010:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media

View File

@@ -58,11 +58,6 @@ services:
- tika
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media

View File

@@ -52,11 +52,6 @@ services:
- broker
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
@@ -67,7 +62,6 @@ services:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
volumes:
data:
media:

View File

@@ -47,11 +47,6 @@ services:
- tika
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media

View File

@@ -38,11 +38,6 @@ services:
- broker
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
@@ -52,7 +47,6 @@ services:
environment:
PAPERLESS_REDIS: redis://broker:6379
volumes:
data:
media:

View File

@@ -86,13 +86,13 @@ initialize() {
"${CONSUME_DIR}"; do
if [[ ! -d "${dir}" ]]; then
echo "Creating directory ${dir}"
mkdir "${dir}"
mkdir --parents "${dir}"
fi
done
local -r tmp_dir="/tmp/paperless"
echo "Creating directory ${tmp_dir}"
mkdir -p "${tmp_dir}"
mkdir --parents "${tmp_dir}"
set +e
echo "Adjusting permissions of paperless files. This may take a while."

View File

@@ -136,6 +136,11 @@ script can access the following relevant environment variables set:
be triggered, leading to failures as two tasks work on the
same document path
!!! warning
If your script modifies `DOCUMENT_WORKING_PATH` in a non-deterministic
way, this may allow duplicate documents to be stored
A simple but common example for this would be creating a simple script
like this:
@@ -608,7 +613,7 @@ scan a completely new "odd numbered pages" one. The old staging file will get di
The collation feature can be used together with the [subdirs as tags](configuration.md#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
in the hierarchy 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`.

View File

@@ -8,7 +8,6 @@ most of the available filters and ordering fields.
The API provides the following main endpoints:
- `/api/consumption_templates/`: Full CRUD support.
- `/api/correspondents/`: Full CRUD support.
- `/api/custom_fields/`: Full CRUD support.
- `/api/documents/`: Full CRUD support, except POSTing new documents.
@@ -24,6 +23,7 @@ The API provides the following main endpoints:
- `/api/tags/`: Full CRUD support.
- `/api/tasks/`: Read-only.
- `/api/users/`: Full CRUD support.
- `/api/workflows/`: Full CRUD support.
All of these endpoints except for the logging endpoint allow you to
fetch (and edit and delete where appropriate) individual objects by
@@ -139,7 +139,7 @@ document. Paperless only reports PDF metadata at this point.
## Authorization
The REST api provides three different forms of authentication.
The REST api provides four different forms of authentication.
1. Basic authentication
@@ -177,6 +177,12 @@ The REST api provides three different forms of authentication.
Tokens can also be managed in the Django admin.
4. Remote User authentication
If already setup (see
[configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER)),
you can authenticate against the API using Remote User auth.
## Searching for documents
Full text searching is available on the `/api/documents/` endpoint. Two
@@ -274,6 +280,7 @@ The endpoint supports the following optional form fields:
- `correspondent`: Specify the ID of a correspondent that the consumer
should use for the document.
- `document_type`: Similar to correspondent.
- `storage_path`: Similar to correspondent.
- `tags`: Similar to correspondent. Specify this multiple times to
have multiple tags added to the document.
- `archive_serial_number`: An optional archive serial number to set.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -1,5 +1,167 @@
# Changelog
## paperless-ngx 2.3.3
### Enhancements
- Enhancement: Explain behavior of unset app config boolean to user [@shamoon](https://github.com/shamoon) ([#5345](https://github.com/paperless-ngx/paperless-ngx/pull/5345))
- Enhancement: title assignment placeholder error handling, fallback [@shamoon](https://github.com/shamoon) ([#5282](https://github.com/paperless-ngx/paperless-ngx/pull/5282))
### Bug Fixes
- Fix: Don't require the JSON user arguments field, interpret empty string as [@stumpylog](https://github.com/stumpylog) ([#5320](https://github.com/paperless-ngx/paperless-ngx/pull/5320))
### Maintenance
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#5336](https://github.com/paperless-ngx/paperless-ngx/pull/5336))
- Chore: add pre-commit hook for codespell [@shamoon](https://github.com/shamoon) ([#5324](https://github.com/paperless-ngx/paperless-ngx/pull/5324))
### All App Changes
<details>
<summary>5 changes</summary>
- Enhancement: Explain behavior of unset app config boolean to user [@shamoon](https://github.com/shamoon) ([#5345](https://github.com/paperless-ngx/paperless-ngx/pull/5345))
- Enhancement: title assignment placeholder error handling, fallback [@shamoon](https://github.com/shamoon) ([#5282](https://github.com/paperless-ngx/paperless-ngx/pull/5282))
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#5336](https://github.com/paperless-ngx/paperless-ngx/pull/5336))
- Fix: Don't require the JSON user arguments field, interpret empty string as [@stumpylog](https://github.com/stumpylog) ([#5320](https://github.com/paperless-ngx/paperless-ngx/pull/5320))
- Chore: add pre-commit hook for codespell [@shamoon](https://github.com/shamoon) ([#5324](https://github.com/paperless-ngx/paperless-ngx/pull/5324))
</details>
## paperless-ngx 2.3.2
### Bug Fixes
- Fix: triggered workflow assignment of customfield fails if field exists in v2.3.1 [@shamoon](https://github.com/shamoon) ([#5302](https://github.com/paperless-ngx/paperless-ngx/pull/5302))
- Fix: Decoding of user arguments for OCR [@stumpylog](https://github.com/stumpylog) ([#5307](https://github.com/paperless-ngx/paperless-ngx/pull/5307))
- Fix: empty workflow trigger match field cannot be saved in v.2.3.1 [@shamoon](https://github.com/shamoon) ([#5301](https://github.com/paperless-ngx/paperless-ngx/pull/5301))
- Fix: Use local time for added/updated workflow triggers [@stumpylog](https://github.com/stumpylog) ([#5304](https://github.com/paperless-ngx/paperless-ngx/pull/5304))
- Fix: workflow edit form loses unsaved changes [@shamoon](https://github.com/shamoon) ([#5299](https://github.com/paperless-ngx/paperless-ngx/pull/5299))
### All App Changes
<details>
<summary>5 changes</summary>
- Fix: triggered workflow assignment of customfield fails if field exists in v2.3.1 [@shamoon](https://github.com/shamoon) ([#5302](https://github.com/paperless-ngx/paperless-ngx/pull/5302))
- Fix: Decoding of user arguments for OCR [@stumpylog](https://github.com/stumpylog) ([#5307](https://github.com/paperless-ngx/paperless-ngx/pull/5307))
- Fix: empty workflow trigger match field cannot be saved in v.2.3.1 [@shamoon](https://github.com/shamoon) ([#5301](https://github.com/paperless-ngx/paperless-ngx/pull/5301))
- Fix: Use local time for added/updated workflow triggers [@stumpylog](https://github.com/stumpylog) ([#5304](https://github.com/paperless-ngx/paperless-ngx/pull/5304))
- Fix: workflow edit form loses unsaved changes [@shamoon](https://github.com/shamoon) ([#5299](https://github.com/paperless-ngx/paperless-ngx/pull/5299))
</details>
## paperless-ngx 2.3.1
### Bug Fixes
- Fix: edit workflow form not displaying trigger settings [@shamoon](https://github.com/shamoon) ([#5276](https://github.com/paperless-ngx/paperless-ngx/pull/5276))
- Fix: Prevent passing 0 pages to OCRMyPDF [@stumpylog](https://github.com/stumpylog) ([#5275](https://github.com/paperless-ngx/paperless-ngx/pull/5275))
### All App Changes
<details>
<summary>2 changes</summary>
- Fix: edit workflow form not displaying trigger settings [@shamoon](https://github.com/shamoon) ([#5276](https://github.com/paperless-ngx/paperless-ngx/pull/5276))
- Fix: Prevent passing 0 pages to OCRMyPDF [@stumpylog](https://github.com/stumpylog) ([#5275](https://github.com/paperless-ngx/paperless-ngx/pull/5275))
</details>
## paperless-ngx 2.3.0
### Notable Changes
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
### Features
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
- Enhancement: fetch mails in bulk [@falkenbt](https://github.com/falkenbt) ([#5249](https://github.com/paperless-ngx/paperless-ngx/pull/5249))
- Enhancement: add parameter to post_document API [@bevanjkay](https://github.com/bevanjkay) ([#5217](https://github.com/paperless-ngx/paperless-ngx/pull/5217))
### Bug Fixes
- Chore: Replaces deprecated Django alias with standard library [@stumpylog](https://github.com/stumpylog) ([#5262](https://github.com/paperless-ngx/paperless-ngx/pull/5262))
- Fix: Crash in barcode ASN reading when the file type isn't supported [@stumpylog](https://github.com/stumpylog) ([#5261](https://github.com/paperless-ngx/paperless-ngx/pull/5261))
- Fix: Allows pre-consume scripts to modify the working path again [@stumpylog](https://github.com/stumpylog) ([#5260](https://github.com/paperless-ngx/paperless-ngx/pull/5260))
- Change: Use fnmatch for more sane workflow path matching [@shamoon](https://github.com/shamoon) ([#5250](https://github.com/paperless-ngx/paperless-ngx/pull/5250))
- Fix: zip exports not respecting the --delete option [@stumpylog](https://github.com/stumpylog) ([#5245](https://github.com/paperless-ngx/paperless-ngx/pull/5245))
- Fix: correctly format tip admonition [@ChrisRBe](https://github.com/ChrisRBe) ([#5229](https://github.com/paperless-ngx/paperless-ngx/pull/5229))
- Fix: filename format remove none when part of directory [@shamoon](https://github.com/shamoon) ([#5210](https://github.com/paperless-ngx/paperless-ngx/pull/5210))
- Fix: Improve Performance for Listing and Paginating Documents [@antoinelibert](https://github.com/antoinelibert) ([#5195](https://github.com/paperless-ngx/paperless-ngx/pull/5195))
- Fix: Disable custom field remove button if user does not have permissions [@shamoon](https://github.com/shamoon) ([#5194](https://github.com/paperless-ngx/paperless-ngx/pull/5194))
- Fix: overlapping button focus highlight on login [@shamoon](https://github.com/shamoon) ([#5193](https://github.com/paperless-ngx/paperless-ngx/pull/5193))
- Fix: symmetric doc links with target doc value None [@shamoon](https://github.com/shamoon) ([#5187](https://github.com/paperless-ngx/paperless-ngx/pull/5187))
- Fix: setting empty doc link with docs to be removed [@shamoon](https://github.com/shamoon) ([#5174](https://github.com/paperless-ngx/paperless-ngx/pull/5174))
- Enhancement: improve validation of custom field values [@shamoon](https://github.com/shamoon) ([#5166](https://github.com/paperless-ngx/paperless-ngx/pull/5166))
- Fix: type casting of db values for 'shared by me' filter [@shamoon](https://github.com/shamoon) ([#5155](https://github.com/paperless-ngx/paperless-ngx/pull/5155))
### Documentation
- Fix: correctly format tip admonition [@ChrisRBe](https://github.com/ChrisRBe) ([#5229](https://github.com/paperless-ngx/paperless-ngx/pull/5229))
### Maintenance
- Chore(deps): Bump the actions group with 5 updates [@dependabot](https://github.com/dependabot) ([#5203](https://github.com/paperless-ngx/paperless-ngx/pull/5203))
### Dependencies
<details>
<summary>4 changes</summary>
- Chore(deps): Bump the actions group with 5 updates [@dependabot](https://github.com/dependabot) ([#5203](https://github.com/paperless-ngx/paperless-ngx/pull/5203))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#5204](https://github.com/paperless-ngx/paperless-ngx/pull/5204))
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot) ([#5207](https://github.com/paperless-ngx/paperless-ngx/pull/5207))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5205](https://github.com/paperless-ngx/paperless-ngx/pull/5205))
</details>
### All App Changes
<details>
<summary>21 changes</summary>
- Chore: Replaces deprecated Django alias with standard library [@stumpylog](https://github.com/stumpylog) ([#5262](https://github.com/paperless-ngx/paperless-ngx/pull/5262))
- Fix: Crash in barcode ASN reading when the file type isn't supported [@stumpylog](https://github.com/stumpylog) ([#5261](https://github.com/paperless-ngx/paperless-ngx/pull/5261))
- Fix: Allows pre-consume scripts to modify the working path again [@stumpylog](https://github.com/stumpylog) ([#5260](https://github.com/paperless-ngx/paperless-ngx/pull/5260))
- Enhancement: add basic filters for listing of custom fields [@shamoon](https://github.com/shamoon) ([#5257](https://github.com/paperless-ngx/paperless-ngx/pull/5257))
- Change: Use fnmatch for more sane workflow path matching [@shamoon](https://github.com/shamoon) ([#5250](https://github.com/paperless-ngx/paperless-ngx/pull/5250))
- Enhancement: fetch mails in bulk [@falkenbt](https://github.com/falkenbt) ([#5249](https://github.com/paperless-ngx/paperless-ngx/pull/5249))
- Fix: zip exports not respecting the --delete option [@stumpylog](https://github.com/stumpylog) ([#5245](https://github.com/paperless-ngx/paperless-ngx/pull/5245))
- Enhancement: add parameter to post_document API [@bevanjkay](https://github.com/bevanjkay) ([#5217](https://github.com/paperless-ngx/paperless-ngx/pull/5217))
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
- Fix: filename format remove none when part of directory [@shamoon](https://github.com/shamoon) ([#5210](https://github.com/paperless-ngx/paperless-ngx/pull/5210))
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#5204](https://github.com/paperless-ngx/paperless-ngx/pull/5204))
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot) ([#5207](https://github.com/paperless-ngx/paperless-ngx/pull/5207))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5205](https://github.com/paperless-ngx/paperless-ngx/pull/5205))
- Fix: Improve Performance for Listing and Paginating Documents [@antoinelibert](https://github.com/antoinelibert) ([#5195](https://github.com/paperless-ngx/paperless-ngx/pull/5195))
- Fix: Disable custom field remove button if user does not have permissions [@shamoon](https://github.com/shamoon) ([#5194](https://github.com/paperless-ngx/paperless-ngx/pull/5194))
- Fix: overlapping button focus highlight on login [@shamoon](https://github.com/shamoon) ([#5193](https://github.com/paperless-ngx/paperless-ngx/pull/5193))
- Fix: symmetric doc links with target doc value None [@shamoon](https://github.com/shamoon) ([#5187](https://github.com/paperless-ngx/paperless-ngx/pull/5187))
- Fix: setting empty doc link with docs to be removed [@shamoon](https://github.com/shamoon) ([#5174](https://github.com/paperless-ngx/paperless-ngx/pull/5174))
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
- Enhancement: improve validation of custom field values [@shamoon](https://github.com/shamoon) ([#5166](https://github.com/paperless-ngx/paperless-ngx/pull/5166))
- Fix: type casting of db values for 'shared by me' filter [@shamoon](https://github.com/shamoon) ([#5155](https://github.com/paperless-ngx/paperless-ngx/pull/5155))
</details>
## paperless-ngx 2.2.1
### Bug Fixes
- Fix: saving doc links with no value [@shamoon](https://github.com/shamoon) ([#5144](https://github.com/paperless-ngx/paperless-ngx/pull/5144))
- Fix: allow multiple consumption templates to assign the same custom field [@shamoon](https://github.com/shamoon) ([#5142](https://github.com/paperless-ngx/paperless-ngx/pull/5142))
- Fix: some dropdowns broken in 2.2.0 [@shamoon](https://github.com/shamoon) ([#5134](https://github.com/paperless-ngx/paperless-ngx/pull/5134))
### All App Changes
<details>
<summary>3 changes</summary>
- Fix: saving doc links with no value [@shamoon](https://github.com/shamoon) ([#5144](https://github.com/paperless-ngx/paperless-ngx/pull/5144))
- Fix: allow multiple consumption templates to assign the same custom field [@shamoon](https://github.com/shamoon) ([#5142](https://github.com/paperless-ngx/paperless-ngx/pull/5142))
- Fix: some dropdowns broken in 2.2.0 [@shamoon](https://github.com/shamoon) ([#5134](https://github.com/paperless-ngx/paperless-ngx/pull/5134))
</details>
## paperless-ngx 2.2.0
### Features
@@ -293,7 +455,7 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
- Enhancement: Allow the user the specify the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
@@ -503,7 +665,7 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
- Fix: completely hide upload widget if user does not have permissions [@nawramm](https://github.com/nawramm) ([#4198](https://github.com/paperless-ngx/paperless-ngx/pull/4198))
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
- Enhancement: Allow the user the specify the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
- Chore: change dark mode to use Bootstrap's color modes [@lkster](https://github.com/lkster) ([#4174](https://github.com/paperless-ngx/paperless-ngx/pull/4174))
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
@@ -535,11 +697,11 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
### Bug Fixes
- Fix: ghostscript rendering error doesnt trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
- Fix: ghostscript rendering error doesn't trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
### All App Changes
- Fix: ghostscript rendering error doesnt trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
- Fix: ghostscript rendering error doesn't trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
## paperless-ngx 1.17.3
@@ -1206,7 +1368,7 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
### Documentation
- Whitespace changes, making sure the example is correcly aligned [@denilsonsa](https://github.com/denilsonsa) ([#3089](https://github.com/paperless-ngx/paperless-ngx/pull/3089))
- Whitespace changes, making sure the example is correctly aligned [@denilsonsa](https://github.com/denilsonsa) ([#3089](https://github.com/paperless-ngx/paperless-ngx/pull/3089))
- Docs: Include additional information about barcodes [@stumpylog](https://github.com/stumpylog) ([#2889](https://github.com/paperless-ngx/paperless-ngx/pull/2889))
- Fix formatting in Setup documentation page [@igrybkov](https://github.com/igrybkov) ([#2880](https://github.com/paperless-ngx/paperless-ngx/pull/2880))
- [Documentation] Update docker-compose steps to support podman [@white-gecko](https://github.com/white-gecko) ([#2855](https://github.com/paperless-ngx/paperless-ngx/pull/2855))
@@ -1261,7 +1423,7 @@ Exports generated in Paperless-ngx v2.0.02.0.1 will **not** contain consumpti
- Fix: update PaperlessTask on hard failures [@shamoon](https://github.com/shamoon) ([#3062](https://github.com/paperless-ngx/paperless-ngx/pull/3062))
- Bump typescript from 4.8.4 to 4.9.5 in /src-ui [@dependabot](https://github.com/dependabot) ([#3071](https://github.com/paperless-ngx/paperless-ngx/pull/3071))
- Bulk Bump npm packages 04.23 [@dependabot](https://github.com/dependabot) ([#3068](https://github.com/paperless-ngx/paperless-ngx/pull/3068))
- Fix: Hide UI tour steps if user doesnt have permissions [@shamoon](https://github.com/shamoon) ([#3060](https://github.com/paperless-ngx/paperless-ngx/pull/3060))
- Fix: Hide UI tour steps if user doesn't have permissions [@shamoon](https://github.com/shamoon) ([#3060](https://github.com/paperless-ngx/paperless-ngx/pull/3060))
- Fix: Hide Permissions tab if user cannot view users [@shamoon](https://github.com/shamoon) ([#3061](https://github.com/paperless-ngx/paperless-ngx/pull/3061))
- v1.14.0 delete document fixes [@shamoon](https://github.com/shamoon) ([#3020](https://github.com/paperless-ngx/paperless-ngx/pull/3020))
- Bump wait-on from 6.0.1 to 7.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#2990](https://github.com/paperless-ngx/paperless-ngx/pull/2990))
@@ -1466,7 +1628,7 @@ older comments. The Docker image will automatically perform this reindex, bare m
- [Docs] Add Paperless Mobile app to docs [@astubenbord](https://github.com/astubenbord) ([#2378](https://github.com/paperless-ngx/paperless-ngx/pull/2378))
- Tiny spelling change [@veverkap](https://github.com/veverkap) ([#2369](https://github.com/paperless-ngx/paperless-ngx/pull/2369))
- Documentation: update build instructions to remove deprecated [@shamoon](https://github.com/shamoon) ([#2334](https://github.com/paperless-ngx/paperless-ngx/pull/2334))
- [Documentation] Add note that PAPERLESS_URL cant contain a path [@shamoon](https://github.com/shamoon) ([#2319](https://github.com/paperless-ngx/paperless-ngx/pull/2319))
- [Documentation] Add note that PAPERLESS_URL can't contain a path [@shamoon](https://github.com/shamoon) ([#2319](https://github.com/paperless-ngx/paperless-ngx/pull/2319))
- [Documentation] Add v1.11.3 changelog [@github-actions](https://github.com/github-actions) ([#2311](https://github.com/paperless-ngx/paperless-ngx/pull/2311))
### Maintenance
@@ -1797,7 +1959,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
### All App Changes
- Add info that re-do OCR doesnt automatically refresh content [@shamoon](https://github.com/shamoon) ([#2025](https://github.com/paperless-ngx/paperless-ngx/pull/2025))
- Add info that re-do OCR doesn't automatically refresh content [@shamoon](https://github.com/shamoon) ([#2025](https://github.com/paperless-ngx/paperless-ngx/pull/2025))
- Bugfix: Fix created_date being a string [@stumpylog](https://github.com/stumpylog) ([#2023](https://github.com/paperless-ngx/paperless-ngx/pull/2023))
- Bugfix: Fixes an issue with mixed text and images when redoing OCR [@stumpylog](https://github.com/stumpylog) ([#2017](https://github.com/paperless-ngx/paperless-ngx/pull/2017))
- Bugfix: Don't allow exceptions during date parsing to fail consume [@stumpylog](https://github.com/stumpylog) ([#1998](https://github.com/paperless-ngx/paperless-ngx/pull/1998))
@@ -2208,7 +2370,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
- Fix local Docker image building [\@stumpylog](https://github.com/stumpylog) ([\#849](https://github.com/paperless-ngx/paperless-ngx/pull/849))
- Fix: show errors on invalid date input [\@shamoon](https://github.com/shamoon) ([\#862](https://github.com/paperless-ngx/paperless-ngx/pull/862))
- Fix: Older dates do not display on frontend [\@shamoon](https://github.com/shamoon) ([\#852](https://github.com/paperless-ngx/paperless-ngx/pull/852))
- Fixes IMAP UTF8 Authenication [\@stumpylog](https://github.com/stumpylog) ([\#725](https://github.com/paperless-ngx/paperless-ngx/pull/725))
- Fixes IMAP UTF8 Authentication [\@stumpylog](https://github.com/stumpylog) ([\#725](https://github.com/paperless-ngx/paperless-ngx/pull/725))
- Fix password field remains visible [\@shamoon](https://github.com/shamoon) ([\#840](https://github.com/paperless-ngx/paperless-ngx/pull/840))
- Fixes Pillow build for armv7 [\@stumpylog](https://github.com/stumpylog) ([\#815](https://github.com/paperless-ngx/paperless-ngx/pull/815))
- Update frontend localization source file [\@shamoon](https://github.com/shamoon) ([\#814](https://github.com/paperless-ngx/paperless-ngx/pull/814))
@@ -2329,7 +2491,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
[\@shamoon](https://github.com/shamoon) ([\#313](https://github.com/paperless-ngx/paperless-ngx/pull/313))
- Fix imap tools bug [\@stumpylog](https://github.com/stumpylog)
([\#393](https://github.com/paperless-ngx/paperless-ngx/pull/393))
- Fix filterable dropdown buttons arent translated
- Fix filterable dropdown buttons aren't translated
[\@shamoon](https://github.com/shamoon) ([\#366](https://github.com/paperless-ngx/paperless-ngx/pull/366))
- Fix 224: "Auto-detected date is day before receipt date"
[\@a17t](https://github.com/a17t) ([\#246](https://github.com/paperless-ngx/paperless-ngx/pull/246))
@@ -3165,7 +3327,7 @@ primarily.
[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF) to perform OCR
on documents. It still uses tesseract under the hood, but the
PDF parser of Paperless has changed considerably and will behave
different for some douments.
different for some documents.
- OCRmyPDF creates archived PDF/A documents with embedded text
that can be selected in the front end.
- Paperless stores archived versions of documents alongside with
@@ -3216,7 +3378,7 @@ primarily.
crash.
- Mail handling no longer exits entirely when encountering errors.
It will skip the account/rule/message on which the error
occured.
occurred.
- Assigning correspondents from mail sender names failed for very
long names. Paperless no longer assigns correspondents in these
cases.

View File

@@ -3,6 +3,11 @@
Paperless provides a wide range of customizations. Depending on how you
run paperless, these settings have to be defined in different places.
Certain configuration options may be set via the UI. This currently includes
common [OCR](#ocr) related settings and some frontend settings. If set, these will take
preference over the settings via environment variables. If not set, the environment setting
or applicable default will be utilized instead.
- If you run paperless on docker, `paperless.conf` is not used.
Rather, configure paperless by copying necessary options to
`docker-compose.env`.
@@ -660,11 +665,13 @@ completely.
Specifying 1 here will only use the first page.
The value must be greater than or equal to 1 to be used.
When combined with `PAPERLESS_OCR_MODE=redo` or
`PAPERLESS_OCR_MODE=force`, paperless will not modify any text it
finds on excluded pages and copy it verbatim.
Defaults to 0, which disables this feature and always uses all
Defaults to unset, which disables this feature and always uses all
pages.
#### [`PAPERLESS_OCR_IMAGE_DPI=<num>`](#PAPERLESS_OCR_IMAGE_DPI) {#PAPERLESS_OCR_IMAGE_DPI}
@@ -678,7 +685,7 @@ fails, it uses this value as a fallback.
Set this to the DPI your scanner produces images at.
Default is none, which will automatically calculate image DPI so
Defaults to unset, which will automatically calculate image DPI so
that the produced PDF documents are A4 sized.
#### [`PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num>`](#PAPERLESS_OCR_MAX_IMAGE_PIXELS) {#PAPERLESS_OCR_MAX_IMAGE_PIXELS}
@@ -1310,6 +1317,10 @@ specified as "chi-tra".
Defaults to none, which does not install any additional languages.
!!! warning
This option must not be used in rootless containers.
#### [`PAPERLESS_ENABLE_FLOWER=<defined>`](#PAPERLESS_ENABLE_FLOWER) {#PAPERLESS_ENABLE_FLOWER}
: If this environment variable is defined, the Celery monitoring tool
@@ -1318,7 +1329,15 @@ started by the container.
You can read more about this in the [advanced documentation](advanced_usage.md#celery-monitoring).
## Update Checking {#update-checking}
## Frontend Settings
#### [`PAPERLESS_APP_TITLE=<bool>`](#PAPERLESS_APP_TITLE) {#PAPERLESS_APP_TITLE}
: If set, overrides the default name "Paperless-ngx"
#### [`PAPERLESS_APP_LOGO=<path>`](#PAPERLESS_APP_LOGO) {#PAPERLESS_APP_LOGO}
: Path to an image file in the /media/logo directory, must include 'logo', e.g. `/logo/Atari_logo.svg`
#### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK}

View File

@@ -18,6 +18,7 @@ physical documents into a searchable online archive so you can keep, well, _less
## Features
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
- _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way.
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
@@ -41,7 +42,7 @@ physical documents into a searchable online archive so you can keep, well, _less
- Configure multiple accounts and rules for each account.
- After processing, paperless can perform actions on the messages such as marking as read, deleting and more.
- A built-in robust **multi-user permissions** system that supports 'global' permissions as well as per document or object.
- A powerful templating system that gives you more control over the consumption pipeline.
- A powerful workflow system that gives you even more control.
- **Optimized** for multi core systems: Paperless-ngx consumes multiple documents in parallel.
- The integrated sanity checker makes sure that your document archive is in good health.
@@ -156,9 +157,9 @@ Tag, correspondent, document type and storage path editing.
</div>
<div class="grid-half-right" markdown>
Consumption templates provide finer control over the document pipeline.
Workflows provide finer control over the document pipeline and trigger actions.
![image](assets/screenshots/consumption_template.png)
![image](assets/screenshots/workflow.png)
</div>
<div class="clear"></div>

View File

@@ -28,7 +28,8 @@ steps described in [Docker setup](#docker_hub) automatically.
1. Make sure that Docker and Docker Compose are installed.
!!! tip
See the Docker installation instructions at https://docs.docker.com/engine/install/
See the Docker installation instructions at https://docs.docker.com/engine/install/
2. Download and run the installation script:
@@ -72,7 +73,7 @@ steps described in [Docker setup](#docker_hub) automatically.
If you want to use the included `docker-compose.*.yml` file, you
need to have at least Docker version **17.09.0** and Docker Compose
version **v2**. To check do: `docker compose -v` or `docker -v`
version **v2**. To check do: `docker compose version` or `docker -v`
See the [Docker installation guide](https://docs.docker.com/engine/install/) on how to install the current
version of Docker for your operating system or Linux distribution of
@@ -95,7 +96,7 @@ steps described in [Docker setup](#docker_hub) automatically.
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
```
Don't change the part after the colon or paperless wont find your
Don't change the part after the colon or paperless won't find your
documents.
You may also need to change the default port that the webserver will
@@ -120,6 +121,10 @@ steps described in [Docker setup](#docker_hub) automatically.
**Rootless**
!!! warning
It is currently not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`.
If you want to run Paperless as a rootless container, you will need
to do the following in your `docker-compose.yml`:

View File

@@ -138,7 +138,7 @@ command:
You might encounter errors such as:
```shell-session
The following error occured while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
The following error occurred while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
```
This happens when paperless does not have permission to delete files

View File

@@ -149,7 +149,7 @@ different means. These are as follows:
- **Flag:** Sets the 'important' flag on mails with consumed
documents. Paperless will not consume flagged mails.
- **Move to folder:** Moves consumed mails out of the way so that
paperless wont consume them again.
paperless won't consume them again.
- **Add custom Tag:** Adds a custom tag to mails with consumed
documents (the IMAP standard calls these "keywords"). Paperless
will not consume mails already tagged. Not all mail servers support
@@ -238,7 +238,7 @@ do not have an owner set.
### Default permissions
Default permissions for documents can be set using consumption templates.
Default permissions for documents can be set using workflows.
For objects created via the web UI (tags, doc types, etc.) the default is to set the current user
as owner and no extra permissions, but you explicitly set these under Settings > Permissions.
@@ -255,29 +255,80 @@ permissions can be granted to limit access to certain parts of the UI (and corre
In order to enable the password reset feature you will need to setup an SMTP backend, see
[`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST)
## Consumption templates
## Workflows
Consumption templates were introduced in v2.0 and allow for finer control over what metadata (tags, doc
types) and permissions (owner, privileges) are assigned to documents during consumption. In general,
templates are applied sequentially (by sort order) but subsequent templates will never override an
assignment from a preceding template. The same is true for mail rules, e.g. if you set the correspondent
in a mail rule any subsequent consumption templates that are applied _will not_ overwrite this. The
exception to this is assignments that can be multiple e.g. tags and permissions, which will be merged.
!!! note
Consumption templates allow you to filter by:
v2.3 added "Workflows" and existing "Consumption Templates" were converted automatically to the new more powerful format.
Workflows allow hooking into the Paperless-ngx document pipeline, for example to alter what metadata (tags, doc types) and
permissions (owner, privileges) are assigned to documents. Workflows can have multiple 'triggers' and 'actions'. Triggers
are events (with optional filtering rules) that will cause the workflow to be run and actions are the set of sequential
actions to apply.
In general, workflows and any actions they contain are applied sequentially by sort order. For "assignment" actions, subsequent
workflow actions will override previous assignments, except for assignments that accept multiple items e.g. tags, custom
fields and permissions, which will be merged.
### Workflow Triggers
Currently, there are three events that correspond to workflow trigger 'types':
1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption
folder or API), file path, file name, mail rule
2. **Document Added**: _after_ a document is added. At this time, file path and source information is no longer available,
but the document content has been extracted and metadata such as document type, tags, etc. have been set, so these can now
be used for filtering.
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
tags, doc type, or correspondent.
The following flow diagram illustrates the three trigger types:
```mermaid
flowchart TD
consumption{"Matching
'Consumption'
trigger(s)"}
added{"Matching
'Added'
trigger(s)"}
updated{"Matching
'Updated'
trigger(s)"}
A[New Document] --> consumption
consumption --> |Yes| C[Workflow Actions Run]
consumption --> |No| D
C --> D[Document Added]
D -- Paperless-ngx 'matching' of tags, etc. --> added
added --> |Yes| F[Workflow Actions Run]
added --> |No| G
F --> G[Document Finalized]
H[Existing Document Changed] --> updated
updated --> |Yes| J[Workflow Actions Run]
updated --> |No| K
J --> K[Document Saved]
```
#### Filters {#workflow-trigger-filters}
Workflows allow you to filter by:
- Source, e.g. documents uploaded via consume folder, API (& the web UI) and mail fetch
- File name, including wildcards e.g. \*.pdf will apply to all pdfs
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
example, automatically assigning documents to different owners based on the upload directory.
- Mail rule. Choosing this option will force 'mail fetch' to be the template source.
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
- Content matching (`Added` and `Updated` triggers only). Filter document content using the matching settings.
- Tags (`Added` and `Updated` triggers only). Filter for documents with any of the specified tags
- Document type (`Added` and `Updated` triggers only). Filter documents with this doc type
- Correspondent (`Added` and `Updated` triggers only). Filter documents with this correspondent
!!! note
### Workflow Actions
You must include a file name filter, a path filter or a mail rule filter. Use * for either to apply
to all files.
Consumption templates can assign:
There is currently one type of workflow action, "Assignment", which can assign:
- Title, see [title placeholders](usage.md#title-placeholders) below
- Tags, correspondent, document types
@@ -285,21 +336,11 @@ Consumption templates can assign:
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
### Consumption template permissions
#### Title placeholders
All users who have application permissions for editing consumption templates can see the same set
of templates. In other words, templates themselves intentionally do not have an owner or permissions.
Given their potentially far-reaching capabilities, you may want to restrict access to templates.
Upon migration, existing installs will grant access to consumption templates to users who can add
documents (and superusers who can always access all parts of the app).
### Title placeholders
Consumption template titles can include placeholders, _only for items that are assigned within the template_.
This is because at the time of consumption (when the title is to be set), no automatic tags etc. have been
applied. You can use the following placeholders:
Workflow titles can include placeholders but the available options differ depending on the type of
workflow trigger. This is because at the time of consumption (when the title is to be set), no automatic tags etc. have been
applied. You can use the following placeholders with any trigger type:
- `{correspondent}`: assigned correspondent name
- `{document_type}`: assigned document type name
@@ -314,6 +355,27 @@ applied. You can use the following placeholders:
- `{added_time}`: added time in HH:MM format
- `{original_filename}`: original file name without extension
The following placeholders are only available for "added" or "updated" triggers
- `{created}`: created datetime
- `{created_year}`: created year
- `{created_year_short}`: created year
- `{created_month}`: created month
- `{created_month_name}`: created month name
- `{created_month_name_short}`: created month short name
- `{created_day}`: created day
- `{created_time}`: created time in HH:MM format
### Workflow permissions
All users who have application permissions for editing workflows can see the same set
of workflows. In other words, workflows themselves intentionally do not have an owner or permissions.
Given their potentially far-reaching capabilities, you may want to restrict access to workflows.
Upon migration, existing installs will grant access to workflows to users who can add
documents (and superusers who can always access all parts of the app).
## Custom Fields {#custom-fields}
Paperless-ngx supports the use of custom fields for documents as of v2.0, allowing a user
@@ -349,7 +411,7 @@ The following custom field types are supported:
## Share Links
Paperless-ngx added the abiltiy to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
Paperless-ngx added the ability to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
- Share links do not require a user to login and thus link directly to a file.
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.

View File

@@ -44,6 +44,11 @@ markdown_extensions:
- pymdownx.inlinehilite
- pymdownx.snippets
- footnotes
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
strict: true
nav:
- index.md

View File

@@ -131,7 +131,7 @@ test('sorting', async ({ page }) => {
await page.getByRole('button', { name: 'Notes' }).click()
await expect(page).toHaveURL(/sort=num_notes/)
await page.getByRole('button', { name: 'Sort' }).click()
await page.locator('.w-100 > label > .toolbaricon').first().click()
await page.locator('.w-100 > label > i-bs').first().click()
await expect(page).not.toHaveURL(/reverse=1/)
})

File diff suppressed because it is too large Load Diff

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

@@ -10,14 +10,14 @@
"hasInstallScript": true,
"dependencies": {
"@angular/cdk": "^17.0.4",
"@angular/common": "~17.0.7",
"@angular/compiler": "~17.0.7",
"@angular/core": "~17.0.7",
"@angular/forms": "~17.0.7",
"@angular/localize": "~17.0.7",
"@angular/platform-browser": "~17.0.7",
"@angular/platform-browser-dynamic": "~17.0.7",
"@angular/router": "~17.0.7",
"@angular/common": "~17.0.8",
"@angular/compiler": "~17.0.8",
"@angular/core": "~17.0.8",
"@angular/forms": "~17.0.8",
"@angular/localize": "~17.0.8",
"@angular/platform-browser": "~17.0.8",
"@angular/platform-browser-dynamic": "~17.0.8",
"@angular/router": "~17.0.8",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ng-select/ng-select": "^12.0.4",
"@ngneat/dirty-check-forms": "^3.0.3",
@@ -25,6 +25,7 @@
"bootstrap": "^5.3.2",
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^17.0.1",
"ngx-file-drop": "^16.0.0",
@@ -37,21 +38,21 @@
},
"devDependencies": {
"@angular-builders/jest": "17.0.0",
"@angular-devkit/build-angular": "~17.0.7",
"@angular-devkit/build-angular": "~17.0.8",
"@angular-eslint/builder": "17.1.1",
"@angular-eslint/eslint-plugin": "17.1.1",
"@angular-eslint/eslint-plugin-template": "17.1.1",
"@angular-eslint/schematics": "17.1.1",
"@angular-eslint/template-parser": "17.1.1",
"@angular/cli": "~17.0.7",
"@angular/cli": "~17.0.8",
"@angular/compiler-cli": "~17.0.7",
"@playwright/test": "^1.40.1",
"@types/jest": "^29.5.10",
"@types/node": "^20.10.2",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@types/node": "^20.10.6",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"concurrently": "^8.2.2",
"eslint": "^8.53.0",
"eslint": "^8.56.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^13.1.4",
@@ -107,12 +108,12 @@
}
},
"node_modules/@angular-devkit/architect": {
"version": "0.1700.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.7.tgz",
"integrity": "sha512-32uitQKsYLGXAKoXBsmOnPsTt9pS+b9cnFI9ZvBFVhJ31I2EOM7vGcMFalhTxdB/DkVHk4TyO78efV0V26DwCA==",
"version": "0.1700.8",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.8.tgz",
"integrity": "sha512-SWVr3CvwO6T0yW2ytszCwBT1g92vyFkwbVUxqE93urYnoD8PvP+81GH5YwVjHQTgvhP4eXQMGZ9hpHx57VOrWQ==",
"dev": true,
"dependencies": {
"@angular-devkit/core": "17.0.7",
"@angular-devkit/core": "17.0.8",
"rxjs": "7.8.1"
},
"engines": {
@@ -122,15 +123,15 @@
}
},
"node_modules/@angular-devkit/build-angular": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.7.tgz",
"integrity": "sha512-AtEzLk6n6BXqQzk0Bsupe6GV0IgUe7RbpBfqROi+NZqMA7OUAHCX3xA6M68Qu+5KxBtW7T5lHeZZ7iP/y39wtQ==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.8.tgz",
"integrity": "sha512-u7R5yX92ZxOL/LfxiKGGqlBo86100sJ5Rabavn8DeGtYP8N0qgwCcNwlW2zaMoUlkw2geMnxcxIX5VJI4iFPUA==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "2.2.1",
"@angular-devkit/architect": "0.1700.7",
"@angular-devkit/build-webpack": "0.1700.7",
"@angular-devkit/core": "17.0.7",
"@angular-devkit/architect": "0.1700.8",
"@angular-devkit/build-webpack": "0.1700.8",
"@angular-devkit/core": "17.0.8",
"@babel/core": "7.23.2",
"@babel/generator": "7.23.0",
"@babel/helper-annotate-as-pure": "7.22.5",
@@ -141,7 +142,7 @@
"@babel/preset-env": "7.23.2",
"@babel/runtime": "7.23.2",
"@discoveryjs/json-ext": "0.5.7",
"@ngtools/webpack": "17.0.7",
"@ngtools/webpack": "17.0.8",
"@vitejs/plugin-basic-ssl": "1.0.1",
"ansi-colors": "4.1.3",
"autoprefixer": "10.4.16",
@@ -245,12 +246,12 @@
}
},
"node_modules/@angular-devkit/build-webpack": {
"version": "0.1700.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.7.tgz",
"integrity": "sha512-B9Mg/qYDpE5my8PJ3VPQyRSUV0Oq1bFUzU8s0ZpqEZl1URKc04pm0LtLmebrMIcUZgDiGk0RHaD+O1E9IV/bdQ==",
"version": "0.1700.8",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.8.tgz",
"integrity": "sha512-GA7QlCAlYB3uBkRaUYgIC/Vfajb9jMmouwYiAAEm34ZyP3ThFjdqsYd/A/exnuESt5o6Bh++C/PI34sV3lawRA==",
"dev": true,
"dependencies": {
"@angular-devkit/architect": "0.1700.7",
"@angular-devkit/architect": "0.1700.8",
"rxjs": "7.8.1"
},
"engines": {
@@ -264,9 +265,9 @@
}
},
"node_modules/@angular-devkit/core": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.7.tgz",
"integrity": "sha512-vATobHo5O5tJba424hJfQWLb40GzvZPNsI74dcgSUTgrDph8ksmk5xB9OvEvf0INorQZ2IMphj/VIWj4/+JqSA==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.8.tgz",
"integrity": "sha512-gI8+SOwGUwr0WOlFrhLjohLolMzcguuoR0LTZEcGjdXvQyPgH4NDSRIIrfWCdu+ZVhfy76o3zQYdYc9QN8NrjQ==",
"dev": true,
"dependencies": {
"ajv": "8.12.0",
@@ -291,12 +292,12 @@
}
},
"node_modules/@angular-devkit/schematics": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.7.tgz",
"integrity": "sha512-BY11OkJkM3xyXcvyD7x5kGY/c8Ufd4AfPvI0D9imhVxbns45Q48b1DlvCQvSnCJ/s+OwnkrYb/Efa70ZiaGu8A==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.8.tgz",
"integrity": "sha512-syo814SVWfJvne448IijjZvpWbuqJsEutdNqHWLTewTfX2U3KrIAr/XRVcXQMuyMvLCDiuxjMgEJxOIP7mcIPw==",
"dev": true,
"dependencies": {
"@angular-devkit/core": "17.0.7",
"@angular-devkit/core": "17.0.8",
"jsonc-parser": "3.2.0",
"magic-string": "0.30.5",
"ora": "5.4.1",
@@ -423,15 +424,15 @@
}
},
"node_modules/@angular/cli": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.7.tgz",
"integrity": "sha512-oSa0GVAQNA7wFbLJYeaO3kV4iUcbKEqXDLxcIE8s1GfHddBOlXH2P1T4fXonCBl5qvV+joP0G0+fs7I0w2utZQ==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.8.tgz",
"integrity": "sha512-yZXYNLAFv9u2qypsVqtS+rRCsnjsIPYXr6TcI/r5buzOtC7UQ2lleYsWJqX47SsyGMk/o3gaYg5Bj2I5mmRDLA==",
"dev": true,
"dependencies": {
"@angular-devkit/architect": "0.1700.7",
"@angular-devkit/core": "17.0.7",
"@angular-devkit/schematics": "17.0.7",
"@schematics/angular": "17.0.7",
"@angular-devkit/architect": "0.1700.8",
"@angular-devkit/core": "17.0.8",
"@angular-devkit/schematics": "17.0.8",
"@schematics/angular": "17.0.8",
"@yarnpkg/lockfile": "1.1.0",
"ansi-colors": "4.1.3",
"ini": "4.1.1",
@@ -457,9 +458,9 @@
}
},
"node_modules/@angular/common": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.7.tgz",
"integrity": "sha512-bPPL6x0KOAOTxKSE2j4EWmEUOnqZYzOYiHzroa5b9UEyA9NvGkd9bm3zIxw8xcndRj1Ehcmvpi6KBLcYBBbWfg==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.8.tgz",
"integrity": "sha512-fFfwtdg7H+OkqnvV/ENu8F8KGfgIiH16DDbQqYY5KQyyQB+SMsoVW29F1fGx6Y30s7ZlsLOy6cHhgrw74itkSw==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -467,14 +468,14 @@
"node": "^18.13.0 || >=20.9.0"
},
"peerDependencies": {
"@angular/core": "17.0.7",
"@angular/core": "17.0.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.7.tgz",
"integrity": "sha512-QHPuLti2c2tGZmOGZ0cfCHo4LxiHUkC27I0aZFDyQSSQqEI5obQGVlEREHysw0nsS3sYIcLvqcwcKcRtXlXtxQ==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.8.tgz",
"integrity": "sha512-48jWypuhBGTrUUbkz1vB9gjbKKZ3hpuJ2DUUncd331Yw4tqkqZQbBa/E3ei4IHiCxEvW2uX3lI4AwlhuozmUtA==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -482,7 +483,7 @@
"node": "^18.13.0 || >=20.9.0"
},
"peerDependencies": {
"@angular/core": "17.0.7"
"@angular/core": "17.0.8"
},
"peerDependenciesMeta": {
"@angular/core": {
@@ -491,9 +492,9 @@
}
},
"node_modules/@angular/compiler-cli": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.7.tgz",
"integrity": "sha512-YnL38idjIYtl3BXYpv+sVJKWGbUjHT6eyQSQVAfO/1AwWqVa21K9hnE+Q37VmUKEcKFMnQembeuErA+KVsGI6A==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.8.tgz",
"integrity": "sha512-ny2SMVgl+icjMuU5ZM57yFGUrhjR0hNxfCn0otAD3jUFliz/Onu9l6EPRKA5Cr8MZx3mg3rTLSBMD17YT8rsOg==",
"dependencies": {
"@babel/core": "7.23.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -513,14 +514,14 @@
"node": "^18.13.0 || >=20.9.0"
},
"peerDependencies": {
"@angular/compiler": "17.0.7",
"@angular/compiler": "17.0.8",
"typescript": ">=5.2 <5.3"
}
},
"node_modules/@angular/core": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.7.tgz",
"integrity": "sha512-mEkelXkzEi6+A9GjdKOSGGzQAfo1iAjVTn6YsplNUeGE5JgDZYZ7sXGQqs0Lin7dzJxnPAgGjCOl7SpWLXIPSQ==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.8.tgz",
"integrity": "sha512-tzYsK24LdkNuKNJK6efF4XOqspvF/qOe9j/n1Y61a6mNvFwsJFGbcmdZMby4hI/YRm6oIDoIIFjSep8ycp6Pbw==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -533,9 +534,9 @@
}
},
"node_modules/@angular/forms": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.7.tgz",
"integrity": "sha512-28BxRxEmgZIofGwVp6s2v3ri/kuWW+/EY/ZXhavlWKJEh4ATJl72k0RkRWNcQi4wnvn0Qb8tFdnVJnvRZvvKEw==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.8.tgz",
"integrity": "sha512-WZBHbMQjaSovAzOMhKqZN+m7eUPGfOzh9rKFKvj6UQLIJ9qSpEpqlvL0omU1z/47s3XXeLiBzomMiRfQISJvvw==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -543,16 +544,16 @@
"node": "^18.13.0 || >=20.9.0"
},
"peerDependencies": {
"@angular/common": "17.0.7",
"@angular/core": "17.0.7",
"@angular/platform-browser": "17.0.7",
"@angular/common": "17.0.8",
"@angular/core": "17.0.8",
"@angular/platform-browser": "17.0.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/localize": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.0.7.tgz",
"integrity": "sha512-avYYQ8zin2thzvsH2YP3WxlwkvOzjNEXxjv4yyZBx6wul68e/753kQK/0RmSUYaBpDTUEZYzrPpDay00TKwBOA==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.0.8.tgz",
"integrity": "sha512-1zW8qWKNMH3r/x4KpwzzUmVY+iN76vYdhjA6gzZDnpJxpon9eyljNEildj9+zSWeNUr2LgJ6HnkIX9q1f3mXfA==",
"dependencies": {
"@babel/core": "7.23.2",
"fast-glob": "3.3.1",
@@ -567,14 +568,14 @@
"node": "^18.13.0 || >=20.9.0"
},
"peerDependencies": {
"@angular/compiler": "17.0.7",
"@angular/compiler-cli": "17.0.7"
"@angular/compiler": "17.0.8",
"@angular/compiler-cli": "17.0.8"
}
},
"node_modules/@angular/platform-browser": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.7.tgz",
"integrity": "sha512-bm9/wt51nc/MPjft/FlRNIgFSeLjDtfJOT7M32Rt6kOHhNKSK7ZTPWdMe9ahuHSbAhLzd0G/4NsT5sKrWSeVZg==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.8.tgz",
"integrity": "sha512-XaI+p2AxQaIHzR761lhPUf4OcOp46WDW0IfbvOzaezHE+8r81joZyVSDQPgXSa/aRfI58YhcfUavuGqyU3PphA==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -582,9 +583,9 @@
"node": "^18.13.0 || >=20.9.0"
},
"peerDependencies": {
"@angular/animations": "17.0.7",
"@angular/common": "17.0.7",
"@angular/core": "17.0.7"
"@angular/animations": "17.0.8",
"@angular/common": "17.0.8",
"@angular/core": "17.0.8"
},
"peerDependenciesMeta": {
"@angular/animations": {
@@ -593,9 +594,9 @@
}
},
"node_modules/@angular/platform-browser-dynamic": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.7.tgz",
"integrity": "sha512-OquwUX9fLWA2JUZW5Jm6atk0CPt0sA7Tg24eGLsr6g1XfTS7jRZprlGaa72NgPLnQVV6m84o/ZiNYS6yPmq1Gg==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.8.tgz",
"integrity": "sha512-BIXNKnfBZb8sdluQ7WIhIXFuVnsJJ0SV+aiMKzQ7B6XhWoAXZQnlvON2thydjIIVuCvaF3YmWTbILI2K8YZ2jQ==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -603,16 +604,16 @@
"node": "^18.13.0 || >=20.9.0"
},
"peerDependencies": {
"@angular/common": "17.0.7",
"@angular/compiler": "17.0.7",
"@angular/core": "17.0.7",
"@angular/platform-browser": "17.0.7"
"@angular/common": "17.0.8",
"@angular/compiler": "17.0.8",
"@angular/core": "17.0.8",
"@angular/platform-browser": "17.0.8"
}
},
"node_modules/@angular/router": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.7.tgz",
"integrity": "sha512-rUFPe1uDlYYw6+3Gq68czW7WxBH7zT/D3UsT1otqwUV4RnQQsVze4fIit9FqJh7tuP4y3WpB4XBNf7p7Oi6TJw==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.8.tgz",
"integrity": "sha512-ptphcRe1RG/mIS60R7ZPilkkrxautqB0sOhds3h5VP3g628G1a2HWzvnmvjEfpJWDMFivV32VJMMBtTLqGr+0Q==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -620,9 +621,9 @@
"node": "^18.13.0 || >=20.9.0"
},
"peerDependencies": {
"@angular/common": "17.0.7",
"@angular/core": "17.0.7",
"@angular/platform-browser": "17.0.7",
"@angular/common": "17.0.8",
"@angular/core": "17.0.8",
"@angular/platform-browser": "17.0.8",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@@ -2895,9 +2896,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -3941,9 +3942,9 @@
}
},
"node_modules/@ngtools/webpack": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.7.tgz",
"integrity": "sha512-gwhUhpwXn0trwwKdSu9WlJbEcLt+s/2fPwoD9lZ0y3wXfrOogsfcNBJKeO5BZf1h+A3AWt7ePmgrZXSJM+865Q==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.8.tgz",
"integrity": "sha512-wx0XBMrbpDeailK2uIhp/ZVMC3GK3BWwJjUu5SbT4BFrcoi2Zd9/9m0RCBAY54UXLBCqKd+ih7pJ6JSvprZmWw==",
"dev": true,
"engines": {
"node": "^18.13.0 || >=20.9.0",
@@ -4459,13 +4460,13 @@
}
},
"node_modules/@schematics/angular": {
"version": "17.0.7",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.7.tgz",
"integrity": "sha512-d7QKmcKrM4owb/2bR7Ipf23roiNbvbD/x7reNhQAtKAPLSHJ3Ulkf1+Yv+dj+9f+K7y9SBviEUSrD27BQ9WaxQ==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.8.tgz",
"integrity": "sha512-1h5mwKFv1B/L5JWZ0mxnC4ms06iwnSi/w+GgRZPeM3P5BpuZuvAkFiClNnM55iLlQJXRQioPNLM3sOsz7spR6w==",
"dev": true,
"dependencies": {
"@angular-devkit/core": "17.0.7",
"@angular-devkit/schematics": "17.0.7",
"@angular-devkit/core": "17.0.8",
"@angular-devkit/schematics": "17.0.8",
"jsonc-parser": "3.2.0"
},
"engines": {
@@ -4878,9 +4879,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.10.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
"version": "20.10.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
"integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -4896,9 +4897,9 @@
}
},
"node_modules/@types/qs": {
"version": "6.9.10",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
"integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==",
"version": "6.9.11",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
"integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==",
"dev": true
},
"node_modules/@types/range-parser": {
@@ -4995,16 +4996,16 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.14.0.tgz",
"integrity": "sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==",
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz",
"integrity": "sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.14.0",
"@typescript-eslint/type-utils": "6.14.0",
"@typescript-eslint/utils": "6.14.0",
"@typescript-eslint/visitor-keys": "6.14.0",
"@typescript-eslint/scope-manager": "6.17.0",
"@typescript-eslint/type-utils": "6.17.0",
"@typescript-eslint/utils": "6.17.0",
"@typescript-eslint/visitor-keys": "6.17.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -5030,13 +5031,13 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz",
"integrity": "sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==",
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.17.0.tgz",
"integrity": "sha512-hDXcWmnbtn4P2B37ka3nil3yi3VCQO2QEB9gBiHJmQp5wmyQWqnjA85+ZcE8c4FqnaB6lBwMrPkgd4aBYz3iNg==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "6.14.0",
"@typescript-eslint/utils": "6.14.0",
"@typescript-eslint/typescript-estree": "6.17.0",
"@typescript-eslint/utils": "6.17.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -5057,17 +5058,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.14.0.tgz",
"integrity": "sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==",
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.17.0.tgz",
"integrity": "sha512-LofsSPjN/ITNkzV47hxas2JCsNCEnGhVvocfyOcLzT9c/tSZE7SfhS/iWtzP1lKNOEfLhRTZz6xqI8N2RzweSQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.14.0",
"@typescript-eslint/types": "6.14.0",
"@typescript-eslint/typescript-estree": "6.14.0",
"@typescript-eslint/scope-manager": "6.17.0",
"@typescript-eslint/types": "6.17.0",
"@typescript-eslint/typescript-estree": "6.17.0",
"semver": "^7.5.4"
},
"engines": {
@@ -5082,15 +5083,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz",
"integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==",
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.17.0.tgz",
"integrity": "sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.14.0",
"@typescript-eslint/types": "6.14.0",
"@typescript-eslint/typescript-estree": "6.14.0",
"@typescript-eslint/visitor-keys": "6.14.0",
"@typescript-eslint/scope-manager": "6.17.0",
"@typescript-eslint/types": "6.17.0",
"@typescript-eslint/typescript-estree": "6.17.0",
"@typescript-eslint/visitor-keys": "6.17.0",
"debug": "^4.3.4"
},
"engines": {
@@ -5110,13 +5111,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz",
"integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==",
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz",
"integrity": "sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.14.0",
"@typescript-eslint/visitor-keys": "6.14.0"
"@typescript-eslint/types": "6.17.0",
"@typescript-eslint/visitor-keys": "6.17.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -5211,9 +5212,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz",
"integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==",
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.17.0.tgz",
"integrity": "sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -5224,16 +5225,17 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz",
"integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==",
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz",
"integrity": "sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.14.0",
"@typescript-eslint/visitor-keys": "6.14.0",
"@typescript-eslint/types": "6.17.0",
"@typescript-eslint/visitor-keys": "6.17.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"minimatch": "9.0.3",
"semver": "^7.5.4",
"ts-api-utils": "^1.0.1"
},
@@ -5250,6 +5252,30 @@
}
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "6.13.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
@@ -5350,12 +5376,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz",
"integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==",
"version": "6.17.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz",
"integrity": "sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.14.0",
"@typescript-eslint/types": "6.17.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -8687,15 +8713,15 @@
}
},
"node_modules/eslint": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.55.0",
"@eslint/js": "8.56.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -13825,6 +13851,22 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"node_modules/ngx-bootstrap-icons": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/ngx-bootstrap-icons/-/ngx-bootstrap-icons-1.9.3.tgz",
"integrity": "sha512-UsFqJ/cn0u5W39hVMIDbm+ze1dCF9fDV839scqeimi70Efcmg41zOx6GgR6i2gWAVFR0OBso1cdqb4E75XhTSw==",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": ">= 16.18.1",
"npm": ">= 8.11.0"
},
"peerDependencies": {
"@angular/common": ">= 13.3.8",
"@angular/core": ">= 13.3.8"
}
},
"node_modules/ngx-color": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-9.0.0.tgz",

View File

@@ -12,14 +12,14 @@
"private": true,
"dependencies": {
"@angular/cdk": "^17.0.4",
"@angular/common": "~17.0.7",
"@angular/compiler": "~17.0.7",
"@angular/core": "~17.0.7",
"@angular/forms": "~17.0.7",
"@angular/localize": "~17.0.7",
"@angular/platform-browser": "~17.0.7",
"@angular/platform-browser-dynamic": "~17.0.7",
"@angular/router": "~17.0.7",
"@angular/common": "~17.0.8",
"@angular/compiler": "~17.0.8",
"@angular/core": "~17.0.8",
"@angular/forms": "~17.0.8",
"@angular/localize": "~17.0.8",
"@angular/platform-browser": "~17.0.8",
"@angular/platform-browser-dynamic": "~17.0.8",
"@angular/router": "~17.0.8",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ng-select/ng-select": "^12.0.4",
"@ngneat/dirty-check-forms": "^3.0.3",
@@ -27,6 +27,7 @@
"bootstrap": "^5.3.2",
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^17.0.1",
"ngx-file-drop": "^16.0.0",
@@ -39,21 +40,21 @@
},
"devDependencies": {
"@angular-builders/jest": "17.0.0",
"@angular-devkit/build-angular": "~17.0.7",
"@angular-devkit/build-angular": "~17.0.8",
"@angular-eslint/builder": "17.1.1",
"@angular-eslint/eslint-plugin": "17.1.1",
"@angular-eslint/eslint-plugin-template": "17.1.1",
"@angular-eslint/schematics": "17.1.1",
"@angular-eslint/template-parser": "17.1.1",
"@angular/cli": "~17.0.7",
"@angular/cli": "~17.0.8",
"@angular/compiler-cli": "~17.0.7",
"@playwright/test": "^1.40.1",
"@types/jest": "^29.5.10",
"@types/node": "^20.10.2",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@types/node": "^20.10.6",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"concurrently": "^8.2.2",
"eslint": "^8.53.0",
"eslint": "^8.56.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^13.1.4",

View File

@@ -21,10 +21,11 @@ import {
PermissionAction,
PermissionType,
} from './services/permissions.service'
import { ConsumptionTemplatesComponent } from './components/manage/consumption-templates/consumption-templates.component'
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
import { MailComponent } from './components/manage/mail/mail.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { ConfigComponent } from './components/admin/config/config.component'
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
@@ -179,6 +180,17 @@ export const routes: Routes = [
},
},
},
{
path: 'config',
component: ConfigComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.Admin,
},
},
},
{
path: 'tasks',
component: TasksComponent,
@@ -202,13 +214,13 @@ export const routes: Routes = [
},
},
{
path: 'templates',
component: ConsumptionTemplatesComponent,
path: 'workflows',
component: WorkflowsComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.ConsumptionTemplate,
type: PermissionType.Workflow,
},
},
},

View File

@@ -176,9 +176,9 @@ export class AppComponent implements OnInit, OnDestroy {
},
},
{
anchorId: 'tour.consumption-templates',
content: $localize`Consumption templates give you finer control over the document ingestion process.`,
route: '/templates',
anchorId: 'tour.workflows',
content: $localize`Workflows give you more control over the document pipeline.`,
route: '/workflows',
backdropConfig: {
offset: 0,
},

View File

@@ -95,8 +95,8 @@ import { UsernamePipe } from './pipes/username.pipe'
import { LogoComponent } from './components/common/logo/logo.component'
import { IsNumberPipe } from './pipes/is-number.pipe'
import { ShareLinksDropdownComponent } from './components/common/share-links-dropdown/share-links-dropdown.component'
import { ConsumptionTemplatesComponent } from './components/manage/consumption-templates/consumption-templates.component'
import { ConsumptionTemplateEditDialogComponent } from './components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component'
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
import { WorkflowEditDialogComponent } from './components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
import { MailComponent } from './components/manage/mail/mail.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { DragDropModule } from '@angular/cdk/drag-drop'
@@ -108,6 +108,177 @@ import { ProfileEditDialogComponent } from './components/common/profile-edit-dia
import { PdfViewerComponent } from './components/common/pdf-viewer/pdf-viewer.component'
import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
import { SwitchComponent } from './components/common/input/switch/switch.component'
import { ConfigComponent } from './components/admin/config/config.component'
import { FileComponent } from './components/common/input/file/file.component'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import {
archive,
arrowCounterclockwise,
arrowDown,
arrowLeft,
arrowRepeat,
arrowRight,
arrowRightShort,
arrowUpRight,
asterisk,
boxArrowUp,
boxArrowUpRight,
boxes,
calendar,
calendarEvent,
caretDown,
caretUp,
chatLeftText,
check,
check2All,
checkAll,
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
clipboard,
clipboardCheckFill,
clipboardFill,
dash,
diagram3,
dice5,
doorOpen,
download,
envelope,
exclamationTriangle,
eye,
fileEarmark,
fileEarmarkCheck,
fileEarmarkFill,
fileEarmarkLock,
files,
fileText,
filter,
folder,
folderFill,
funnel,
gear,
grid,
gripVertical,
hash,
hddStack,
house,
infoCircle,
link,
listTask,
listUl,
pencil,
people,
peopleFill,
person,
personCircle,
personFill,
personFillLock,
personLock,
plus,
plusCircle,
questionCircle,
search,
slashCircle,
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
tagFill,
tags,
textIndentLeft,
textLeft,
threeDots,
threeDotsVertical,
trash,
uiRadios,
upcScan,
x,
xLg,
} from 'ngx-bootstrap-icons'
const icons = {
archive,
arrowCounterclockwise,
arrowDown,
arrowLeft,
arrowRepeat,
arrowRight,
arrowRightShort,
arrowUpRight,
asterisk,
boxArrowUp,
boxArrowUpRight,
boxes,
calendar,
calendarEvent,
caretDown,
caretUp,
chatLeftText,
check,
check2All,
checkAll,
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
clipboard,
clipboardCheckFill,
clipboardFill,
dash,
diagram3,
dice5,
doorOpen,
download,
envelope,
exclamationTriangle,
eye,
fileEarmark,
fileEarmarkCheck,
fileEarmarkFill,
fileEarmarkLock,
files,
fileText,
filter,
folder,
folderFill,
funnel,
gear,
grid,
gripVertical,
hash,
hddStack,
house,
infoCircle,
link,
listTask,
listUl,
pencil,
people,
peopleFill,
person,
personCircle,
personFill,
personFillLock,
personLock,
plus,
plusCircle,
questionCircle,
search,
slashCircle,
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
tagFill,
tags,
textIndentLeft,
textLeft,
threeDots,
threeDotsVertical,
trash,
uiRadios,
upcScan,
x,
xLg,
}
import localeAf from '@angular/common/locales/af'
import localeAr from '@angular/common/locales/ar'
@@ -251,8 +422,8 @@ function initializeApp(settings: SettingsService) {
LogoComponent,
IsNumberPipe,
ShareLinksDropdownComponent,
ConsumptionTemplatesComponent,
ConsumptionTemplateEditDialogComponent,
WorkflowsComponent,
WorkflowEditDialogComponent,
MailComponent,
UsersAndGroupsComponent,
FileDropComponent,
@@ -263,6 +434,9 @@ function initializeApp(settings: SettingsService) {
PdfViewerComponent,
DocumentLinkComponent,
PreviewPopupComponent,
SwitchComponent,
ConfigComponent,
FileComponent,
],
imports: [
BrowserModule,
@@ -276,6 +450,7 @@ function initializeApp(settings: SettingsService) {
ColorSliderModule,
TourNgBootstrapModule,
DragDropModule,
NgxBootstrapIconsModule.pick(icons),
],
providers: [
{

View File

@@ -0,0 +1,59 @@
<pngx-page-header
title="Application Configuration"
i18n-title
info="Global app configuration options which apply to <strong>every</strong> user of this install of Paperless-ngx. Options can also be set using environment variables or the configuration file but the value here will always take precedence."
i18n-info
infoLink="configuration">
</pngx-page-header>
<form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4">
<ul ngbNav #nav="ngbNav" class="nav-tabs">
@for (category of optionCategories; track category) {
<li [ngbNavItem]="category">
<a ngbNavLink i18n>{{category}}</a>
<ng-template ngbNavContent>
<div class="p-3">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
@for (option of getCategoryOptions(category); track option.key) {
<div class="col">
<div class="card bg-light">
<div class="card-body">
<div class="card-title">
<h6>
{{option.title}}
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
<i-bs name="info-circle"></i-bs>
</a>
</h6>
</div>
<div class="mb-n3">
@switch (option.type) {
@case (ConfigOptionType.Select) { <pngx-input-select [formControlName]="option.key" [error]="errors[option.key]" [items]="option.choices" [allowNull]="true"></pngx-input-select> }
@case (ConfigOptionType.Number) { <pngx-input-number [formControlName]="option.key" [error]="errors[option.key]" [showAdd]="false"></pngx-input-number> }
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [showUnsetNote]="true" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
}
</div>
</div>
</div>
</div>
}
</div>
</div>
</ng-template>
</li>
}
</ul>
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group me-2">
<button type="button" (click)="discardChanges()" class="btn btn-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,143 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ConfigComponent } from './config.component'
import { ConfigService } from 'src/app/services/config.service'
import { ToastService } from 'src/app/services/toast.service'
import { of, throwError } from 'rxjs'
import { OutputTypeConfig } from 'src/app/data/paperless-config'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { BrowserModule } from '@angular/platform-browser'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { TextComponent } from '../../common/input/text/text.component'
import { NumberComponent } from '../../common/input/number/number.component'
import { SwitchComponent } from '../../common/input/switch/switch.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { SelectComponent } from '../../common/input/select/select.component'
import { FileComponent } from '../../common/input/file/file.component'
import { SettingsService } from 'src/app/services/settings.service'
describe('ConfigComponent', () => {
let component: ConfigComponent
let fixture: ComponentFixture<ConfigComponent>
let configService: ConfigService
let toastService: ToastService
let settingService: SettingsService
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
ConfigComponent,
TextComponent,
SelectComponent,
NumberComponent,
SwitchComponent,
FileComponent,
PageHeaderComponent,
],
imports: [
HttpClientTestingModule,
BrowserModule,
NgbModule,
NgSelectModule,
FormsModule,
ReactiveFormsModule,
],
}).compileComponents()
configService = TestBed.inject(ConfigService)
toastService = TestBed.inject(ToastService)
settingService = TestBed.inject(SettingsService)
fixture = TestBed.createComponent(ConfigComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should load config on init, show error if necessary', () => {
const getSpy = jest.spyOn(configService, 'getConfig')
const errorSpy = jest.spyOn(toastService, 'showError')
getSpy.mockReturnValueOnce(
throwError(() => new Error('Error getting config'))
)
component.ngOnInit()
expect(getSpy).toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
getSpy.mockReturnValueOnce(
of({ output_type: OutputTypeConfig.PDF_A } as any)
)
component.ngOnInit()
expect(component.initialConfig).toEqual({
output_type: OutputTypeConfig.PDF_A,
})
})
it('should save config, show error if necessary', () => {
const saveSpy = jest.spyOn(configService, 'saveConfig')
const errorSpy = jest.spyOn(toastService, 'showError')
saveSpy.mockReturnValueOnce(
throwError(() => new Error('Error saving config'))
)
component.saveConfig()
expect(saveSpy).toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
saveSpy.mockReturnValueOnce(
of({ output_type: OutputTypeConfig.PDF_A } as any)
)
component.saveConfig()
expect(component.initialConfig).toEqual({
output_type: OutputTypeConfig.PDF_A,
})
})
it('should support discard changes', () => {
component.initialConfig = { output_type: OutputTypeConfig.PDF_A2 } as any
component.configForm.patchValue({ output_type: OutputTypeConfig.PDF_A })
component.discardChanges()
expect(component.configForm.get('output_type').value).toEqual(
OutputTypeConfig.PDF_A2
)
})
it('should support JSON validation for e.g. user_args', () => {
component.configForm.patchValue({ user_args: '{ foo bar }' })
expect(component.errors).toEqual({ user_args: 'Invalid JSON' })
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
expect(component.errors).toEqual({ user_args: null })
})
it('should upload file, show error if necessary', () => {
const uploadSpy = jest.spyOn(configService, 'uploadFile')
const errorSpy = jest.spyOn(toastService, 'showError')
uploadSpy.mockReturnValueOnce(
throwError(() => new Error('Error uploading file'))
)
component.uploadFile(new File([], 'test.png'), 'app_logo')
expect(uploadSpy).toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
uploadSpy.mockReturnValueOnce(
of({ app_logo: 'https://example.com/logo/test.png' } as any)
)
component.uploadFile(new File([], 'test.png'), 'app_logo')
expect(component.initialConfig).toEqual({
app_logo: 'https://example.com/logo/test.png',
})
})
it('should refresh ui settings after save or upload', () => {
const saveSpy = jest.spyOn(configService, 'saveConfig')
const initSpy = jest.spyOn(settingService, 'initializeSettings')
saveSpy.mockReturnValueOnce(
of({ output_type: OutputTypeConfig.PDF_A } as any)
)
component.saveConfig()
expect(initSpy).toHaveBeenCalled()
const uploadSpy = jest.spyOn(configService, 'uploadFile')
uploadSpy.mockReturnValueOnce(
of({ app_logo: 'https://example.com/logo/test.png' } as any)
)
component.uploadFile(new File([], 'test.png'), 'app_logo')
expect(initSpy).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,189 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'
import {
BehaviorSubject,
Observable,
Subject,
Subscription,
first,
takeUntil,
} from 'rxjs'
import {
PaperlessConfigOptions,
ConfigCategory,
ConfigOption,
ConfigOptionType,
PaperlessConfig,
} from 'src/app/data/paperless-config'
import { ConfigService } from 'src/app/services/config.service'
import { ToastService } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { SettingsService } from 'src/app/services/settings.service'
@Component({
selector: 'pngx-config',
templateUrl: './config.component.html',
styleUrl: './config.component.scss',
})
export class ConfigComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy, DirtyComponent
{
public readonly ConfigOptionType = ConfigOptionType
// generated dynamically
public configForm = new FormGroup({})
public errors = {}
get optionCategories(): string[] {
return Object.values(ConfigCategory)
}
getCategoryOptions(category: string): ConfigOption[] {
return PaperlessConfigOptions.filter((o) => o.category === category)
}
public loading: boolean = false
initialConfig: PaperlessConfig
store: BehaviorSubject<any>
storeSub: Subscription
isDirty$: Observable<boolean>
private unsubscribeNotifier: Subject<any> = new Subject()
constructor(
private configService: ConfigService,
private toastService: ToastService,
private settingsService: SettingsService
) {
super()
this.configForm.addControl('id', new FormControl())
PaperlessConfigOptions.forEach((option) => {
this.configForm.addControl(option.key, new FormControl())
})
}
ngOnInit(): void {
this.loading = true
this.configService
.getConfig()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (config) => {
this.loading = false
this.initialize(config)
},
error: (e) => {
this.loading = false
this.toastService.showError($localize`Error retrieving config`, e)
},
})
// validate JSON inputs
PaperlessConfigOptions.filter(
(o) => o.type === ConfigOptionType.JSON
).forEach((option) => {
this.configForm
.get(option.key)
.addValidators((control: AbstractControl) => {
if (!control.value || control.value.toString().length === 0)
return null
try {
JSON.parse(control.value)
} catch (e) {
return [
{
user_args: e,
},
]
}
return null
})
this.configForm.get(option.key).statusChanges.subscribe((status) => {
this.errors[option.key] =
status === 'INVALID' ? $localize`Invalid JSON` : null
})
this.configForm.get(option.key).updateValueAndValidity()
})
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
}
private initialize(config: PaperlessConfig) {
if (!this.store) {
this.store = new BehaviorSubject(config)
this.store
.asObservable()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((state) => {
this.configForm.patchValue(state, { emitEvent: false })
})
this.isDirty$ = dirtyCheck(this.configForm, this.store.asObservable())
}
this.configForm.patchValue(config)
this.initialConfig = config
}
getDocsUrl(key: string) {
return `https://docs.paperless-ngx.com/configuration/#${key}`
}
public saveConfig() {
this.loading = true
this.configService
.saveConfig(this.configForm.value as PaperlessConfig)
.pipe(takeUntil(this.unsubscribeNotifier), first())
.subscribe({
next: (config) => {
this.loading = false
this.initialize(config)
this.store.next(config)
this.settingsService.initializeSettings().subscribe()
this.toastService.showInfo($localize`Configuration updated`)
},
error: (e) => {
this.loading = false
this.toastService.showError(
$localize`An error occurred updating configuration`,
e
)
},
})
}
public discardChanges() {
this.configForm.reset(this.initialConfig)
}
public uploadFile(file: File, key: string) {
this.loading = true
this.configService
.uploadFile(file, this.configForm.value['id'], key)
.pipe(takeUntil(this.unsubscribeNotifier), first())
.subscribe({
next: (config) => {
this.loading = false
this.initialize(config)
this.store.next(config)
this.settingsService.initializeSettings().subscribe()
this.toastService.showInfo($localize`File successfully updated`)
},
error: (e) => {
this.loading = false
this.toastService.showError(
$localize`An error occurred uploading file`,
e
)
},
})
}
}

View File

@@ -1,6 +1,10 @@
<pngx-page-header title="Logs" i18n-title>
<div class="form-check form-switch" (click)="toggleAutoRefresh()">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
<pngx-page-header
title="Logs"
i18n-title
info="Review the log files for the application and for email checking."
i18n-info>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div>
</pngx-page-header>

View File

@@ -2,9 +2,9 @@ import {
Component,
ElementRef,
OnInit,
AfterViewChecked,
ViewChild,
OnDestroy,
ChangeDetectorRef,
} from '@angular/core'
import { Subject, takeUntil } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service'
@@ -14,8 +14,11 @@ import { LogService } from 'src/app/services/rest/log.service'
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'],
})
export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
constructor(private logService: LogService) {}
export class LogsComponent implements OnInit, OnDestroy {
constructor(
private logService: LogService,
private changedetectorRef: ChangeDetectorRef
) {}
public logs: string[] = []
@@ -47,10 +50,6 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
})
}
ngAfterViewChecked() {
this.scrollToBottom()
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
@@ -66,6 +65,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
next: (result) => {
this.logs = result
this.isLoading = false
this.scrollToBottom()
},
error: () => {
this.logs = []
@@ -89,6 +89,7 @@ export class LogsComponent implements OnInit, AfterViewChecked, OnDestroy {
}
scrollToBottom(): void {
this.changedetectorRef.detectChanges()
this.logContainer?.nativeElement.scroll({
top: this.logContainer.nativeElement.scrollHeight,
left: 0,

View File

@@ -1,10 +1,13 @@
<pngx-page-header title="Settings" i18n-title>
<pngx-page-header
title="Settings"
i18n-title
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
i18n-info
>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container>
<svg class="sidebaricon ms-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
</svg>
<i-bs name="arrow-up-right"></i-bs>
</a>
</pngx-page-header>
@@ -135,204 +138,202 @@
</div>
<div class="col-2">
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg><ng-container i18n>Reset</ng-container>
</button>
</div>
<i-bs width="1em" height="1em" name="x"></i-bs><ng-container i18n>Reset</ng-container>
</button>
</div>
</div>
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<p i18n>
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
Actual updating of the app must still be performed manually.
</p>
<p i18n>
<em>No tracking data is collected by the app in any way.</em>
</p>
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<p i18n>
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
Actual updating of the app must still be performed manually.
</p>
<p i18n>
<em>No tracking data is collected by the app in any way.</em>
</p>
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
</div>
</div>
<h4 class="mt-4" i18n>Bulk editing</h4>
<h4 class="mt-4" i18n>Bulk editing</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check>
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check>
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
</div>
</div>
<h4 class="mt-4" i18n>Notes</h4>
<h4 class="mt-4" i18n>Notes</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
</div>
</div>
</ng-template>
</li>
</ng-template>
</li>
<li [ngbNavItem]="SettingsNavIDs.Permissions">
<a ngbNavLink i18n>Permissions</a>
<ng-template ngbNavContent>
<li [ngbNavItem]="SettingsNavIDs.Permissions">
<a ngbNavLink i18n>Permissions</a>
<ng-template ngbNavContent>
<h4 i18n>Default Permissions</h4>
<h4 i18n>Default Permissions</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<p i18n>
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
</p>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<p i18n>
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI
</p>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default Owner</span>
</div>
<div class="col-md-5">
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default Owner</span>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default View Permissions</span>
</div>
<div class="col-md-5">
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
</ng-container>
</div>
<div class="col-md-5">
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default View Permissions</span>
</div>
<div class="col-md-5">
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group>
</ng-container>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<pngx-permissions-user type="view" formControlName="defaultPermsViewUsers"></pngx-permissions-user>
</ng-container>
</div>
</div>
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsViewGroups"></pngx-permissions-group>
</ng-container>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default Edit Permissions</span>
</div>
<div class="col-md-5">
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
</ng-container>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Default Edit Permissions</span>
</div>
<div class="col-md-5">
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Users:</span>
</div>
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
</ng-container>
</div>
</div>
<div class="row">
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<pngx-permissions-user type="view" formControlName="defaultPermsEditUsers"></pngx-permissions-user>
</ng-container>
</div>
</div>
</div>
</ng-template>
</li>
<li [ngbNavItem]="SettingsNavIDs.Notifications">
<a ngbNavLink i18n>Notifications</a>
<ng-template ngbNavContent>
<h4 i18n>Document processing</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
<div class="row">
<div class="col-3">
<span class="d-block pt-1" i18n>Groups:</span>
</div>
<div class="col">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Group }">
<pngx-permissions-group type="view" formControlName="defaultPermsEditGroups"></pngx-permissions-group>
</ng-container>
</div>
</div>
<div class="row">
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div>
</div>
</div>
</ng-template>
</li>
</ng-template>
</li>
<li [ngbNavItem]="SettingsNavIDs.Notifications">
<a ngbNavLink i18n>Notifications</a>
<ng-template ngbNavContent>
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
<a ngbNavLink i18n>Saved views</a>
<ng-template ngbNavContent>
<h4 i18n>Document processing</h4>
<h4 i18n>Settings</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show notifications when new documents are detected" formControlName="notificationsConsumerNewDocument"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing completes successfully" formControlName="notificationsConsumerSuccess"></pngx-input-check>
<pngx-input-check i18n-title title="Show notifications when document processing fails" formControlName="notificationsConsumerFailed"></pngx-input-check>
<pngx-input-check i18n-title title="Suppress notifications on dashboard" formControlName="notificationsConsumerSuppressOnDashboard" i18n-hint hint="This will suppress all messages about document processing status on the dashboard."></pngx-input-check>
</div>
</div>
</ng-template>
</li>
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
<a ngbNavLink i18n>Saved views</a>
<ng-template ngbNavContent>
<h4 i18n>Settings</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
</div>
</div>
<h4 i18n>Views</h4>
<div formGroupName="savedViews">
@for (view of savedViews; track view) {
<div [formGroupName]="view.id" class="row">
<div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<div class="mb-2 col">
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n>&nbsp;<span class="visually-hidden">Appears on</span></label>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div>
</div>
<div class="mb-2 col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
</div>
</div>
</div>
}
<h4 i18n>Views</h4>
<div formGroupName="savedViews">
@if (savedViews && savedViews.length === 0) {
<div i18n>No saved views defined.</div>
}
@for (view of savedViews; track view) {
<div [formGroupName]="view.id" class="row">
<div class="mb-3 col">
<label class="form-label" for="name_{{view.id}}" i18n>Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<div class="mb-2 col">
<label class="form-label" for="show_on_dashboard_{{view.id}}" i18n>&nbsp;<span class="visually-hidden">Appears on</span></label>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="form-check-label" for="show_on_dashboard_{{view.id}}" i18n>Show on dashboard</label>
</div>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
<label class="form-check-label" for="show_in_sidebar_{{view.id}}" i18n>Show in sidebar</label>
</div>
</div>
<div class="mb-2 col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
</div>
</div>
}
@if (!savedViews) {
<div>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</div>
}
@if (savedViews && savedViews.length === 0) {
<div i18n>No saved views defined.</div>
}
</div>
@if (!savedViews) {
<div>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</div>
}
</ng-template>
</li>
</ul>
</div>
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
</form>
<button type="submit" class="btn btn-primary mb-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
</form>

View File

@@ -363,7 +363,7 @@ export class SettingsComponent
}
ngOnDestroy() {
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didnt save
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didn't save
this.storeSub && this.storeSub.unsubscribe()
this.settings.organizingSidebarSavedViews = false
}

View File

@@ -1,155 +1,155 @@
<pngx-page-header title="File Tasks" i18n-title>
<pngx-page-header
title="File Tasks"
i18n-title
info="File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process."
i18n-info
>
<div class="btn-toolbar col col-md-auto align-items-center">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#check2-all"/>
</svg>&nbsp;<ng-container i18n>{{dismissButtonText}}</ng-container>
</button>
<div class="form-check form-switch mb-0" (click)="toggleAutoRefresh()">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div>
</div>
</pngx-page-header>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary me-4" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
<i-bs name="check2-all"></i-bs>&nbsp;{{dismissButtonText}}
</button>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch" (click)="toggleAutoRefresh()" [attr.checked]="autoRefreshInterval">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div>
</div>
</pngx-page-header>
@if (!tasksService.completedFileTasks && tasksService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
}
@if (!tasksService.completedFileTasks && tasksService.loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
}
<ng-template let-tasks="tasks" #tasksTemplate>
<table class="table table-striped align-middle border shadow-sm">
<thead>
<tr>
<th scope="col">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-tasks"></label>
</div>
</th>
<th scope="col" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
</td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<td class="d-none d-lg-table-cell">
@if (task.result?.length > 50) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span>
</div>
}
@if (task.result?.length <= 50) {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
}
<ng-template #resultPopover>
<pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
&hellip;
}</pre>
@if (task.result?.length > 300) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template>
</td>
}
<td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<svg fill="currentColor" class="" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
</button>
</td>
<td scope="row">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>&nbsp;<ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document) {
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>&nbsp;<ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
</div>
</td>
</tr>
<tr>
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
</td>
</tr>
}
</tbody>
</table>
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
@if (tasks.length > 0) {
<div class="pb-2 pb-sm-0" i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</div>
}
@if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
}
<ng-template let-tasks="tasks" #tasksTemplate>
<table class="table table-striped align-middle border shadow-sm">
<thead>
<tr>
<th scope="col">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-tasks"></label>
</div>
</ng-template>
</th>
<th scope="col" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
</td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<td class="d-none d-lg-table-cell">
@if (task.result?.length > 50) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span>
</div>
}
@if (task.result?.length <= 50) {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
}
<ng-template #resultPopover>
<pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
&hellip;
}</pre>
@if (task.result?.length > 300) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template>
</td>
}
<td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</button>
</td>
<td scope="row">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<i-bs name="check"></i-bs>&nbsp;<ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document) {
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
<i-bs name="file-text"></i-bs>&nbsp;<ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
</div>
</td>
</tr>
<tr>
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
</td>
</tr>
}
</tbody>
</table>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
<li ngbNavItem="failed">
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="completed">
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="started">
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
@if (tasks.length > 0) {
<div class="pb-2 pb-sm-0">
<ng-container i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</ng-container>
@if (selectedTasks.size > 0) {
<ng-container i18n>&nbsp;({{selectedTasks.size}} selected)</ng-container>
}
</div>
}
@if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
}
</div>
</ng-template>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
<li ngbNavItem="failed">
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.failedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="completed">
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.completedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="started">
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.startedFileTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:tasksService.queuedFileTasks}"></ng-container>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>

View File

@@ -1,14 +1,17 @@
<pngx-page-header title="Users & Groups" i18n-title>
<pngx-page-header
title="Users & Groups"
i18n-title
info="Create, delete and edit users and groups."
i18n-info
infoLink="usage/#users-and-groups"
>
</pngx-page-header>
@if (users) {
<h4 class="d-flex">
<ng-container i18n>Users</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
<svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg>
<ng-container i18n>Add User</ng-container>
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add User</ng-container>
</button>
</h4>
<ul class="list-group">
@@ -29,14 +32,10 @@
<div class="col">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
@@ -50,10 +49,7 @@
<h4 class="mt-4 d-flex">
<ng-container i18n>Groups</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
<svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg>
<ng-container i18n>Add Group</ng-container>
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Group</ng-container>
</button>
</h4>
@if (groups.length > 0) {
@@ -75,14 +71,10 @@
<div class="col">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>

View File

@@ -4,30 +4,36 @@
(click)="isMenuCollapsed = !isMenuCollapsed">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-auto col-md-3 col-lg-2'" routerLink="/dashboard"
<a class="navbar-brand d-flex col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
[ngClass]="{ 'slim': slimSidebarEnabled, 'd-flex col-auto col-md-3 col-lg-2' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
routerLink="/dashboard"
tourAnchor="tour.intro">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
<path
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
transform="translate(0 0)" />
</svg>
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
<div class="ms-2 d-inline-block" [class.visually-hidden]="slimSidebarEnabled">
@if (customAppTitle?.length) {
<div class="d-flex flex-column align-items-start">
<span class="title">{{customAppTitle}}</span>
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
</div>
} @else {
Paperless-ngx
}
</div>
</a>
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
<svg width="1em" height="1em" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#search" />
</svg>
<i-bs style="top: .25em;" width="1em" height="1em" name="search"></i-bs>
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
(selectItem)="itemSelected($event)" i18n-placeholder>
@if (!searchFieldEmpty) {
<button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
<svg fill="currentColor" class="buttonicon-sm me-1">
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg>
<i-bs width="1em" height="1em" name="x"></i-bs>
</button>
}
</form>
@@ -38,9 +44,7 @@
<span class="small me-2 d-none d-sm-inline">
{{this.settingsService.displayName}}
</span>
<svg width="1.3em" height="1.3em" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-circle" />
</svg>
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>
</button>
<div ngbDropdownMenu class="dropdown-menu-end shadow me-2" aria-labelledby="userDropdown">
<div class="d-sm-none">
@@ -48,27 +52,19 @@
<div class="dropdown-divider"></div>
</div>
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person" />
</svg><ng-container i18n>My Profile</ng-container>
<i-bs class="me-2" name="person"></i-bs>&nbsp;<ng-container i18n>My Profile</ng-container>
</button>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.UISettings }">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear" />
</svg><ng-container i18n>Settings</ng-container>
<i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container>
</a>
<a ngbDropdownItem class="nav-link" href="accounts/logout/" (click)="onLogout()">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#door-open" />
</svg><ng-container i18n>Logout</ng-container>
<a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()">
<i-bs class="me-2" name="door-open"></i-bs><ng-container i18n>Logout</ng-container>
</a>
<div class="dropdown-divider"></div>
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
href="https://docs.paperless-ngx.com">
<svg class="sidebaricon me-2" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
</svg><ng-container i18n>Documentation</ng-container>
<i-bs class="me-2" name="question-circle"></i-bs><ng-container i18n>Documentation</ng-container>
</a>
</div>
</li>
@@ -81,13 +77,11 @@
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
[ngbCollapse]="isMenuCollapsed">
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
<svg class="sidebaricon-sm" fill="currentColor">
@if (slimSidebarEnabled) {
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-right" />
} @else {
<use xlink:href="assets/bootstrap-icons.svg#chevron-double-left" />
}
</svg>
@if (slimSidebarEnabled) {
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
} @else {
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
}
</button>
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
<ul class="nav flex-column">
@@ -95,18 +89,14 @@
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#house" />
</svg><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
<i-bs class="me-1" name="house"></i-bs><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#files" />
</svg><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
<i-bs class="me-1" name="files"></i-bs><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
</a>
</li>
</ul>
@@ -128,15 +118,11 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
</svg><span>&nbsp;{{view.name}}</span>
<i-bs class="me-1" name="funnel"></i-bs><span>&nbsp;{{view.name}}</span>
</a>
@if (settingsService.organizingSidebarSavedViews) {
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
<svg class="sidebaricon text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical" />
</svg>
<i-bs name="grip-vertical"></i-bs>
</div>
}
</li>
@@ -157,13 +143,9 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text" />
</svg><span>&nbsp;{{d.title | documentTitle}}</span>
<i-bs class="me-1" name="file-text"></i-bs><span>&nbsp;{{d.title | documentTitle}}</span>
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
<svg fill="currentColor" class="toolbaricon">
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg>
<i-bs name="x"></i-bs>
</span>
</a>
</li>
@@ -173,9 +155,7 @@
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
<i-bs class="me-1" name="x"></i-bs><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
</a>
</li>
}
@@ -191,9 +171,7 @@
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person" />
</svg><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
<i-bs class="me-1" name="person"></i-bs><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
@@ -201,9 +179,7 @@
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#tags" />
</svg><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
<i-bs class="me-1" name="tags"></i-bs><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a>
</li>
<li class="nav-item"
@@ -211,38 +187,30 @@
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#hash" />
</svg><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
<i-bs class="me-1" name="hash"></i-bs><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#folder" />
</svg><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
<i-bs class="me-1" name="folder"></i-bs><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
</svg><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
<i-bs class="me-1" name="ui-radios"></i-bs><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a>
</li>
<li class="nav-item"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }"
tourAnchor="tour.consumption-templates">
<a class="nav-link" routerLink="templates" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Consumption templates" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Workflow }"
tourAnchor="tour.workflows">
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-ruled" />
</svg><span>&nbsp;<ng-container i18n>Templates</ng-container></span>
<i-bs class="me-1" name="boxes"></i-bs><span>&nbsp;<ng-container i18n>Workflows</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
@@ -250,9 +218,7 @@
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#envelope" />
</svg><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
<i-bs class="me-1" name="envelope"></i-bs><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
</a>
</li>
</ul>
@@ -266,18 +232,21 @@
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear" />
</svg><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
<i-bs class="me-1" name="gear"></i-bs><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="sliders2-vertical"></i-bs><span>&nbsp;<ng-container i18n>Configuration</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people" />
</svg><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
<i-bs class="me-1" name="people"></i-bs><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
</a>
</li>
<li class="nav-item"
@@ -286,23 +255,19 @@
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="list-task"></i-bs><span>&nbsp;<ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span>
}</span>
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span>
}
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#list-task" />
</svg><span>&nbsp;<ng-container i18n>File Tasks@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span>
}</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-left" />
</svg><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
<i-bs class="me-1" name="text-left"></i-bs><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
</a>
</li>
<li class="nav-item mt-2" tourAnchor="tour.outro">
@@ -310,9 +275,7 @@
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle" />
</svg><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span>
<i-bs class="d-flex" name="question-circle"></i-bs><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span>
</a>
</li>
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
@@ -351,10 +314,7 @@
href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;"
viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
@if (appRemoteVersion?.update_available) {
<ng-container i18n>Update available</ng-container>
}
@@ -364,10 +324,7 @@
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;"
viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</a>
}
</div>

View File

@@ -152,9 +152,9 @@ main {
font-weight: bold;
}
.sidebaricon {
margin-right: 4px;
color: inherit;
i-bs {
position: relative;
top: -1px;
}
}
@@ -186,11 +186,11 @@ main {
width: 1.8rem;
height: 100%;
svg {
i-bs {
opacity: 0.5;
}
&:hover svg {
&:hover i-bs {
opacity: 1;
}
}
@@ -205,7 +205,7 @@ main {
text-decoration: underline;
}
svg {
i-bs {
margin-bottom: 2px;
}
}
@@ -217,9 +217,16 @@ main {
*/
.navbar-brand {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 1rem;
.flex-column {
padding: 0.15rem 0;
}
.byline {
font-size: 0.5rem;
letter-spacing: 0.1rem;
}
}
@media screen and (min-width: 768px) {
@@ -241,7 +248,7 @@ main {
.navbar .dropdown-menu {
font-size: 0.875rem; // body size
a svg {
a i-bs {
opacity: 0.6;
}
}
@@ -252,7 +259,7 @@ main {
form {
position: relative;
> svg {
> i-bs {
position: absolute;
left: 0.6rem;
top: 0.5rem;
@@ -262,7 +269,7 @@ main {
&:focus-within {
form > svg {
form > i-bs {
display: none;
}
@@ -316,6 +323,6 @@ main {
cursor: move;
}
::ng-deep .navItemDrag .position-absolute svg {
::ng-deep .navItemDrag .position-absolute i-bs {
display: none;
}

View File

@@ -248,7 +248,7 @@ describe('AppFrameComponent', () => {
expect(toastSpy).toHaveBeenCalled()
})
it('should support collapsable menu', () => {
it('should support collapsible menu', () => {
const button: HTMLButtonElement = (
fixture.nativeElement as HTMLDivElement
).querySelector('button[data-toggle=collapse]')

View File

@@ -102,6 +102,10 @@ export class AppFrameComponent
}, 200) // slightly longer than css animation for slim sidebar
}
get customAppTitle(): string {
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
}
get slimSidebarEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
}

View File

@@ -1,15 +1,11 @@
@if (active) {
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
@if (!isNumbered && selected) {
<svg width="1em" height="1em" class="check m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#check-lg"/>
</svg>
<i-bs class="check" width="1em" height="1em" name="check-lg"></i-bs>
}
@if (isNumbered) {
<div class="number">{{number}}<span class="visually-hidden">selected</span></div>
}
<svg width=".9em" height="1em" class="x m-0 p-0 opacity-75" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#x-lg"/>
</svg>
<i-bs class="x" width=".9em" height="1em" name="x-lg"></i-bs>
</button>
}

View File

@@ -22,7 +22,7 @@ button:hover {
.x {
display: inline-block;
position: absolute;
top: 5px;
left: calc(50% - 4px);
top: .4em;
left: calc(50% - .4em);
}
}

View File

@@ -12,8 +12,8 @@
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
<span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span>
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
<span>

View File

@@ -37,6 +37,12 @@ export class ConfirmDialogComponent {
@Input()
alternativeBtnCaption
@Input()
cancelBtnClass = 'btn-outline-secondary'
@Input()
cancelBtnCaption = $localize`Cancel`
@Input()
buttonsEnabled = true

View File

@@ -1,8 +1,6 @@
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose()">
<button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#ui-radios" />
</svg>
<i-bs name="ui-radios"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Custom Fields</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
@@ -20,14 +18,10 @@
</pngx-input-select>
<div class="btn-toolbar" role="toolbar">
<button class="btn btn-sm btn-outline-secondary me-auto" type="button" (click)="createField()" [disabled]="!canCreateFields">
<svg fill="currentColor" class="buttonicon-sm me-1 mb-1">
<use xlink:href="assets/bootstrap-icons.svg#asterisk"/>
</svg><ng-container i18n>Create New Field</ng-container>
<i-bs width="1em" height="1em" name="asterisk"></i-bs>&nbsp;<ng-container i18n>Create New Field</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary me-1" type="button" (click)="addField(); fieldDropdown.close()" [disabled]="field === undefined">
<svg fill="currentColor" class="buttonicon me-1">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle"/>
</svg><ng-container i18n>Add</ng-container>
<i-bs width="1.2em" height="1.2em" name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add</ng-container>
</button>
</div>
</li>

View File

@@ -9,9 +9,7 @@
<button class="list-group-item small list-goup list-group-item-action d-flex p-2" role="menuitem" (click)="setRelativeDate(rd.id)">
<div class="selected-icon">
@if (relativeDate === rd.id) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
<i-bs width="1em" height="1em" name="check"></i-bs>
}
</div>
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
@@ -32,9 +30,7 @@
<div i18n>After</div>
@if (dateAfter) {
<a class="btn btn-link p-0 m-0" (click)="clearAfter()">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<i-bs width="1em" height="1em" name="x"></i-bs>
<small i18n>Clear</small>
</a>
}
@@ -44,9 +40,7 @@
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateAfter" ngbDatepicker #dateAfterPicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateAfterPicker.toggle()" type="button">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
<i-bs width="1em" height="1em" name="calendar"></i-bs>
</button>
</div>
@@ -57,9 +51,7 @@
<div i18n>Before</div>
@if (dateBefore) {
<a class="btn btn-link p-0 m-0" (click)="clearBefore()">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<i-bs width="1em" height="1em" name="x"></i-bs>
<small i18n>Clear</small>
</a>
}
@@ -69,9 +61,7 @@
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
maxlength="10" [(ngModel)]="dateBefore" ngbDatepicker #dateBeforePicker="ngbDatepicker">
<button class="btn btn-outline-secondary" (click)="dateBeforePicker.toggle()" type="button">
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#calendar"/>
</svg>
<i-bs width="1em" height="1em" name="calendar"></i-bs>
</button>
</div>

View File

@@ -1,95 +0,0 @@
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
</div>
<div class="col">
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
</div>
</div>
<div class="row">
<div class="col-md-4">
<h5 class="border-bottom pb-2" i18n>Filters</h5>
<p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p>
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
</div>
<div class="col">
<div class="row">
<div class="col">
<h5 class="border-bottom pb-2" i18n>Assignments</h5>
</div>
</div>
<div class="row">
<div class="col">
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#consumption-templates'>documentation</a>." [error]="error?.assign_title"></pngx-input-text>
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
<pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
</div>
<div class="col">
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
<div>
<label class="form-label" i18n>Assign view permissions</label>
<div class="mb-2">
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
</div>
</div>
</div>
<label class="form-label" i18n>Assign edit permissions</label>
<div>
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
</div>
</div>
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
@if (error?.non_field_errors) {
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
}
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>

View File

@@ -1,125 +0,0 @@
import { Component } from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import {
DocumentSource,
ConsumptionTemplate,
} from 'src/app/data/consumption-template'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
import { StoragePath } from 'src/app/data/storage-path'
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent } from '../edit-dialog.component'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
import { MailRule } from 'src/app/data/mail-rule'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { CustomField } from 'src/app/data/custom-field'
export const DOCUMENT_SOURCE_OPTIONS = [
{
id: DocumentSource.ConsumeFolder,
name: $localize`Consume Folder`,
},
{
id: DocumentSource.ApiUpload,
name: $localize`API Upload`,
},
{
id: DocumentSource.MailFetch,
name: $localize`Mail Fetch`,
},
]
@Component({
selector: 'pngx-consumption-template-edit-dialog',
templateUrl: './consumption-template-edit-dialog.component.html',
styleUrls: ['./consumption-template-edit-dialog.component.scss'],
})
export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<ConsumptionTemplate> {
templates: ConsumptionTemplate[]
correspondents: Correspondent[]
documentTypes: DocumentType[]
storagePaths: StoragePath[]
mailRules: MailRule[]
customFields: CustomField[]
constructor(
service: ConsumptionTemplateService,
activeModal: NgbActiveModal,
correspondentService: CorrespondentService,
documentTypeService: DocumentTypeService,
storagePathService: StoragePathService,
mailRuleService: MailRuleService,
userService: UserService,
settingsService: SettingsService,
customFieldsService: CustomFieldsService
) {
super(service, activeModal, userService, settingsService)
correspondentService
.listAll()
.pipe(first())
.subscribe((result) => (this.correspondents = result.results))
documentTypeService
.listAll()
.pipe(first())
.subscribe((result) => (this.documentTypes = result.results))
storagePathService
.listAll()
.pipe(first())
.subscribe((result) => (this.storagePaths = result.results))
mailRuleService
.listAll()
.pipe(first())
.subscribe((result) => (this.mailRules = result.results))
customFieldsService
.listAll()
.pipe(first())
.subscribe((result) => (this.customFields = result.results))
}
getCreateTitle() {
return $localize`Create new consumption template`
}
getEditTitle() {
return $localize`Edit consumption template`
}
getForm(): FormGroup {
return new FormGroup({
name: new FormControl(null),
account: new FormControl(null),
filter_filename: new FormControl(null),
filter_path: new FormControl(null),
filter_mailrule: new FormControl(null),
order: new FormControl(null),
sources: new FormControl([]),
assign_title: new FormControl(null),
assign_tags: new FormControl([]),
assign_owner: new FormControl(null),
assign_document_type: new FormControl(null),
assign_correspondent: new FormControl(null),
assign_storage_path: new FormControl(null),
assign_view_users: new FormControl([]),
assign_view_groups: new FormControl([]),
assign_change_users: new FormControl([]),
assign_change_groups: new FormControl([]),
assign_custom_fields: new FormControl([]),
})
}
get sourceOptions() {
return DOCUMENT_SOURCE_OPTIONS
}
}

View File

@@ -97,7 +97,7 @@ export abstract class EditDialogComponent<
})
}
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
// wait to enable close button so it doesn't steal focus from input since its the first clickable element in the DOM
setTimeout(() => {
this.closeEnabled = true
})

View File

@@ -0,0 +1,195 @@
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
</div>
<div class="col-4">
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
</div>
<div class="col">
<pngx-input-switch i18n-title title="Enabled" formControlName="enabled" [error]="error?.enabled"></pngx-input-switch>
</div>
</div>
<div ngbAccordion>
<div ngbAccordionItem>
<h2 ngbAccordionHeader>
<button ngbAccordionButton i18n>Triggers</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="d-flex">
<p class="p-2" i18n>Trigger Workflow On:</p>
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addTrigger()">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Trigger</ng-container>
</button>
</div>
<div ngbAccordion [closeOthers]="true">
@for (trigger of object?.triggers; track trigger; let i = $index){
<div ngbAccordionItem>
<div ngbAccordionHeader>
<button ngbAccordionButton>{{i + 1}}. {{getTriggerTypeOptionName(triggerFields.controls[i].value.type)}}
@if(trigger.id > -1) {
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{trigger.id}}</span>
}
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeTrigger(i)">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</button>
</div>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template [ngTemplateOutlet]="triggerForm" [ngTemplateOutletContext]="{ formGroup: triggerFields.controls[i], trigger: trigger }"></ng-template>
</div>
</div>
</div>
}
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem>
<h2 ngbAccordionHeader>
<button class="btn-lg" ngbAccordionButton i18n>Actions</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="d-flex">
<p class="p-2" i18n>Apply Actions:</p>
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Action</ng-container>
</button>
</div>
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
@for (action of object?.actions; track action; let i = $index){
<div ngbAccordionItem cdkDrag [formGroup]="actionFields.controls[i]">
<div ngbAccordionHeader>
<button ngbAccordionButton>{{i + 1}}. {{getActionTypeOptionName(actionFields.controls[i].value.type)}}
@if(action.id > -1) {
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
}
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeAction(i)">
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</button>
</div>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
<input type="hidden" formControlName="id" />
<div class="row">
<div class="col">
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
<pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
</div>
<div class="col">
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
<div>
<label class="form-label" i18n>Assign view permissions</label>
<div class="mb-2">
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
</div>
</div>
</div>
<label class="form-label" i18n>Assign edit permissions</label>
<div>
<div class="row mb-1">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
</div>
</div>
<div class="row">
<div class="col-lg-3">
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
</div>
<div class="col-lg-9">
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
</div>
</div>
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
</div>
</ng-template>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
@if (error?.non_field_errors) {
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
}
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>
<ng-template #triggerForm let-formGroup="formGroup" let-trigger="trigger">
<div [formGroup]="formGroup">
<input type="hidden" formControlName="id" />
<pngx-input-select i18n-title title="Trigger type" [horizontal]="true" [items]="triggerTypeOptions" formControlName="type"></pngx-input-select>
<p class="small" i18n>Trigger for documents that match <em>all</em> filters specified below.</p>
<div class="row">
<div class="col">
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a>" [error]="error?.filter_path"></pngx-input-text>
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
}
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
<pngx-input-select i18n-title title="Content matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) {
<pngx-input-text i18n-title title="Content matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
}
}
</div>
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
<div class="col-md-6">
<pngx-input-tags [allowCreate]="false" i18n-title title="Has tags" formControlName="filter_has_tags"></pngx-input-tags>
<pngx-input-select i18n-title title="Has correspondent" [items]="correspondents" [allowNull]="true" formControlName="filter_has_correspondent"></pngx-input-select>
<pngx-input-select i18n-title title="Has document type" [items]="documentTypes" [allowNull]="true" formControlName="filter_has_document_type"></pngx-input-select>
</div>
}
</div>
</div>
</ng-template>

View File

@@ -0,0 +1,5 @@
.btn.text-danger {
&:hover, &:focus {
color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;
}
}

View File

@@ -18,24 +18,69 @@ import { PermissionsUserComponent } from '../../input/permissions/permissions-us
import { SelectComponent } from '../../input/select/select.component'
import { TagsComponent } from '../../input/tags/tags.component'
import { TextComponent } from '../../input/text/text.component'
import { SwitchComponent } from '../../input/switch/switch.component'
import { EditDialogMode } from '../edit-dialog.component'
import { ConsumptionTemplateEditDialogComponent } from './consumption-template-edit-dialog.component'
import {
DOCUMENT_SOURCE_OPTIONS,
WORKFLOW_ACTION_OPTIONS,
WORKFLOW_TYPE_OPTIONS,
WorkflowEditDialogComponent,
} from './workflow-edit-dialog.component'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { Workflow } from 'src/app/data/workflow'
import {
WorkflowTriggerType,
DocumentSource,
} from 'src/app/data/workflow-trigger'
import { CdkDragDrop } from '@angular/cdk/drag-drop'
import {
WorkflowAction,
WorkflowActionType,
} from 'src/app/data/workflow-action'
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
describe('ConsumptionTemplateEditDialogComponent', () => {
let component: ConsumptionTemplateEditDialogComponent
const workflow: Workflow = {
name: 'Workflow 1',
id: 1,
order: 1,
enabled: true,
triggers: [
{
id: 1,
type: WorkflowTriggerType.Consumption,
sources: [DocumentSource.ConsumeFolder],
filter_filename: '*',
},
],
actions: [
{
id: 1,
type: WorkflowActionType.Assignment,
assign_title: 'foo',
},
{
id: 4,
type: WorkflowActionType.Assignment,
assign_owner: 2,
},
],
}
describe('WorkflowEditDialogComponent', () => {
let component: WorkflowEditDialogComponent
let settingsService: SettingsService
let fixture: ComponentFixture<ConsumptionTemplateEditDialogComponent>
let fixture: ComponentFixture<WorkflowEditDialogComponent>
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
ConsumptionTemplateEditDialogComponent,
WorkflowEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
NumberComponent,
SwitchComponent,
TagsComponent,
PermissionsUserComponent,
PermissionsGroupComponent,
@@ -113,7 +158,7 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
],
}).compileComponents()
fixture = TestBed.createComponent(ConsumptionTemplateEditDialogComponent)
fixture = TestBed.createComponent(WorkflowEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
component = fixture.componentInstance
@@ -121,15 +166,71 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
fixture.detectChanges()
})
it('should support create and edit modes', () => {
it('should support create and edit modes, support adding triggers and actions on new workflow', () => {
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()
expect(component.object).toBeUndefined()
component.addAction()
expect(component.object).not.toBeUndefined()
expect(component.object.actions).toHaveLength(1)
component.object = undefined
component.addTrigger()
expect(component.object).not.toBeUndefined()
expect(component.object.triggers).toHaveLength(1)
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
it('should return source options, type options, type name', () => {
// coverage
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
expect(component.triggerTypeOptions).toEqual(WORKFLOW_TYPE_OPTIONS)
expect(
component.getTriggerTypeOptionName(WorkflowTriggerType.DocumentAdded)
).toEqual('Document Added')
expect(component.getTriggerTypeOptionName(null)).toEqual('')
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
expect(component.actionTypeOptions).toEqual(WORKFLOW_ACTION_OPTIONS)
expect(
component.getActionTypeOptionName(WorkflowActionType.Assignment)
).toEqual('Assignment')
expect(component.getActionTypeOptionName(null)).toEqual('')
})
it('should support add and remove triggers and actions', () => {
component.object = workflow
component.addTrigger()
expect(component.object.triggers.length).toEqual(2)
component.addAction()
expect(component.object.actions.length).toEqual(3)
component.removeTrigger(1)
expect(component.object.triggers.length).toEqual(1)
component.removeAction(1)
expect(component.object.actions.length).toEqual(2)
})
it('should update order and remove ids from actions on drag n drop', () => {
const action1 = workflow.actions[0]
const action2 = workflow.actions[1]
component.object = workflow
component.ngOnInit()
component.onActionDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
WorkflowAction[]
>)
expect(component.object.actions).toEqual([action2, action1])
expect(action1.id).toBeNull()
expect(action2.id).toBeNull()
})
it('should not include auto matching in algorithms', () => {
expect(component.getMatchingAlgorithms()).not.toContain(
MATCHING_ALGORITHMS.find((a) => a.id === MATCH_AUTO)
)
})
})

View File

@@ -0,0 +1,320 @@
import { Component, OnInit } from '@angular/core'
import { FormGroup, FormControl, FormArray } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import { Workflow } from 'src/app/data/workflow'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
import { StoragePath } from 'src/app/data/storage-path'
import { WorkflowService } from 'src/app/services/rest/workflow.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent } from '../edit-dialog.component'
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
import { MailRule } from 'src/app/data/mail-rule'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { CustomField } from 'src/app/data/custom-field'
import {
DocumentSource,
WorkflowTrigger,
WorkflowTriggerType,
} from 'src/app/data/workflow-trigger'
import {
WorkflowAction,
WorkflowActionType,
} from 'src/app/data/workflow-action'
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
import {
MATCHING_ALGORITHMS,
MATCH_AUTO,
MATCH_NONE,
} from 'src/app/data/matching-model'
export const DOCUMENT_SOURCE_OPTIONS = [
{
id: DocumentSource.ConsumeFolder,
name: $localize`Consume Folder`,
},
{
id: DocumentSource.ApiUpload,
name: $localize`API Upload`,
},
{
id: DocumentSource.MailFetch,
name: $localize`Mail Fetch`,
},
]
export const WORKFLOW_TYPE_OPTIONS = [
{
id: WorkflowTriggerType.Consumption,
name: $localize`Consumption Started`,
},
{
id: WorkflowTriggerType.DocumentAdded,
name: $localize`Document Added`,
},
{
id: WorkflowTriggerType.DocumentUpdated,
name: $localize`Document Updated`,
},
]
export const WORKFLOW_ACTION_OPTIONS = [
{
id: WorkflowActionType.Assignment,
name: $localize`Assignment`,
},
]
const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
(a) => a.id !== MATCH_AUTO
)
@Component({
selector: 'pngx-workflow-edit-dialog',
templateUrl: './workflow-edit-dialog.component.html',
styleUrls: ['./workflow-edit-dialog.component.scss'],
})
export class WorkflowEditDialogComponent
extends EditDialogComponent<Workflow>
implements OnInit
{
public WorkflowTriggerType = WorkflowTriggerType
templates: Workflow[]
correspondents: Correspondent[]
documentTypes: DocumentType[]
storagePaths: StoragePath[]
mailRules: MailRule[]
customFields: CustomField[]
expandedItem: number = null
constructor(
service: WorkflowService,
activeModal: NgbActiveModal,
correspondentService: CorrespondentService,
documentTypeService: DocumentTypeService,
storagePathService: StoragePathService,
mailRuleService: MailRuleService,
userService: UserService,
settingsService: SettingsService,
customFieldsService: CustomFieldsService
) {
super(service, activeModal, userService, settingsService)
correspondentService
.listAll()
.pipe(first())
.subscribe((result) => (this.correspondents = result.results))
documentTypeService
.listAll()
.pipe(first())
.subscribe((result) => (this.documentTypes = result.results))
storagePathService
.listAll()
.pipe(first())
.subscribe((result) => (this.storagePaths = result.results))
mailRuleService
.listAll()
.pipe(first())
.subscribe((result) => (this.mailRules = result.results))
customFieldsService
.listAll()
.pipe(first())
.subscribe((result) => (this.customFields = result.results))
}
getCreateTitle() {
return $localize`Create new workflow`
}
getEditTitle() {
return $localize`Edit workflow`
}
getForm(): FormGroup {
return new FormGroup({
name: new FormControl(null),
order: new FormControl(null),
enabled: new FormControl(true),
triggers: new FormArray([]),
actions: new FormArray([]),
})
}
getMatchingAlgorithms() {
// No auto matching
return TRIGGER_MATCHING_ALGORITHMS
}
ngOnInit(): void {
super.ngOnInit()
this.updateAllTriggerActionFields()
}
get triggerFields(): FormArray {
return this.objectForm.get('triggers') as FormArray
}
get actionFields(): FormArray {
return this.objectForm.get('actions') as FormArray
}
private createTriggerField(
trigger: WorkflowTrigger,
emitEvent: boolean = false
) {
this.triggerFields.push(
new FormGroup({
id: new FormControl(trigger.id),
type: new FormControl(trigger.type),
sources: new FormControl(trigger.sources),
filter_filename: new FormControl(trigger.filter_filename),
filter_path: new FormControl(trigger.filter_path),
filter_mailrule: new FormControl(trigger.filter_mailrule),
matching_algorithm: new FormControl(trigger.matching_algorithm),
match: new FormControl(trigger.match),
is_insensitive: new FormControl(trigger.is_insensitive),
filter_has_tags: new FormControl(trigger.filter_has_tags),
filter_has_correspondent: new FormControl(
trigger.filter_has_correspondent
),
filter_has_document_type: new FormControl(
trigger.filter_has_document_type
),
}),
{ emitEvent }
)
}
private createActionField(
action: WorkflowAction,
emitEvent: boolean = false
) {
this.actionFields.push(
new FormGroup({
id: new FormControl(action.id),
type: new FormControl(action.type),
assign_title: new FormControl(action.assign_title),
assign_tags: new FormControl(action.assign_tags),
assign_owner: new FormControl(action.assign_owner),
assign_document_type: new FormControl(action.assign_document_type),
assign_correspondent: new FormControl(action.assign_correspondent),
assign_storage_path: new FormControl(action.assign_storage_path),
assign_view_users: new FormControl(action.assign_view_users),
assign_view_groups: new FormControl(action.assign_view_groups),
assign_change_users: new FormControl(action.assign_change_users),
assign_change_groups: new FormControl(action.assign_change_groups),
assign_custom_fields: new FormControl(action.assign_custom_fields),
}),
{ emitEvent }
)
}
private updateAllTriggerActionFields(emitEvent: boolean = false) {
this.triggerFields.clear({ emitEvent: false })
this.object?.triggers.forEach((trigger) => {
this.createTriggerField(trigger, emitEvent)
})
this.actionFields.clear({ emitEvent: false })
this.object?.actions.forEach((action) => {
this.createActionField(action, emitEvent)
})
}
get sourceOptions() {
return DOCUMENT_SOURCE_OPTIONS
}
get triggerTypeOptions() {
return WORKFLOW_TYPE_OPTIONS
}
getTriggerTypeOptionName(type: WorkflowTriggerType): string {
return this.triggerTypeOptions.find((t) => t.id === type)?.name ?? ''
}
addTrigger() {
if (!this.object) {
this.object = Object.assign({}, this.objectForm.value)
}
const trigger: WorkflowTrigger = {
type: WorkflowTriggerType.Consumption,
sources: [],
filter_filename: null,
filter_path: null,
filter_mailrule: null,
filter_has_tags: [],
filter_has_correspondent: null,
filter_has_document_type: null,
matching_algorithm: MATCH_NONE,
match: '',
is_insensitive: true,
}
this.object.triggers.push(trigger)
this.createTriggerField(trigger)
}
get actionTypeOptions() {
return WORKFLOW_ACTION_OPTIONS
}
getActionTypeOptionName(type: WorkflowActionType): string {
return this.actionTypeOptions.find((t) => t.id === type)?.name ?? ''
}
addAction() {
if (!this.object) {
this.object = Object.assign({}, this.objectForm.value)
}
const action: WorkflowAction = {
type: WorkflowActionType.Assignment,
assign_title: null,
assign_tags: [],
assign_document_type: null,
assign_correspondent: null,
assign_storage_path: null,
assign_owner: null,
assign_view_users: [],
assign_view_groups: [],
assign_change_users: [],
assign_change_groups: [],
assign_custom_fields: [],
}
this.object.actions.push(action)
this.createActionField(action)
}
removeTrigger(index: number) {
this.object.triggers.splice(index, 1).pop()
this.triggerFields.removeAt(index)
}
removeAction(index: number) {
this.object.actions.splice(index, 1)
this.actionFields.removeAt(index)
}
onActionDrop(event: CdkDragDrop<WorkflowAction[]>) {
moveItemInArray(
this.object.actions,
event.previousIndex,
event.currentIndex
)
const actionField = this.actionFields.at(event.previousIndex)
this.actionFields.removeAt(event.previousIndex)
this.actionFields.insert(event.currentIndex, actionField)
// removing id will effectively re-create the actions in this order
this.object.actions.forEach((a) => (a.id = null))
}
}

View File

@@ -1,8 +1,6 @@
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)">
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
<svg class="toolbaricon" fill="currentColor">
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
</svg>
<i-bs name="{{icon}}"></i-bs>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
@if (!editing && selectionModel.totalCount > 0) {
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
@@ -49,9 +47,7 @@
@if (editing) {
<button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
<svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
</svg>
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
</button>
}
@if (!editing && manyToOne) {

View File

@@ -25,10 +25,6 @@
}
}
small > svg {
margin-top: -2px;
}
.list-group-item-note {
line-height: 1;

View File

@@ -1,19 +1,13 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled">
<div class="selected-icon me-1">
@if (isChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-check">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
<i-bs width="1em" height="1em" name="check"></i-bs>
}
@if (isPartiallyChecked()) {
<svg fill="currentColor" class="buttonicon-sm bi-dash">
<use xlink:href="assets/bootstrap-icons.svg#dash"/>
</svg>
<i-bs width="1em" height="1em" name="dash"></i-bs>
}
@if (isExcluded()) {
<svg fill="currentColor" class="buttonicon-sm bi-x">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<i-bs width="1em" height="1em" name="x"></i-bs>
}
</div>
<div class="me-1">

View File

@@ -5,9 +5,7 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -16,10 +16,7 @@
<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">
<path d="M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3H3z"/>
<path d="M5.5 4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm4-4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
</svg>
<i-bs name="dice5"></i-bs>
</button>
</div>

View File

@@ -4,9 +4,7 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
@@ -16,15 +14,11 @@
(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">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="buttonicon">
<use _ngcontent-ng-c3750736003="" xlink:href="assets/bootstrap-icons.svg#calendar"></use>
</svg>
<i-bs width="1.2em" height="1.2em" name="calendar"></i-bs>
</button>
@if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ filterButtonTitle }}">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
</button>
}
</div>

View File

@@ -6,51 +6,45 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div [class.col-md-9]="horizontal">
<div>
<ng-select name="inputId" [(ngModel)]="selectedDocuments"
[disabled]="disabled"
[items]="foundDocuments$ | async"
placeholder="Search for documents"
[notFoundText]="notFoundText"
[multiple]="true"
bindValue="id"
[compareWith]="compareDocuments"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="loading"
[typeahead]="documentsInput$"
(change)="onChange(selectedDocuments)">
<ng-template ng-label-tmp let-document="item">
<div class="d-flex align-items-center">
<svg class="sidebaricon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" (click)="unselect(document)">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
<svg class="sidebaricon-sm me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg><span>{{document.title}}</span>
</a>
</div>
</ng-template>
<ng-template ng-loadingspinner-tmp>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</ng-template>
<ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
<div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
</ng-template>
</ng-select>
</div>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div>
</div>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div [class.col-md-9]="horizontal">
<div>
<ng-select name="inputId" [(ngModel)]="selectedDocuments"
[disabled]="disabled"
[items]="foundDocuments$ | async"
placeholder="Search for documents"
[notFoundText]="notFoundText"
[multiple]="true"
bindValue="id"
[compareWith]="compareDocuments"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="loading"
[typeahead]="documentsInput$"
(change)="onChange(selectedDocuments)">
<ng-template ng-label-tmp let-document="item">
<div class="d-flex align-items-center">
<i-bs (click)="unselect(document)" name="x"></i-bs>
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();">
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<span>{{document.title}}</span>
</a>
</div>
</ng-template>
<ng-template ng-loadingspinner-tmp>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</ng-template>
<ng-template ng-option-tmp let-document="item" let-index="index" let-search="searchTerm">
<div>{{document.title}} <small class="text-muted">({{document.created | customDate:'shortDate'}})</small></div>
</ng-template>
</ng-select>
</div>
@if (hint) {
<small class="form-text text-muted">{{hint}}</small>
}
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@
}
}
.sidebaricon {
i-bs {
cursor: pointer;
}

View File

@@ -0,0 +1,33 @@
<div class="mb-3" [class.pb-3]="error">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
@if (title) {
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div class="input-group" [class.col-md-9]="horizontal" [class.is-invalid]="error">
<input #fileInput type="file" class="form-control" [id]="inputId" (change)="onFile($event)" [disabled]="disabled">
<button class="btn btn-outline-primary py-0" type="button" (click)="uploadClicked()" [disabled]="disabled || !file" i18n>Upload</button>
</div>
@if (filename) {
<div class="form-text d-flex align-items-center">
<span class="text-muted">{{filename}}</span>
<button type="button" class="btn btn-link btn-sm text-danger ms-2" (click)="clear()">
<i-bs name="x"></i-bs><small i18n>Remove</small>
</button>
</div>
}
<input #inputField type="hidden" class="form-control small" [(ngModel)]="value" [disabled]="true">
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>
</div>
</div>

View File

@@ -0,0 +1,41 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FileComponent } from './file.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
describe('FileComponent', () => {
let component: FileComponent
let fixture: ComponentFixture<FileComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileComponent],
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
}).compileComponents()
fixture = TestBed.createComponent(FileComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should update file on change', () => {
const event = { target: { files: [new File([], 'test.png')] } }
component.onFile(event as any)
expect(component.file.name).toEqual('test.png')
})
it('should get filename', () => {
component.value = 'https://example.com:8000/logo/filename.svg'
expect(component.filename).toEqual('filename.svg')
})
it('should fire upload event', () => {
let firedFile
component.file = new File([], 'test.png')
component.upload.subscribe((file) => (firedFile = file))
component.uploadClicked()
expect(firedFile.name).toEqual('test.png')
expect(component.file).toBeUndefined()
})
})

View File

@@ -0,0 +1,53 @@
import {
Component,
ElementRef,
EventEmitter,
Output,
ViewChild,
forwardRef,
} from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@Component({
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FileComponent),
multi: true,
},
],
selector: 'pngx-input-file',
templateUrl: './file.component.html',
styleUrl: './file.component.scss',
})
export class FileComponent extends AbstractInputComponent<string> {
@Output()
upload = new EventEmitter<File>()
public file: File
@ViewChild('fileInput') fileInput: ElementRef
get filename(): string {
return this.value
? this.value.substring(this.value.lastIndexOf('/') + 1)
: null
}
onFile(event: Event) {
this.file = (event.target as HTMLInputElement).files[0]
}
uploadClicked() {
this.upload.emit(this.file)
this.clear()
}
clear() {
this.file = undefined
this.fileInput.nativeElement.value = null
this.writeValue(null)
this.onChange(null)
}
}

View File

@@ -1,12 +1,12 @@
<div class="mb-3" [class.pb-3]="error">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (title) {
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -56,7 +56,7 @@ describe('NumberComponent', () => {
component.step = 0.1
component.writeValue(12.3456)
expect(component.value).toEqual(12.3456)
// float (step = .1) doesnt force 2 decimals
// float (step = .1) doesn't force 2 decimals
component.writeValue(11.1)
expect(component.value).toEqual(11.1)
})

View File

@@ -4,9 +4,7 @@
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
@if (showReveal) {
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#eye" />
</svg>
<i-bs name="eye"></i-bs>
</button>
}
</div>

View File

@@ -28,7 +28,7 @@ describe('PasswordComponent', () => {
it('should support use of input field', () => {
expect(component.value).toBeUndefined()
// TODO: why doesnt this work?
// TODO: why doesn't this work?
// input.value = 'foo'
// input.dispatchEvent(new Event('change'))
// fixture.detectChanges()

View File

@@ -0,0 +1,4 @@
.accordion {
--bs-accordion-btn-padding-x: 0.75rem;
--bs-accordion-btn-padding-y: 0.375rem;
}

View File

@@ -6,9 +6,7 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
@@ -40,16 +38,12 @@
</ng-select>
@if (allowCreateNew) {
<button class="btn btn-outline-secondary" type="button" (click)="addItem()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
<i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
</button>
}
@if (showFilter) {
<button 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>
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
</button>
}
</div>

View File

@@ -0,0 +1,40 @@
<div class="mb-3">
<div class="row">
@if (!horizontal) {
<div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
<label class="form-label" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
{{title}}
@if (showUnsetNote && isUnset) {
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
}
</label>
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
}
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}">
<div class="form-check form-switch">
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
@if (horizontal) {
<label class="form-check-label" [class.text-muted]="showUnsetNote && isUnset" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
{{title}}
@if (showUnsetNote && isUnset) {
&nbsp;<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
}
</label>
}
@if (hint) {
<div class="form-text text-muted">{{hint}}</div>
}
</div>
</div>
</div>
</div>
<ng-template #tipContent>
<span class="text-light fst-italic" i18n>Note: value has not yet been set and will not apply until explicitly changed</span>
</ng-template>

View File

@@ -0,0 +1,45 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { SwitchComponent } from './switch.component'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
describe('SwitchComponent', () => {
let component: SwitchComponent
let fixture: ComponentFixture<SwitchComponent>
let input: HTMLInputElement
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [SwitchComponent],
providers: [],
imports: [FormsModule, ReactiveFormsModule, NgbTooltipModule],
}).compileComponents()
fixture = TestBed.createComponent(SwitchComponent)
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()
})
it('should show note if unset', () => {
component.value = null
expect(component.isUnset).toBeTruthy()
})
})

View File

@@ -0,0 +1,28 @@
import { Component, Input, forwardRef } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
@Component({
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SwitchComponent),
multi: true,
},
],
selector: 'pngx-input-switch',
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.scss'],
})
export class SwitchComponent extends AbstractInputComponent<boolean> {
@Input()
showUnsetNote: boolean = false
constructor() {
super()
}
get isUnset(): boolean {
return this.value === null || this.value === undefined
}
}

View File

@@ -18,9 +18,7 @@
<ng-template ng-label-tmp let-item="item">
<span class="tag-wrap tag-wrap-delete" (mousedown)="removeTag($event, item.id)">
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<i-bs name="x"></i-bs>
@if (item.id && tags) {
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
}
@@ -36,16 +34,12 @@
</ng-select>
@if (allowCreate) {
<button class="btn btn-outline-secondary" type="button" (click)="createTag()" [disabled]="disabled">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
<i-bs width="1.2em" height="1.2em" name="plus"></i-bs>
</button>
}
@if (showFilter) {
<button class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="hasPrivate || this.value === null" i18n-title title="Filter documents with these Tags">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#filter" />
</svg>
<i-bs width="1.2em" height="1.2em" name="filter"></i-bs>
</button>
}
</div>

View File

@@ -1,12 +1,12 @@
<div class="mb-3" [class.pb-3]="error">
<div class="row">
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (title) {
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -27,7 +27,7 @@ describe('TextComponent', () => {
it('should support use of input field', () => {
expect(component.value).toBeUndefined()
// TODO: why doesnt this work?
// TODO: why doesn't this work?
// input.value = 'foo'
// input.dispatchEvent(new Event('change'))
// fixture.detectChanges()

View File

@@ -4,27 +4,23 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error">
<input #inputField type="url" class="form-control" [class.is-invalid]="error" placeholder="https://" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
<a class="btn btn-outline-secondary rounded-end" title="Open link" i18n-title [href]="value" target="_blank">
<svg class="buttonicon mb-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#box-arrow-up-right" />
</svg>
</a>
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
<div [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error">
<input #inputField type="url" class="form-control" [class.is-invalid]="error" placeholder="https://" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
<a class="btn btn-outline-secondary rounded-end" title="Open link" i18n-title [href]="value" target="_blank">
<i-bs width="1.2em" height="1.2em" name="box-arrow-up-right"></i-bs>
</a>
<div class="invalid-feedback position-absolute top-100">
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div>
</div>
</div>

View File

@@ -27,7 +27,7 @@ describe('TextComponent', () => {
it('should support use of input field', () => {
expect(component.value).toBeUndefined()
// TODO: why doesnt this work?
// TODO: why doesn't this work?
// input.value = 'foo'
// input.dispatchEvent(new Event('change'))
// fixture.detectChanges()

View File

@@ -1,18 +1,22 @@
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'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>
@if (customLogo) {
<img src="{{customLogo}}" height="100%" width="100%" [attr.style]="'height:'+height" />
} @else {
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'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>
}

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -2,15 +2,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
import { LogoComponent } from './logo.component'
import { By } from '@angular/platform-browser'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { SettingsService } from 'src/app/services/settings.service'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
describe('LogoComponent', () => {
let component: LogoComponent
let fixture: ComponentFixture<LogoComponent>
let settingsService: SettingsService
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [LogoComponent],
imports: [HttpClientTestingModule],
})
settingsService = TestBed.inject(SettingsService)
fixture = TestBed.createComponent(LogoComponent)
component = fixture.componentInstance
fixture.detectChanges()
@@ -33,4 +39,9 @@ describe('LogoComponent', () => {
'height:10em'
)
})
it('should support getting custom logo', () => {
settingsService.set(SETTINGS_KEYS.APP_LOGO, '/logo/test.png')
expect(component.customLogo).toEqual('http://localhost:8000/logo/test.png')
})
})

View File

@@ -1,4 +1,7 @@
import { Component, Input } from '@angular/core'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from 'src/app/services/settings.service'
import { environment } from 'src/environments/environment'
@Component({
selector: 'pngx-logo',
@@ -12,6 +15,17 @@ export class LogoComponent {
@Input()
height = '6em'
get customLogo(): string {
return this.settingsService.get(SETTINGS_KEYS.APP_LOGO)?.length
? environment.apiBaseUrl.replace(
/\/api\/$/,
this.settingsService.get(SETTINGS_KEYS.APP_LOGO)
)
: null
}
constructor(private settingsService: SettingsService) {}
getClasses() {
return ['logo'].concat(this.extra_classes).join(' ')
}

View File

@@ -5,6 +5,18 @@
@if (subTitle) {
<span class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span>
}
@if (info) {
<button class="btn btn-sm btn-link text-muted me-auto p-0 p-md-2" title="What's this?" i18n-title type="button" [ngbPopover]="infoPopover" [autoClose]="true">
<i-bs name="question-circle"></i-bs>
</button>
<ng-template #infoPopover>
<p [class.mb-0]="!infoLink" [innerHTML]="info"></p>
@if (infoLink) {
<a href="https://docs.paperless-ngx.com/{{infoLink}}" target="_blank" referrerpolicy="noopener noreferrer" i18n>Read more</a>
<i-bs class="ms-1" width=".8em" height=".8em" name="box-arrow-up-right"></i-bs>
}
</ng-template>
}
</h3>
</div>
<div class="btn-toolbar col col-md-auto">

View File

@@ -24,4 +24,10 @@ export class PageHeaderComponent {
@Input()
subTitle: string = ''
@Input()
info: string
@Input()
infoLink: string
}

View File

@@ -465,33 +465,42 @@ export class PdfViewerComponent
this.clear()
this.setupViewer()
this.loadingTask = PDFJS.getDocument(this.getDocumentParams())
this.loadingTask!.onProgress = (progressData: PDFProgressData) => {
this.onProgress.emit(progressData)
if (this.pdfViewer) {
this.pdfViewer._resetView()
this.pdfViewer = null
}
const src = this.src
this.setupViewer()
from(this.loadingTask!.promise as Promise<PDFDocumentProxy>)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (pdf) => {
this._pdf = pdf
this.lastLoaded = src
try {
this.loadingTask = PDFJS.getDocument(this.getDocumentParams())
this.afterLoadComplete.emit(pdf)
this.resetPdfDocument()
this.loadingTask!.onProgress = (progressData: PDFProgressData) => {
this.onProgress.emit(progressData)
}
this.update()
},
error: (error) => {
this.lastLoaded = null
this.onError.emit(error)
},
})
const src = this.src
from(this.loadingTask!.promise as Promise<PDFDocumentProxy>)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: (pdf) => {
this._pdf = pdf
this.lastLoaded = src
this.afterLoadComplete.emit(pdf)
this.resetPdfDocument()
this.update()
},
error: (error) => {
this.lastLoaded = null
this.onError.emit(error)
},
})
} catch (e) {
this.onError.emit(e)
}
}
private update() {

View File

@@ -1,8 +1,6 @@
<div class="btn-group w-100" ngbDropdown role="group">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="isActive ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>
<i-bs name="person-fill-lock"></i-bs>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
</button>
@@ -11,9 +9,7 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NONE)" [disabled]="disabled">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.NONE) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
<i-bs width="1em" height="1em" name="check"></i-bs>
}
</div>
<div class="me-1">
@@ -23,9 +19,7 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SELF)" [disabled]="disabled">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.SELF) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
<i-bs width="1em" height="1em" name="check"></i-bs>
}
</div>
<div class="me-1">
@@ -35,9 +29,7 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.NOT_SELF)" [disabled]="disabled">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
<i-bs width="1em" height="1em" name="check"></i-bs>
}
</div>
<div class="me-1">
@@ -47,9 +39,7 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
<i-bs width="1em" height="1em" name="check"></i-bs>
}
</div>
<div class="me-1">
@@ -59,9 +49,7 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
<i-bs width="1em" height="1em" name="check"></i-bs>
}
</div>
<div class="me-1">
@@ -71,9 +59,7 @@
<button *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }" class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" [disabled]="disabled">
<div class="selected-icon me-1">
@if (selectionModel.ownerFilter === OwnerFilterType.OTHERS) {
<svg fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
<i-bs width="1em" height="1em" name="check"></i-bs>
}
</div>
<div class="me-1 w-100">

View File

@@ -9,9 +9,7 @@
} @else {
@if (requiresPassword) {
<div class="w-100 h-100 position-relative">
<svg width="2em" height="2em" fill="currentColor" class="position-absolute top-50 start-50 translate-middle">
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-lock"/>
</svg>
<i-bs width="2em" height="2em" class="position-absolute top-50 start-50 translate-middle" name="file-earmark-lock"></i-bs>
</div>
}
@if (!requiresPassword) {

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