Compare commits

...
Author SHA1 Message Date
shamoon 198ee6ea09 Just some stuff 2024-12-20 22:13:53 -08:00
shamoon 112b39c4de Lol this works 2024-12-20 20:09:30 -08:00
shamoon 01706dd391 Just messing around 2024-12-20 17:08:59 -08:00
452ea2ccf9 Enhancement: add timeout for Tika client (#8520)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-12-19 16:58:26 +00:00
shamoonandGitHub 2bcbed31e9 Fix: add some minor frontend permissions checks (#8524) 2024-12-19 08:08:46 -08:00
shamoon 20b7ff9f9f Fix: resolve test warning 2024-12-19 08:05:06 -08:00
dependabot[bot]shamoondependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3f7a0802a4 Chore(deps-dev): Bump the development group with 2 updates (#8502)
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.8.2 to 0.8.3
- [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/0.8.2...0.8.3)

Updates `mkdocs-material` from 9.5.48 to 9.5.49
- [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.48...9.5.49)

---
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-12-16 13:53:12 -08:00
shamoonandGitHub ea514a7ed8 FIx: obliquely trim spaces from global search (#8484) 2024-12-13 07:39:16 -08:00
shamoon af67dbe523 Update frontend strings 2024-12-13 07:36:41 -08:00
shamoon 8cd09ba10d Chore: add prettier organize imports 2024-12-13 00:45:20 -08:00
shamoonandGitHub 9e4bc05a24 Fix: include global perms for bulk edit endpoint (#8468) 2024-12-12 15:38:54 +00:00
shamoonandGitHub dafb0b1f21 Enhancement: process mail button (#8466) 2024-12-11 11:03:53 -08:00
shamoonandGitHub 2ac2a6dec6 Fix: frontend better reflect global perms for bulk edit, disabled form state (#8469) 2024-12-11 01:05:23 -08:00
shamoon beb8ed8313 Fix: minor safari visual fixes around loading cards 2024-12-10 08:49:18 -08:00
shamoonandGitHub 997db0fea1 Chore: update ng2 pdf viewer (#8462) 2024-12-09 16:12:53 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
64d40a7c37 Chore(deps-dev): Bump the development group with 2 updates (#8458)
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.8.1 to 0.8.2
- [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/0.8.1...0.8.2)

Updates `mkdocs-material` from 9.5.47 to 9.5.48
- [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.47...9.5.48)

---
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-12-09 22:56:28 +00:00
dependabot[bot]GitHubshamoondependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dc422b5dac Chore(deps): Bump django-soft-delete from 1.0.15 to 1.0.16 in the django group (#8459)
* Chore(deps): Bump django-soft-delete in the django group

Bumps the django group with 1 update: [django-soft-delete](https://github.com/san4ezy/django_softdelete).


Updates `django-soft-delete` from 1.0.15 to 1.0.16
- [Commits](https://github.com/san4ezy/django_softdelete/commits)

---
updated-dependencies:
- dependency-name: django-soft-delete
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: django
...

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 21:48:54 +00:00
dependabot[bot]GitHubshamoondependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f56ab150b0 Chore(deps): Bump the small-changes group with 4 updates (#8460)
* Chore(deps): Bump the small-changes group with 4 updates

Bumps the small-changes group with 4 updates: [httpx-oauth](https://github.com/frankie567/httpx-oauth), [ocrmypdf](https://github.com/ocrmypdf/OCRmyPDF), [redis](https://github.com/redis/redis-py) and [scikit-learn](https://github.com/scikit-learn/scikit-learn).


Updates `httpx-oauth` from 0.15.1 to 0.16.0
- [Release notes](https://github.com/frankie567/httpx-oauth/releases)
- [Commits](https://github.com/frankie567/httpx-oauth/compare/v0.15.1...v0.16.0)

Updates `ocrmypdf` from 16.6.2 to 16.7.0
- [Release notes](https://github.com/ocrmypdf/OCRmyPDF/releases)
- [Changelog](https://github.com/ocrmypdf/OCRmyPDF/blob/main/docs/release_notes.rst)
- [Commits](https://github.com/ocrmypdf/OCRmyPDF/compare/v16.6.2...v16.7.0)

Updates `redis` from 5.2.0 to 5.2.1
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v5.2.0...v5.2.1)

Updates `scikit-learn` from 1.5.2 to 1.6.0
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.5.2...1.6.0)

---
updated-dependencies:
- dependency-name: httpx-oauth
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
- dependency-name: ocrmypdf
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
- dependency-name: redis
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: scikit-learn
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
...

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 21:35:09 +00:00
shamoonandGitHub e3fa3fe818 Chore: use rxjs instead of JS setInterval for timers (#8461) 2024-12-09 13:14:48 -08:00
shamoon 0a7c296194 Chore: refactor loading stuff to be more DRY 2024-12-09 12:57:58 -08:00
shamoon c2e34b36ce Fix: only adjust icon position inside buttons 2024-12-09 12:17:27 -08:00
shamoon 084f2f2822 Enhancement: slightly nicer filter editor loading 2024-12-09 09:52:36 -08:00
shamoonandGitHub e4f69dc945 Feature: bulk edit custom field values (#8428) 2024-12-09 17:35:49 +00:00
shamoon 8574d28c6f Fix: hide other filtering buttons, fix e2e test paths 2024-12-08 21:23:41 -08:00
shamoon 27f575c2d1 Fix some strings 2024-12-08 20:34:16 -08:00
shamoon 20e45407f5 Fix: fix trash column display on mobile 2024-12-07 08:05:11 -08:00
shamoon f836c5ce3e Chore: some logging to trash emptying 2024-12-07 08:04:10 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
476d753720 Chore(deps): Bump django from 5.1.3 to 5.1.4 (#8445)
Bumps [django](https://github.com/django/django) from 5.1.3 to 5.1.4.
- [Commits](https://github.com/django/django/compare/5.1.3...5.1.4)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-06 16:20:38 -08:00
shamoon 65b48952cd Refactor: loading component 2024-12-06 00:46:21 -08:00
shamoonandGitHub 0647812699 Enhancement: improved loading visuals (#8435) 2024-12-05 20:26:28 -08:00
shamoon 8722ff481c Fix: use update preview component for dashboard widget 2024-12-04 23:14:08 -08:00
shamoon 0c883d064e Fix: some random frontend fixes and coverage 2024-12-04 21:15:45 -08:00
shamoon df2c139721 Chore: mark another test flaky 2024-12-04 12:10:31 -08:00
shamoon 24b678b7e6 Delete workflow-dialog.component.scss 2024-12-04 00:21:28 -08:00
shamoon a14b9127b6 Fix: sidebar saved views label, add spacing to settings columns 2024-12-03 11:44:44 -08:00
shamoonandGitHub 7d182ab894 Enhancement: prune audit logs and management command (#8416) 2024-12-03 19:28:27 +00:00
shamoon 51c339d1b7 Chore: compact settings 2024-12-03 11:19:49 -08:00
shamoonandGitHub ab548e36c7 Change: make saved views manage its own component (#8423) 2024-12-03 11:09:02 -08:00
shamoon ae4e8808b0 Fix: move filter to header, mobile layout improvement 2024-12-03 11:07:04 -08:00
shamoonandGitHub b4e369b556 Enhancement: file task filtering (#8421) 2024-12-03 10:44:52 -08:00
shamoon 5f7c60d9a1 Chore: remove some tsconfig and jest stuff 2024-12-03 09:12:38 -08:00
shamoon fca10227bc Update translation strings 2024-12-02 23:04:58 -08:00
shamoon 007e5a5473 Fix: dont link related document if document is in the trash 2024-12-02 19:42:54 -08:00
shamoonandGitHub 70d9a6fd36 Enhancement: auto-link duplicate document for failed tasks (#8415) 2024-12-02 19:11:40 -08:00
0406fca59b Enhancement: include current filename placeholder in workflows (#8319)
Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-12-03 03:09:27 +00:00
shamoonandGitHub 1d65628132 Feature: email, webhook workflow actions (#8108) 2024-12-03 00:12:40 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>shamoon
81a5baa451 Chore(deps-dev): Bump the development group with 4 updates (#8414)
* Chore(deps-dev): Bump the development group with 4 updates

Bumps the development group with 4 updates: [ruff](https://github.com/astral-sh/ruff), [pytest](https://github.com/pytest-dev/pytest), [pytest-httpx](https://github.com/Colin-b/pytest_httpx) and [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


Updates `ruff` from 0.8.0 to 0.8.1
- [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/0.8.0...0.8.1)

Updates `pytest` from 8.3.3 to 8.3.4
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.3...8.3.4)

Updates `pytest-httpx` from 0.34.0 to 0.35.0
- [Release notes](https://github.com/Colin-b/pytest_httpx/releases)
- [Changelog](https://github.com/Colin-b/pytest_httpx/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/Colin-b/pytest_httpx/compare/v0.34.0...v0.35.0)

Updates `mkdocs-material` from 9.5.46 to 9.5.47
- [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.46...9.5.47)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: pytest
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: pytest-httpx
  dependency-type: direct:development
  update-type: version-update:semver-minor
  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>

* Update .pre-commit-config.yaml

---------

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-12-02 23:29:56 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e9254d4eef Chore(deps): Bump codecov/codecov-action in the actions group (#8401)
Bumps the actions group with 1 update: [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `codecov/codecov-action` from 4 to 5
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 08:59:45 -08:00
shamoonandGitHub 0fc1860d4c Enhancement: use stable unique IDs for custom field select options (#8299) 2024-12-02 04:15:38 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
00485138f9 Chore(deps-dev): Bump the development group with 4 updates (#8352)
Bumps the development group with 4 updates: [ruff](https://github.com/astral-sh/ruff), [pytest-httpx](https://github.com/Colin-b/pytest_httpx), [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) and [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


Updates `ruff` from 0.7.3 to 0.8.0
- [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/0.7.3...0.8.0)

Updates `pytest-httpx` from 0.33.0 to 0.34.0
- [Release notes](https://github.com/Colin-b/pytest_httpx/releases)
- [Changelog](https://github.com/Colin-b/pytest_httpx/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/Colin-b/pytest_httpx/compare/v0.33.0...v0.34.0)

Updates `pytest-rerunfailures` from 14.0 to 15.0
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/14.0...15.0)

Updates `mkdocs-material` from 9.5.44 to 9.5.46
- [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.44...9.5.46)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: pytest-httpx
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: pytest-rerunfailures
  dependency-type: direct:development
  update-type: version-update:semver-major
  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-12-01 18:56:54 -08:00
shamoonandGitHub 79345f0a69 Enhancement: better TIFF display support (#8087) 2024-12-01 11:46:19 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3aec0b3372 Chore(deps): Bump the small-changes group across 1 directory with 7 updates (#8399)
Bumps the small-changes group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [django-allauth](https://github.com/sponsors/pennersr) | `65.2.0` | `65.3.0` |
| [channels](https://github.com/django/channels) | `4.1.0` | `4.2.0` |
| [channels-redis](https://github.com/django/channels_redis) | `4.2.0` | `4.2.1` |
| [mysqlclient](https://github.com/PyMySQL/mysqlclient) | `2.2.5` | `2.2.6` |
| [ocrmypdf](https://github.com/ocrmypdf/OCRmyPDF) | `16.6.1` | `16.6.2` |
| [setproctitle](https://github.com/dvarrazzo/py-setproctitle) | `1.3.3` | `1.3.4` |
| [tqdm](https://github.com/tqdm/tqdm) | `4.67.0` | `4.67.1` |



Updates `django-allauth` from 65.2.0 to 65.3.0
- [Commits](https://github.com/sponsors/pennersr/commits)

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

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

Updates `mysqlclient` from 2.2.5 to 2.2.6
- [Release notes](https://github.com/PyMySQL/mysqlclient/releases)
- [Changelog](https://github.com/PyMySQL/mysqlclient/blob/main/HISTORY.rst)
- [Commits](https://github.com/PyMySQL/mysqlclient/compare/v2.2.5...v2.2.6)

Updates `ocrmypdf` from 16.6.1 to 16.6.2
- [Release notes](https://github.com/ocrmypdf/OCRmyPDF/releases)
- [Changelog](https://github.com/ocrmypdf/OCRmyPDF/blob/main/docs/release_notes.rst)
- [Commits](https://github.com/ocrmypdf/OCRmyPDF/compare/v16.6.1...v16.6.2)

Updates `setproctitle` from 1.3.3 to 1.3.4
- [Changelog](https://github.com/dvarrazzo/py-setproctitle/blob/master/HISTORY.rst)
- [Commits](https://github.com/dvarrazzo/py-setproctitle/compare/version-1.3.3...version-1.3.4)

Updates `tqdm` from 4.67.0 to 4.67.1
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.67.0...v4.67.1)

---
updated-dependencies:
- dependency-name: django-allauth
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
- dependency-name: channels
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: small-changes
- dependency-name: channels-redis
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: mysqlclient
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: ocrmypdf
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: setproctitle
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: small-changes
- dependency-name: tqdm
  dependency-type: direct:production
  update-type: version-update:semver-patch
  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-12-01 19:12:23 +00:00
shamoonandGitHub dbccd13915 Enhancement: History (audit log) for bulk edit operations (#8196) 2024-12-01 17:53:52 +00:00
shamoon 5a74a92b74 Fix: missing import in frontend tests 2024-11-29 23:01:15 -08:00
shamoonandGitHub d1a4c1f2eb Enhancement: larger previews in action dialogs (#8387) 2024-11-29 22:58:59 -08:00
shamoonandGitHub 9614528033 Enhancement: filterable list count sorting and opacification (#8386) 2024-11-29 22:36:40 -08:00
shamoonandGitHub 548a7f05d8 Enhancement: preview button for document list and trash, refactor (#8384) 2024-11-29 21:24:33 -08:00
shamoon 771b6606e0 Fix: include subpath in logo url (#8374) 2024-11-28 08:57:44 -08:00
shamoonandGitHub c08ca6e3e5 Fixhancement: dispatch change event from current field prior to save (#8369) 2024-11-27 23:25:52 -08:00
shamoonandGitHub a186527f07 Enhancement: use theme-color meta tag (#8359) 2024-11-26 10:24:48 -08:00
shamoon ecb300845c Update frontend translation strings 2024-11-26 09:23:26 -08:00
shamoon cc93bc41df Chore: tiny typo in ci.yml 2024-11-24 12:55:39 -08:00
shamoon c6fdf4409b Fix: include distinct on workflow objects filter 2024-11-24 12:22:48 -08:00
shamoonandGitHub 37f8a77516 Chore: cleanup urls, use actions for some views (#8346) 2024-11-24 12:06:31 -08:00
shamoonandGitHub 2b29233a1e Feature: scheduled workflow trigger (#8036) 2024-11-24 18:22:31 +00:00
Trenton HandGitHub d5572137de Fixes install script to handle languages with dashes or underscores (#8341) 2024-11-23 08:15:13 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d370704286 Chore(deps): Bump tornado from 6.4.1 to 6.4.2 (#8336)
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4.1 to 6.4.2.
- [Changelog](https://github.com/tornadoweb/tornado/blob/v6.4.2/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.4.1...v6.4.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 14:48:51 -08:00
shamoonandGitHub be684c9a11 Fix: handle very old dates with positive offset too (#8335) 2024-11-22 08:39:51 -08:00
shamoonandGitHub 447b4cdb98 Refactor: fix unnecessary use of filterable dropdown sorting (#8328) 2024-11-21 18:50:52 -08:00
shamoonandGitHub f6cc2f9fc3 Enhancement: offer link to restored document (#8321) 2024-11-20 16:51:00 -08:00
shamoon beb69ae01e Update frontend translation strings 2024-11-20 16:42:45 -08:00
shamoonandGitHub 8bfe68743d Enhancement: support owner permissions for file tasks (#8195) 2024-11-20 20:25:53 +00:00
shamoonandGitHub 9c1561adfb Change: change update content to handle archive disabled (#8315) 2024-11-20 20:01:13 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d7d3fed833 Chore(deps): Bump watchdog in the major-versions group (#8257)
Bumps the major-versions group with 1 update: [watchdog](https://github.com/gorakhargosh/watchdog).


Updates `watchdog` from 5.0.3 to 6.0.0
- [Release notes](https://github.com/gorakhargosh/watchdog/releases)
- [Changelog](https://github.com/gorakhargosh/watchdog/blob/master/changelog.rst)
- [Commits](https://github.com/gorakhargosh/watchdog/compare/v5.0.3...v6.0.0)

---
updated-dependencies:
- dependency-name: watchdog
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-versions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 08:50:21 -08:00
Kevin DorenandGitHub 827121808a Enhancement: Add --compare-json option to document_exporter to write json files only if changed (#8261) 2024-11-19 07:20:24 -08:00
shamoonandGitHub f0e71330ac Enhancement: next / previous shortcuts for document list (#8309) 2024-11-18 11:05:00 -08:00
shamoon e4578b4589 Chore: change doc detail icon 2024-11-18 11:03:56 -08:00
shamoonandGitHub e94a92ed59 Feature: two-factor authentication (#8012) 2024-11-18 18:34:46 +00:00
ftibi93andGitHub 6c3d6d562d Documentation: fix docker-compose.portainer.yml GID (#8273) 2024-11-16 04:19:18 +00:00
shamoonandGitHub c324a71e91 Fix: include db_index caveat in squashed migrations (#8292) 2024-11-15 07:42:10 -08:00
shamoonandGitHub 2e6f85cc3a Tweak: use fixed position for navbar (#8279) 2024-11-14 00:22:26 -08:00
shamoon 56ef263540 Fix: fix log line trackby function 2024-11-14 00:05:38 -08:00
shamoonandGitHub 36ecb8587d Fix: prevent duplicate workflow runs (#8268) 2024-11-13 07:18:21 -08:00
shamoon ad9bdaf1b8 Merge branch 'main' into dev 2024-11-12 22:35:42 -08:00
shamoonandGitHub 22f29b3659 Chore: add note about select options to edit dialog (#8267) 2024-11-12 22:25:05 -08:00
shamoon 0dc47d156f Update frontend translation strings 2024-11-12 19:42:39 -08:00
shamoonandGitHub 3633fb382f Enhancement: save & next / close shortcut key (#8243) 2024-11-12 16:21:10 -08:00
shamoonandGitHub a283a65813 Feature: loading preview, better text popup preview (#8011) 2024-11-12 16:20:52 -08:00
Sebastian SteinbeißerandGitHub 74d0c9fda5 Switch src/documents/bulk*.py from os.path to pathlib.Path (#7862)
Also:
* Ensure that the ruff PTH check remains enabled for these files and
all files added in the future.
* Add some type annotations.
2024-11-12 17:04:07 +00:00
Trenton HandGitHub d1f255a22e Chore: Bulk backend dependency updates (#8212) 2024-11-11 11:54:51 -08:00
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>shamoon
ceb834bdef Documentation: Add v2.13.5 changelog (#8248)
* Changelog v2.13.5 - GHA

* Update changelog.md

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-11-10 09:03:05 -08:00
shamoon 6c230f098a Bump version to 2.13.5 2024-11-10 08:25:10 -08:00
shamoon da40d03be6 Merge branch 'dev' 2024-11-10 08:24:31 -08:00
shamoonandGitHub a6f4c75a72 Fix: handle page count exception for pw-protected files (#8240) 2024-11-10 03:33:47 -08:00
shamoonandGitHub c22a80abd3 Fix: correctly track task id in list for change detection (#8230) 2024-11-08 00:19:05 -08:00
Trenton HandGitHub 2806b1820e Updates all runner images to use Ubuntu Noble (#8213) 2024-11-08 02:14:47 +00:00
Trenton HandGitHub 177cc9d985 Fixes admin pages to show trashed documents too (#8068) 2024-11-07 23:04:29 +00:00
shamoonandGitHub da85b05ea4 Fix: tag colors stay fixed in list when selected (#8225) 2024-11-07 11:59:03 -08:00
shamoon 82fd706dca Fix: another Firefox plaintext popup fix 2024-11-06 08:58:15 -08:00
shamoon 2f06680f76 Fix: fix re-activation of save button when adding / removing array items (#8208) 2024-11-05 20:01:36 -08:00
shamoon 64095a710a Fix: Dont check empty ASNs 2024-11-05 20:01:36 -08:00
shamoonandGitHub 005150c84f Fix: fix thumbnail clipping, select inverted color in safari (#8193) 2024-11-04 13:44:35 -08:00
shamoonandGitHub c57cd7e298 Fix: select checkbox should remain visible (#8185) 2024-11-04 04:32:57 -08:00
shamoonandGitHub ef329fc687 Fix: warn with proper error on ASN exists in trash (#8176) 2024-11-03 17:52:59 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
695967cbb2 Chore(deps): Bump stumpylog/image-cleaner-action in the actions group (#8142)
Bumps the actions group with 1 update: [stumpylog/image-cleaner-action](https://github.com/stumpylog/image-cleaner-action).


Updates `stumpylog/image-cleaner-action` from 0.8.0 to 0.9.0
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/v0.8.0...v0.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-03 15:33:12 +00:00
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>
c028910cdd Changelog v2.13.4 - GHA (#8170)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-03 06:27:10 -08:00
shamoon 37f836db2c Merge branch 'dev' 2024-11-03 05:28:57 -08:00
shamoonandGitHub d01192b81e Fix: fix dark mode icon blend mode in 2.13.3 (#8166) 2024-11-03 05:27:20 -08:00
shamoon 9655c89b69 Bump version to 2.13.4 2024-11-03 05:08:08 -08:00
shamoon ccf1430f82 Merge branch 'dev' 2024-11-03 05:07:43 -08:00
shamoonandGitHub c2509ea439 Fix: fix clipped popup preview in 2.13.3 (#8165) 2024-11-03 05:06:39 -08:00
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>
3cd9bcebe6 Changelog v2.13.3 - GHA (#8160)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-02 22:47:15 -07:00
shamoon 2368fd15cb Bump version to 2.13.3 2024-11-02 22:19:03 -07:00
shamoon cc35b321c5 Merge branch 'dev' 2024-11-02 22:18:35 -07:00
1ddbc31c59 New Crowdin translations by GitHub Action (#8159)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-11-02 21:58:06 -07:00
6ae8ab4af3 New Crowdin translations by GitHub Action (#8130)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-11-02 21:56:35 -07:00
shamoonandGitHub 37dc791301 Fix: fix auto-clean PDFs, create parent dir for storing unmodified original (#8157) 2024-11-02 20:54:28 -07:00
Yichi YangandGitHub 5fed311ffc Fix: correctly handle "exists, false" in custom field query filter (#8158) 2024-11-02 18:40:50 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e74f182662 Chore(deps-dev): Bump the frontend-eslint-dependencies group (#8145)
Bumps the frontend-eslint-dependencies group in /src-ui with 4 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), [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) and [eslint](https://github.com/eslint/eslint).


Updates `@typescript-eslint/eslint-plugin` from 8.8.0 to 8.12.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.12.2/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.8.0 to 8.12.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.12.2/packages/parser)

Updates `@typescript-eslint/utils` from 8.8.0 to 8.12.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.12.2/packages/utils)

Updates `eslint` from 9.11.1 to 9.14.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/v9.11.1...v9.14.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: "@typescript-eslint/utils"
  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-11-02 02:44:00 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3f094c88bd Chore(deps-dev): Bump @types/node from 22.7.4 to 22.8.6 in /src-ui (#8148)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.7.4 to 22.8.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-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-01 23:44:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
631eaa2109 Chore(deps-dev): Bump @playwright/test from 1.47.2 to 1.48.2 in /src-ui (#8147)
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.47.2 to 1.48.2.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.47.2...v1.48.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-01 23:35:04 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
27fadf6963 Chore(deps): Bump uuid from 10.0.0 to 11.0.2 in /src-ui (#8146)
Bumps [uuid](https://github.com/uuidjs/uuid) from 10.0.0 to 11.0.2.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v10.0.0...v11.0.2)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-01 23:25:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d53b300a7f Chore(deps): Bump tslib from 2.7.0 to 2.8.1 in /src-ui (#8149)
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.7.0 to 2.8.1.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.7.0...v2.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-01 23:13:13 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6cc626fd86 Chore(deps-dev): Bump @codecov/webpack-plugin in /src-ui (#8150)
Bumps @codecov/webpack-plugin from 1.2.0 to 1.2.1.

---
updated-dependencies:
- dependency-name: "@codecov/webpack-plugin"
  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-11-01 23:01:54 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8b8f1af513 Chore(deps-dev): Bump @types/jest (#8144)
Bumps the frontend-jest-dependencies group in /src-ui with 1 update: [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest).


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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-01 22:45:03 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>shamoon
4226a1bddc Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 21 updates (#8143)
* Chore(deps): Bump the frontend-angular-dependencies group

Bumps the frontend-angular-dependencies group in /src-ui with 21 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `18.2.6` | `18.2.11` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `18.2.6` | `18.2.10` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `18.2.6` | `18.2.10` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `18.2.6` | `18.2.10` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `18.2.6` | `18.2.10` |
| [@angular/localize](https://github.com/angular/angular) | `18.2.6` | `18.2.10` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `18.2.6` | `18.2.10` |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `18.2.6` | `18.2.10` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `18.2.6` | `18.2.10` |
| [@ng-select/ng-select](https://github.com/ng-select/ng-select) | `13.9.0` | `13.9.1` |
| [ng2-pdf-viewer](https://github.com/VadimDez/ng2-pdf-viewer) | `10.3.1` | `10.3.4` |
| [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | `18.2.6` | `18.2.11` |
| [@angular-devkit/core](https://github.com/angular/angular-cli) | `18.2.6` | `18.2.11` |
| [@angular-devkit/schematics](https://github.com/angular/angular-cli) | `18.2.6` | `18.2.11` |
| [@angular-eslint/builder](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/builder) | `18.3.1` | `18.4.0` |
| [@angular-eslint/eslint-plugin](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin) | `18.3.1` | `18.4.0` |
| [@angular-eslint/eslint-plugin-template](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin-template) | `18.3.1` | `18.4.0` |
| [@angular-eslint/schematics](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/schematics) | `18.3.1` | `18.4.0` |
| [@angular-eslint/template-parser](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/template-parser) | `18.3.1` | `18.4.0` |
| [@angular/cli](https://github.com/angular/angular-cli) | `18.2.6` | `18.2.11` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `18.2.6` | `18.2.10` |


Updates `@angular/cdk` from 18.2.6 to 18.2.11
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/18.2.6...18.2.11)

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

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

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

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

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

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

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

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

Updates `@ng-select/ng-select` from 13.9.0 to 13.9.1
- [Release notes](https://github.com/ng-select/ng-select/releases)
- [Changelog](https://github.com/ng-select/ng-select/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ng-select/ng-select/compare/v13.9.0...v13.9.1)

Updates `ng2-pdf-viewer` from 10.3.1 to 10.3.4
- [Release notes](https://github.com/VadimDez/ng2-pdf-viewer/releases)
- [Changelog](https://github.com/VadimDez/ng2-pdf-viewer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/VadimDez/ng2-pdf-viewer/compare/10.3.1...10.3.4)

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

Updates `@angular-devkit/core` from 18.2.6 to 18.2.11
- [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/18.2.6...18.2.11)

Updates `@angular-devkit/schematics` from 18.2.6 to 18.2.11
- [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/18.2.6...18.2.11)

Updates `@angular-eslint/builder` from 18.3.1 to 18.4.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/builder/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.4.0/packages/builder)

Updates `@angular-eslint/eslint-plugin` from 18.3.1 to 18.4.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.4.0/packages/eslint-plugin)

Updates `@angular-eslint/eslint-plugin-template` from 18.3.1 to 18.4.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.4.0/packages/eslint-plugin-template)

Updates `@angular-eslint/schematics` from 18.3.1 to 18.4.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/schematics/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.4.0/packages/schematics)

Updates `@angular-eslint/template-parser` from 18.3.1 to 18.4.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/template-parser/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.4.0/packages/template-parser)

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

Updates `@angular/compiler-cli` from 18.2.6 to 18.2.10
- [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/18.2.10/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular/cdk"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/common"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@ng-select/ng-select"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: ng2-pdf-viewer
  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-devkit/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/schematics"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/builder"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/eslint-plugin-template"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/schematics"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/template-parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/cli"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
...

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

* Change output name

---------

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-11-01 22:32:12 +00:00
shamoonandGitHub 289a7a2348 Documentation: re-add 2.13.0 changelog (#8135) 2024-10-31 12:18:32 -07:00
horvatkmandGitHub 702ab1b77a Chore: update field in bug-report.yml (#8123) 2024-10-30 22:07:35 +00:00
shamoonandGitHub e22ccbda26 Fix: dont use filters for inverted thumbnails (#8121) 2024-10-30 11:43:02 -07:00
shamoonandGitHub 56d296f04b Fix: use static object for activedisplayfields to prevent changes (#8120) 2024-10-30 11:21:01 -07:00
shamoonandGitHub 11cfa0871e Fix: dont invert pdf colors in FF (#8110) 2024-10-30 00:54:19 -07:00
shamoonandGitHub 159344f033 Fix: make mail account password and refresh token text fields (#8107) 2024-10-29 21:54:47 -07:00
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>
d8cfed5f5e Changelog v2.13.2 - GHA (#8103)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-10-29 12:45:11 -07:00
shamoon 94fe7a9e3d Bump version to 2.13.2 2024-10-29 11:09:19 -07:00
shamoon 9c9b4effe2 Merge branch 'dev' 2024-10-29 11:08:50 -07:00
75f5007ede New Crowdin translations by GitHub Action (#8101)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-10-29 11:07:03 -07:00
d95baf4e6b New Crowdin translations by GitHub Action (#8093)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-10-29 11:05:08 -07:00
shamoonandGitHub aac04e73b9 Fix: correct serializing of auth tokens for export (#8100) 2024-10-29 17:02:32 +00:00
shamoonandGitHub 3af3484a00 Fix: cf query dropdown styling affecting other components (#8095) 2024-10-28 23:14:11 -07:00
90e68af6cf Documentation: Add v2.13.1 changelog (#8085)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-10-28 12:44:21 -07:00
shamoon eee08d389f Bump version to 2.13.1 2024-10-28 11:27:57 -07:00
b3b0e95d2d New Crowdin translations by GitHub Action (#8083)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-10-28 11:00:35 -07:00
shamoonandGitHub 35907313e8 Fix: allow removing dead document links from UI, validate via API (#8081) 2024-10-28 10:39:17 -07:00
d4a20c7e30 New Crowdin translations by GitHub Action (#8080)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-10-28 09:00:31 -07:00
shamoon 3b1dffe0dc Fix: dont display trashed docs in doc links 2024-10-28 08:57:38 -07:00
shamoon fa0ab0de27 Merge branch 'main' into dev 2024-10-28 08:42:07 -07:00
Trenton HandGitHub 3b2b4a9177 Removes whitenoise patches and upgrades it to 6.8.1 (#8079) 2024-10-28 15:34:26 +00:00
3d030637ca New Crowdin translations by GitHub Action (#8025)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-10-28 07:03:42 -07:00
shamoonandGitHub 79092c27c5 Fix: Make customfieldinstance soft delete, fix filepath when deleted (#8067) 2024-10-28 14:02:09 +00:00
shamoonandGitHub 28fdb170bf Fix: handle uuid fields created under mariadb and Django 4 (#8034) 2024-10-28 13:54:16 +00:00
Trenton HandGitHub 335c6c3820 Fix: Update filename correctly if the document is in the trash (#8066)
* Fixes an issue where the filename is not updated if the document is in the trash (but the file is moved)
2024-10-28 02:45:31 +00:00
Trenton HandGitHub ad23cce2e6 Handle a special case where the none marker exists in a way which could create an absolute path (#8060) 2024-10-28 01:51:07 +00:00
shamoonandGitHub 0d96cd03d5 Fix: disable custom field signals during import in 2.13.0 (#8065) 2024-10-27 18:43:24 -07:00
shamoonandGitHub 1888ee6a3f Fix: doc link documents search should exclude null (#8064) 2024-10-27 17:13:29 -07:00
605aa50b00 Documentation: Fix list indentation (#8050)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-10-28 00:02:06 +00:00
shamoonandGitHub 149d770ad1 Enhancement: auto-update document filenames with CF select fields (#8045) 2024-10-27 23:45:21 +00:00
shamoonandGitHub b2e9f3195a Fix: fix custom field query empty element removal (#8056) 2024-10-27 06:53:59 -07:00
shamoonandGitHub 33e9990ed5 Chore: fix changelog script (#8022) 2024-10-27 03:36:12 +00:00
shamoonandGitHub e775b6346a Fix: dont try to load PAPERLESS_MODEL_FILE as env from file (#8040) 2024-10-26 22:29:20 +00:00
shamoonandGitHub 54e17f5b74 Documentation: fix docs index page on mobile (#8035) 2024-10-26 11:36:40 -07:00
shamoonandGitHub ff1639d58b Fix: dont include all allauth urls (#8010) 2024-10-26 15:06:22 +00:00
shamoonandGitHub 7649903d3c Enhancement / fix: include social accounts and api tokens in export (#8016) 2024-10-26 06:51:22 -07:00
7a5d707fc0 Documentation: Resolve display and link errors in changelog (#8019)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-10-26 01:13:22 -07:00
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>shamoon
85e00aecb4 Documentation: Add v2.13.0 changelog (#8009)
* Changelog v2.13.0 - GHA

* Update changelog.md

* Update changelog.md

* Update changelog.md

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-10-26 01:00:19 -07:00
shamoonandGitHub 53aa216a4a Fix: oauth settings without base url (#8020) 2024-10-26 00:57:36 -07:00
shamoonandGitHub 2814cd110d Merge pull request #7902 from paperless-ngx/beta
[Beta] Paperless-ngx v2.13.0 Beta Release
2024-10-25 10:13:40 -07:00
shamoon b9315b018a Merge branch 'dev' into beta 2024-10-25 09:33:44 -07:00
27f7ba8cf8 New Crowdin translations by GitHub Action (#8008)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-10-25 09:33:22 -07:00
shamoon df9917b0f4 Merge branch 'dev' into beta 2024-10-25 09:18:11 -07:00
86418f6e04 New Crowdin translations by GitHub Action (#7911)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-10-25 09:17:18 -07:00
shamoonandGitHub 69a6a12319 Correct repo maintenance documentation 2024-10-25 09:16:24 -07:00
shamoon b501d89846 Update repo maintenance rules 2024-10-24 16:15:59 -07:00
shamoonandshamoon f6548e0e55 Enhancement: automatically refresh display list after custom field edit 2024-10-22 09:19:22 -07:00
shamoon a4b8bf1250 Fix: prevent focus error on custom field edit when switching from select 2024-10-20 19:19:30 -07:00
shamoon 1726aec989 Enhancement: automatically refresh display list after custom field edit 2024-10-20 19:17:18 -07:00
shamoon 549312859e Merge branch 'dev' into beta 2024-10-20 19:06:52 -07:00
Trenton HandGitHub 544e9c4fe2 Use a TextField for the storage path field (#7967) 2024-10-20 11:23:46 -07:00
shamoon 0520db5e93 Update translation strings 2024-10-19 22:56:57 -07:00
shamoon 1cb85d41f3 Change: dont show remove button at all from doc link dropdowns if disabled 2024-10-19 17:23:37 -07:00
tooommandGitHub fb94a5d377 Fix: remove space before my profile button in dropdown (#7963) 2024-10-19 17:17:53 -07:00
shamoon 85e2081e40 Merge branch 'dev' into beta 2024-10-18 22:36:27 -07:00
shamoonandGitHub 71e2565386 Enhancement: auto-focus default select field in custom field dropdown (#7961) 2024-10-18 22:35:57 -07:00
shamoon 82be90f7ff Merge branch 'dev' into beta 2024-10-17 14:33:05 -07:00
shamoon f0ad073bb2 Fix: document link field consistent behavior with insufficient permissions (#7953) 2024-10-17 14:32:56 -07:00
shamoon 61c804a6e3 Merge branch 'dev' into beta 2024-10-16 16:57:08 -07:00
shamoonandGitHub de95b296a0 Change: open not edit (#7942) 2024-10-16 16:56:40 -07:00
shamoon 86a57838a8 Fix: tweak icon placement in small doc card buttons 2024-10-16 16:39:20 -07:00
shamoon bddb9bfad8 Update preview-popup.component.scss 2024-10-15 21:58:25 -07:00
shamoon 7098ec9bf5 Merge branch 'dev' into beta 2024-10-15 10:56:53 -07:00
Trenton HandGitHub c2cfaaf8af Fix: Handling of Nones when using custom fields in filepath templating (#7933) 2024-10-15 17:54:15 +00:00
shamoonandGitHub 6292296876 Fix: trigger move and rename after custom fields saved (#7927) 2024-10-15 10:08:50 -07:00
shamoon 9f68e0f76a Merge branch 'dev' into beta 2024-10-14 07:57:01 -07:00
shamoonandGitHub 4e849b545a Fix: v2.13.0 RC1: increase field max lengths to accommodate larger tokens (#7916) 2024-10-14 07:56:47 -07:00
shamoon e43fee41cb Merge branch 'main' into beta 2024-10-14 07:56:21 -07:00
shamoon 613f8a0065 Bump version to 2.13.0 2024-10-14 07:56:14 -07:00
shamoon baf6484454 Chore: include beta in tagged version display 2024-10-14 07:55:03 -07:00
shamoonandGitHub 9b84dc06b6 Enhancement: support retain barcode split pages (#7912) 2024-10-13 20:51:39 -07:00
shamoonandGitHub cb617531bc Fix: preserve text linebreaks in doc edit (#7908) 2024-10-13 10:45:05 -07:00
shamoon 8e61a29137 Chore: dont run repo maintenance in forks 2024-10-13 10:44:13 -07:00
tooommandGitHub dfcecb3a5c Chore: do not run Crowdin action on forks (#7904) 2024-10-13 08:37:41 -07:00
0a61b8e6fc New Crowdin translations by GitHub Action (#7728)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-10-12 20:58:10 -07:00
shamoon e78d758656 Fix: resolve some silly frontend test things 2024-10-10 20:38:24 -07:00
shamoonandGitHub 073c42984a Enhancement: dont wait to get preview (#7894) 2024-10-10 19:43:13 -07:00
shamoonandGitHub 2994f3a740 Fix: only show colon on cards if correspondent and title shown (#7893) 2024-10-10 15:07:52 -07:00
shamoonandGitHub 2353f7c2db Feature: OAuth2 Gmail and Outlook email support (#7866) 2024-10-10 20:57:32 +00:00
shamoonandGitHub dcc8d4046a Chore: Unify workflow logic (#7880) 2024-10-10 20:28:44 +00:00
shamoonandGitHub 024b60638a Feature: live preview of storage path (#7870) 2024-10-09 23:35:36 +00:00
shamoon 8dd355f6bf Chore: fix test comments 2024-10-08 23:36:09 -07:00
Trenton HandGitHub cf3645c296 Fixes the ASN checking to allow an ASN of 0 (#7878) 2024-10-08 12:47:37 -07:00
shamoonandGitHub facec317ef Fix: fix auto-dismiss completed tasks on open document (#7869) 2024-10-07 07:25:52 -07:00
shamoonandGitHub 95d1abd416 Fix: trigger change warning for saved views with default fields if changed (#7865) 2024-10-06 14:27:02 -07:00
7c11a37150 Feature: Enhanced templating for filename format (#7836)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-10-06 12:54:01 -07:00
shamoon e49ed58f1a Fix: skip accounts without enabled rules 2024-10-04 23:59:31 -07:00
shamoonandGitHub 54293bedb1 Enhancement: management list button improvements (#7848) 2024-10-03 23:00:28 -07:00
shamoon fc683e150a Add missing interface to resolve test warning 2024-10-02 20:21:47 -07:00
b3487f1843 Enhancement: check for mail destination directory, log post-consume errors (#7808)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-10-02 20:21:35 -07:00
shamoonandGitHub f8d79b012f Feature: custom fields queries (#7761) 2024-10-03 00:15:42 +00:00
shamoon 2e3637d712 Update .codecov.yml 2024-10-01 21:37:35 -07:00
shamoon 74001bd0da Fix: wrap table header columns in row (#7832) 2024-10-01 17:37:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
374a1ceb05 Chore(deps-dev): Bump @codecov/webpack-plugin in /src-ui (#7830)
Bumps @codecov/webpack-plugin from 1.0.1 to 1.2.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 21:38:03 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
59f726b2a2 Chore(deps-dev): Bump @types/node from 22.5.2 to 22.7.4 in /src-ui (#7829)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.5.2 to 22.7.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 21:29:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
77bebc861d Chore(deps-dev): Bump the frontend-eslint-dependencies group (#7827)
Bumps the frontend-eslint-dependencies group in /src-ui with 4 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), [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) and [eslint](https://github.com/eslint/eslint).


Updates `@typescript-eslint/eslint-plugin` from 8.3.0 to 8.8.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/v8.8.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.3.0 to 8.8.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/v8.8.0/packages/parser)

Updates `@typescript-eslint/utils` from 8.3.0 to 8.8.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.8.0/packages/utils)

Updates `eslint` from 9.9.1 to 9.11.1
- [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/v9.9.1...v9.11.1)

---
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: "@typescript-eslint/utils"
  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-10-01 21:20:53 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
85e57ede9b Chore(deps-dev): Bump the frontend-jest-dependencies group (#7826)
Bumps the frontend-jest-dependencies group in /src-ui with 2 updates: [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) and [jest-preset-angular](https://github.com/thymikee/jest-preset-angular).


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

Updates `jest-preset-angular` from 14.2.2 to 14.2.4
- [Release notes](https://github.com/thymikee/jest-preset-angular/releases)
- [Changelog](https://github.com/thymikee/jest-preset-angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/thymikee/jest-preset-angular/compare/v14.2.2...v14.2.4)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-jest-dependencies
- dependency-name: jest-preset-angular
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-jest-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 21:10:16 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a7424a7bfe Chore(deps-dev): Bump @playwright/test from 1.46.1 to 1.47.2 in /src-ui (#7828)
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.46.1 to 1.47.2.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.46.1...v1.47.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 13:59:11 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
46b8e536a8 Chore(deps): Bump the frontend-angular-dependencies group (#7825)
Bumps the frontend-angular-dependencies group in /src-ui with 21 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `18.2.2` | `18.2.6` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `18.2.2` | `18.2.6` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `18.2.2` | `18.2.6` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `18.2.2` | `18.2.6` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `18.2.2` | `18.2.6` |
| [@angular/localize](https://github.com/angular/angular) | `18.2.2` | `18.2.6` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `18.2.2` | `18.2.6` |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `18.2.2` | `18.2.6` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `18.2.2` | `18.2.6` |
| [@ng-select/ng-select](https://github.com/ng-select/ng-select) | `13.7.0` | `13.9.0` |
| [ng2-pdf-viewer](https://github.com/VadimDez/ng2-pdf-viewer) | `10.3.0` | `10.3.1` |
| [@angular-devkit/build-angular](https://github.com/angular/angular-cli) | `18.2.2` | `18.2.6` |
| [@angular-devkit/core](https://github.com/angular/angular-cli) | `18.2.2` | `18.2.6` |
| [@angular-devkit/schematics](https://github.com/angular/angular-cli) | `18.2.2` | `18.2.6` |
| [@angular-eslint/builder](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/builder) | `18.3.0` | `18.3.1` |
| [@angular-eslint/eslint-plugin](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin) | `18.3.0` | `18.3.1` |
| [@angular-eslint/eslint-plugin-template](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin-template) | `18.3.0` | `18.3.1` |
| [@angular-eslint/schematics](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/schematics) | `18.3.0` | `18.3.1` |
| [@angular-eslint/template-parser](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/template-parser) | `18.3.0` | `18.3.1` |
| [@angular/cli](https://github.com/angular/angular-cli) | `18.2.2` | `18.2.6` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `18.2.2` | `18.2.6` |


Updates `@angular/cdk` from 18.2.2 to 18.2.6
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/18.2.2...18.2.6)

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

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

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

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

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

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

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

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

Updates `@ng-select/ng-select` from 13.7.0 to 13.9.0
- [Release notes](https://github.com/ng-select/ng-select/releases)
- [Changelog](https://github.com/ng-select/ng-select/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ng-select/ng-select/compare/v13.7.0...v13.9.0)

Updates `ng2-pdf-viewer` from 10.3.0 to 10.3.1
- [Release notes](https://github.com/VadimDez/ng2-pdf-viewer/releases)
- [Changelog](https://github.com/VadimDez/ng2-pdf-viewer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/VadimDez/ng2-pdf-viewer/compare/10.3.0...10.3.1)

Updates `@angular-devkit/build-angular` from 18.2.2 to 18.2.6
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/18.2.2...18.2.6)

Updates `@angular-devkit/core` from 18.2.2 to 18.2.6
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/18.2.2...18.2.6)

Updates `@angular-devkit/schematics` from 18.2.2 to 18.2.6
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/18.2.2...18.2.6)

Updates `@angular-eslint/builder` from 18.3.0 to 18.3.1
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/builder/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/builder)

Updates `@angular-eslint/eslint-plugin` from 18.3.0 to 18.3.1
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/eslint-plugin)

Updates `@angular-eslint/eslint-plugin-template` from 18.3.0 to 18.3.1
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/eslint-plugin-template)

Updates `@angular-eslint/schematics` from 18.3.0 to 18.3.1
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/schematics/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/schematics)

Updates `@angular-eslint/template-parser` from 18.3.0 to 18.3.1
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/template-parser/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v18.3.1/packages/template-parser)

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

Updates `@angular/compiler-cli` from 18.2.2 to 18.2.6
- [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/18.2.6/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular/cdk"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/common"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@ng-select/ng-select"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: ng2-pdf-viewer
  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-devkit/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/schematics"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/builder"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/eslint-plugin-template"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/schematics"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/template-parser"
  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
- dependency-name: "@angular/compiler-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-10-01 13:43:28 -07:00
Trenton HandGitHub 2ab71137b9 Chore: Upgrades OCRMyPDF to v16 (#7815) 2024-10-01 02:53:44 +00:00
shamoonandGitHub 0b829cab32 Enhancement: workflow overview toggle enable button (#7818) 2024-09-30 19:44:02 -07:00
shamoonandGitHub 991c9b0ca4 Enhancement: disable-able mail rules, add toggle to overview (#7810) 2024-09-30 19:42:19 -07:00
b9c1ba8a1d Documentation: Mention the Redis licensing and its forks (#7817)
* Mention the Redis licensing and its forks

* Note date

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-10-01 01:58:07 +00:00
Martin RichtarskyandGitHub c9e33a3401 Documentation: update development docker build steps (#7806) 2024-09-30 14:56:24 +00:00
Trenton HandGitHub dd9b10bdf8 Upgrades the Docker image to use Python 3.12 (#7796) 2024-09-28 18:42:55 +00:00
Trenton HandGitHub 546fd2740b Chore: Upgrade Django to 5.1 (#7795)
* Upgrades Django to 5.1
2024-09-28 18:06:13 +00:00
shamoon 56e1365b4b Fix page_count migration 2024-09-27 21:47:30 -07:00
Trenton HandGitHub e6f59472e4 Chore: Drop Python 3.9 support (#7774) 2024-09-26 12:22:24 -07:00
shamoonandGitHub 5e687d9a93 Feature: auto-clean some invalid pdfs (#7651) 2024-09-25 15:57:20 +00:00
c92c3e224a Feature: page count (#7750)
---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-09-25 08:22:12 -07:00
shamoonandGitHub 4adf20af1e Fix: hidden canvas element causes scroll bug (#7770) 2024-09-24 12:26:48 -07:00
gawa971andGitHub a9b7965dcf Enhancement: use apt only when needed docker-entrypoint.sh (#7756) 2024-09-23 23:46:03 -07:00
shamoonandYichi Yang d7ba6d98d3 Feature: Enhanced backend custom field search API (#7589)
commit 910dae8413028f647e6295f30207cb5d4fc6605d
Author: Yichi Yang <yiy067@ucsd.edu>
Date:   Wed Sep 4 12:47:19 2024 -0700

    Fix: correctly handle the case where custom_field_lookup refers to multiple fields

commit e43f70d708b7d6b445f3ca8c8bf9dbdf5ee26085
Author: Yichi Yang <yiy067@ucsd.edu>
Date:   Sat Aug 31 14:06:45 2024 -0700

Co-Authored-By: Yichi Yang <yichiyan@usc.edu>
2024-09-23 23:33:49 -07:00
MatthieuandGitHub f6135f9ad0 Documentation: fix link to redis security docs (#7766) 2024-09-23 14:45:35 -07:00
shamoon f06ff85b7d Simplify this a bit more 2024-09-23 10:46:20 -07:00
shamoonandGitHub 1b7cacc877 Enhancement: compactify dates dropdown (#7759) 2024-09-23 10:30:13 -07:00
shamoonandGitHub 870d6ee782 Fix: handle overflowing dropdowns on mobile (#7758)
See https://github.com/ng-bootstrap/ng-bootstrap/pull/4760
2024-09-23 10:29:37 -07:00
shamoon 3aba68c09f Fix sidebar mobile width 2024-09-23 10:27:16 -07:00
shamoonandGitHub 609fa9a212 Enhancement: set Django SESSION_EXPIRE_AT_BROWSER_CLOSE from PAPERLESS_ACCOUNT_SESSION_REMEMBER (#7748) 2024-09-20 10:36:40 -07:00
shamoonandGitHub 16069cde23 Documentation: fix session cookie config type 2024-09-19 19:20:13 -07:00
shamoonandGitHub a440c88b81 Enhancement: allow setting session cookie age (#7743) 2024-09-19 18:58:40 -07:00
shamoon 97030a807f Merge branch 'main' into dev 2024-09-19 16:24:34 -07:00
shamoon 4146b140d3 Documentation: clarify session remember 2024-09-19 07:13:37 -07:00
shamoonandGitHub fa6f013db5 Fix: chrome scrolling in >= 129 (#7738) 2024-09-18 17:09:42 -07:00
shamoonandGitHub 6192c15c4d Feature: copy workflows and mail rules, improve layout (#7727) 2024-09-16 22:02:51 -07:00
shamoon 8aa35540b5 Fix a random test error 2024-09-16 19:36:44 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>shamoon
e787055294 Chore(deps-dev): Bump the development group with 2 updates (#7723)
* Chore(deps-dev): Bump the development group with 2 updates

Bumps the development group with 2 updates: [ruff](https://github.com/astral-sh/ruff) and [pytest](https://github.com/pytest-dev/pytest).


Updates `ruff` from 0.6.4 to 0.6.5
- [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/0.6.4...0.6.5)

Updates `pytest` from 8.3.2 to 8.3.3
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.3)

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

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

* Update .pre-commit-config.yaml

---------

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-09-16 23:25:33 +00:00
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>
045b62ca66 Changelog v2.12.1 - GHA (#7715)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-09-15 20:31:11 -07:00
shamoon 3b7fdb2f37 Bump version to 2.12.1 2024-09-15 20:08:24 -07:00
shamoon bd1f05df24 Merge branch 'dev' 2024-09-15 20:07:59 -07:00
eeeec498d4 New Crowdin translations by GitHub Action (#7668)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-09-15 19:40:35 -07:00
shamoonandGitHub 0af2b967e4 Fix: wait to apply tag changes until other changes saved with multiple workflows (#7711) 2024-09-16 01:26:24 +00:00
shamoonandGitHub 4193401be7 Fix: delete_pages should require ownership (#7714) 2024-09-15 16:24:40 -07:00
shamoonandGitHub 36df6fd3e5 Enhancement: improve text contrast for selected documents in list view dark mode (#7712) 2024-09-15 11:40:59 -07:00
shamoonandGitHub 86a540e68e Fix: filter out shown custom fields that have been deleted from saved view edit (#7710) 2024-09-15 08:02:38 -07:00
shamoon fb3a881387 Refactor: allow filterpipe to specify key 2024-09-13 22:21:38 -07:00
shamoonandGitHub 8e555cce9e Fix: only filter by string or number properties for filter pipe (#7699) 2024-09-13 20:41:31 -07:00
shamoonandGitHub 4f8e59030e Documentation: move usually-unneeded 'mime-support' from bare-metal docs (#7689) 2024-09-11 12:58:53 -07:00
shamoon 5075d0bab0 Documentation: missed incorrect gpg decryptor reference 2024-09-11 10:53:52 -07:00
shamoonandGitHub fb3a136b32 Documentation: fix PAPERLESS_ENABLE_GPG_DECRYPTOR (#7688) 2024-09-11 10:39:41 -07:00
shamoon 66a8057e31 Fix: fix display of permissions filter users dropdown 2024-09-11 09:47:48 -07:00
shamoon cb6cf7f771 Chore: Add codecov bundle analysis (#7673) 2024-09-10 17:55:12 -07:00
shamoon 9a7f95865f Chore: mark some more tests as flaky 2024-09-10 16:41:11 -07:00
shamoondependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0d1e0bc70e Chore(deps): Bump express and related dependencies in /src-ui (#7674)
* Chore(deps): Bump send and express in /src-ui

Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.19.2 to 4.20.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

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

* Chore(deps): Bump body-parser and express in /src-ui (#7676)

Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

Updates `express` from 4.19.2 to 4.20.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

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

* Chore(deps): Bump serve-static and express in /src-ui (#7677)

Bumps [serve-static](https://github.com/expressjs/serve-static) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `serve-static` from 1.15.0 to 1.16.0
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...1.16.0)

Updates `express` from 4.19.2 to 4.20.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-10 14:02:33 -07:00
shamoonandGitHub bee963c23d Fix: saved view permissions fixes (#7672) 2024-09-10 13:33:19 -07:00
shamoonandGitHub f1559b7108 Fix: add permissions to OPTIONS requests for notes (#7661) 2024-09-09 08:34:21 -07:00
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>shamoon
a64a182fc3 Documentation: Add v2.12.0 changelog (#7659)
---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2024-09-08 18:37:26 -07:00
578 changed files with 91834 additions and 49180 deletions
+11 -3
View File
@@ -14,6 +14,9 @@ flag_management:
# codecov will only comment if coverage changes
comment:
require_changes: true
# https://docs.codecov.com/docs/javascript-bundle-analysis
require_bundle_changes: true
bundle_change_threshold: "50Kb"
coverage:
status:
project:
@@ -22,7 +25,12 @@ coverage:
threshold: 1%
patch:
default:
# For the changed lines only, target 75% covered, but
# allow as low as 50%
target: 75%
# For the changed lines only, target 100% covered, but
# allow as low as 75%
target: 100%
threshold: 25%
# https://docs.codecov.com/docs/javascript-bundle-analysis
bundle_analysis:
# Fail if the bundle size increases by more than 1MB
warning_threshold: "1MB"
status: true
+1 -1
View File
@@ -98,7 +98,7 @@ body:
label: Browser
description: Which browser you are using, if relevant.
placeholder: e.g. Chrome, Safari
- type: input
- type: textarea
id: config-changes
attributes:
label: Configuration changes
+45 -25
View File
@@ -16,9 +16,9 @@ on:
env:
# This is the version of pipenv all the steps will use
# If changing this, change Dockerfile
DEFAULT_PIP_ENV_VERSION: "2024.0.1"
DEFAULT_PIP_ENV_VERSION: "2024.4.0"
# This is the default version of Python to use in most steps which aren't specific
DEFAULT_PYTHON_VERSION: "3.10"
DEFAULT_PYTHON_VERSION: "3.11"
jobs:
pre-commit:
@@ -30,7 +30,7 @@ jobs:
github.repository
name: Linting Checks
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
-
name: Checkout repository
@@ -46,7 +46,7 @@ jobs:
documentation:
name: "Build & Deploy Documentation"
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- pre-commit
steps:
@@ -95,12 +95,12 @@ jobs:
tests-backend:
name: "Backend Tests (Python ${{ matrix.python-version }})"
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- pre-commit
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
python-version: ['3.10', '3.11', '3.12']
fail-fast: false
steps:
-
@@ -170,7 +170,7 @@ jobs:
install-frontend-depedendencies:
name: "Install Frontend Dependencies"
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- pre-commit
steps:
@@ -201,7 +201,7 @@ jobs:
tests-frontend:
name: "Frontend Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- install-frontend-depedendencies
strategy:
@@ -260,8 +260,8 @@ jobs:
retention-days: 7
tests-coverage-upload:
name: "Upload Coverage"
runs-on: ubuntu-22.04
name: "Upload to Codecov"
runs-on: ubuntu-24.04
needs:
- tests-backend
- tests-frontend
@@ -283,7 +283,7 @@ jobs:
merge-multiple: true
-
name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
# not required for public repos, but intermittently fails otherwise
token: ${{ secrets.CODECOV_TOKEN }}
@@ -299,17 +299,41 @@ jobs:
path: src/
-
name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
# not required for public repos, but intermittently fails otherwise
token: ${{ secrets.CODECOV_TOKEN }}
# future expansion
flags: backend
directory: src/
-
name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'npm'
cache-dependency-path: 'src-ui/package-lock.json'
-
name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.npm
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
-
name: Re-link Angular cli
run: cd src-ui && npm link @angular/cli
-
name: Build frontend and upload analysis
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: cd src-ui && ng build --configuration=production
build-docker-image:
name: Build Docker image for ${{ github.ref_name }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
concurrency:
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
@@ -382,7 +406,7 @@ jobs:
-
name: Login to Docker Hub
uses: docker/login-action@v3
# Don't attempt to login is not pushing to Docker Hub
# Don't attempt to login if not pushing to Docker Hub
if: steps.push-other-places.outputs.enable == 'true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -390,7 +414,7 @@ jobs:
-
name: Login to Quay.io
uses: docker/login-action@v3
# Don't attempt to login is not pushing to Quay.io
# Don't attempt to login if not pushing to Quay.io
if: steps.push-other-places.outputs.enable == 'true'
with:
registry: quay.io
@@ -437,7 +461,7 @@ jobs:
needs:
- build-docker-image
- documentation
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
-
name: Checkout
@@ -458,12 +482,6 @@ jobs:
name: Install Python dependencies
run: |
pipenv --python ${{ steps.setup-python.outputs.python-version }} sync --dev
-
name: Patch whitenoise
run: |
curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch
patch -d $(pipenv --venv)/lib/python3.10/site-packages --verbose -p2 < 484.patch
rm 484.patch
-
name: Install system dependencies
run: |
@@ -551,7 +569,7 @@ jobs:
publish-release:
name: "Publish Release"
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
outputs:
prerelease: ${{ steps.get_version.outputs.prerelease }}
changelog: ${{ steps.create-release.outputs.body }}
@@ -601,7 +619,7 @@ jobs:
append-changelog:
name: "Append Changelog"
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- publish-release
if: needs.publish-release.outputs.prerelease == 'false'
@@ -631,7 +649,9 @@ jobs:
git checkout ${{ needs.publish-release.outputs.version }}-changelog
echo -e "# Changelog\n\n${{ needs.publish-release.outputs.changelog }}\n" > changelog-new.md
echo "Manually linking usernames"
sed -i -r 's|@(.+?) \(\[#|[@\1](https://github.com/\1) ([#|ig' changelog-new.md
sed -i -r 's|@([a-zA-Z0-9_]+) \(\[#|[@\1](https://github.com/\1) ([#|g' changelog-new.md
echo "Removing unneeded comment tags"
sed -i -r 's|@<!---->|@|g' changelog-new.md
CURRENT_CHANGELOG=`tail --lines +2 changelog.md`
echo -e "$CURRENT_CHANGELOG" >> changelog-new.md
mv changelog-new.md changelog.md
+4 -4
View File
@@ -21,7 +21,7 @@ jobs:
cleanup-images:
name: Cleanup Image Tags for ${{ matrix.primary-name }}
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@@ -33,7 +33,7 @@ jobs:
-
name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
@@ -47,7 +47,7 @@ jobs:
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- cleanup-images
strategy:
@@ -61,7 +61,7 @@ jobs:
-
name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.8.0
uses: stumpylog/image-cleaner-action/untagged@v0.9.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
+1 -1
View File
@@ -23,7 +23,7 @@ on:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
+2 -1
View File
@@ -15,7 +15,8 @@ on:
jobs:
synchronize-with-crowdin:
name: Crowdin Sync
runs-on: ubuntu-latest
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- name: Checkout
+1 -1
View File
@@ -15,7 +15,7 @@ permissions:
jobs:
pr_opened_or_reopened:
name: pr_opened_or_reopened
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
# write permission is required for autolabeler
pull-requests: write
+22 -9
View File
@@ -16,7 +16,8 @@ concurrency:
jobs:
stale:
name: 'Stale'
runs-on: ubuntu-latest
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/stale@v9
with:
@@ -31,7 +32,8 @@ jobs:
for your contributions. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
lock-threads:
name: 'Lock Old Threads'
runs-on: ubuntu-latest
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: dessant/lock-threads@v5
with:
@@ -56,7 +58,8 @@ jobs:
See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
close-answered-discussions:
name: 'Close Answered Discussions'
runs-on: ubuntu-latest
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v7
with:
@@ -112,7 +115,8 @@ jobs:
}
close-outdated-discussions:
name: 'Close Outdated Discussions'
runs-on: ubuntu-latest
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v7
with:
@@ -203,7 +207,8 @@ jobs:
}
close-unsupported-feature-requests:
name: 'Close Unsupported Feature Requests'
runs-on: ubuntu-latest
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v7
with:
@@ -212,15 +217,20 @@ jobs:
return new Promise(resolve => setTimeout(resolve, ms));
}
const CUTOFF_MAX_COUNT = 80;
const CUTOFF_1_DAYS = 180;
const CUTOFF_1_COUNT = 5;
const CUTOFF_2_DAYS = 365;
const CUTOFF_2_COUNT = 10;
const CUTOFF_2_COUNT = 20;
const CUTOFF_3_DAYS = 730;
const CUTOFF_3_COUNT = 40;
const cutoff1Date = new Date();
cutoff1Date.setDate(cutoff1Date.getDate() - CUTOFF_1_DAYS);
const cutoff2Date = new Date();
cutoff2Date.setDate(cutoff2Date.getDate() - CUTOFF_2_DAYS);
const cutoff3Date = new Date();
cutoff3Date.setDate(cutoff3Date.getDate() - CUTOFF_3_DAYS);
const query = `query(
$owner:String!,
@@ -250,9 +260,12 @@ jobs:
const result = await github.graphql(query, variables);
for (const discussion of result.repository.discussions.nodes) {
const discussionDate = new Date(discussion.updatedAt);
if ((discussionDate < cutoff1Date && discussion.upvoteCount < CUTOFF_1_COUNT) ||
(discussionDate < cutoff2Date && discussion.upvoteCount < CUTOFF_2_COUNT)) {
const discussionUpdatedDate = new Date(discussion.updatedAt);
const discussionCreatedDate = new Date(discussion.createdAt);
if ((discussionUpdatedDate < cutoff1Date && discussion.upvoteCount < CUTOFF_MAX_COUNT) ||
(discussionCreatedDate < cutoff1Date && discussion.upvoteCount < CUTOFF_1_COUNT) ||
(discussionCreatedDate < cutoff2Date && discussion.upvoteCount < CUTOFF_2_COUNT) ||
(discussionCreatedDate < cutoff3Date && discussion.upvoteCount < CUTOFF_3_COUNT)) {
console.log(`Closing discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt} with votes ${discussion.upvoteCount}`);
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
+8 -3
View File
@@ -5,7 +5,7 @@
repos:
# General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-docstring-first
- id: check-json
@@ -32,7 +32,7 @@ repos:
rev: v2.3.0
hooks:
- id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)|(^src/paperless_einvoice/tests/samples/)|(^src/paperless_einvoice/templates/)"
exclude_types:
- pofile
- json
@@ -46,9 +46,12 @@ repos:
- ts
- markdown
exclude: "(^Pipfile\\.lock$)"
additional_dependencies:
- prettier@3.3.3
- 'prettier-plugin-organize-imports@4.1.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.6.4'
rev: v0.8.3
hooks:
- id: ruff
- id: ruff-format
@@ -62,6 +65,8 @@ repos:
rev: v6.2.1
hooks:
- id: beautysh
additional_dependencies:
- setuptools
args:
- "--tab"
- repo: https://github.com/shellcheck-py/shellcheck-py
-16
View File
@@ -1,16 +0,0 @@
{
# https://prettier.io/docs/en/options.html#semicolons
"semi": false,
# https://prettier.io/docs/en/options.html#quotes
"singleQuote": true,
# https://prettier.io/docs/en/options.html#trailing-commas
"trailingComma": "es5",
"overrides": [
{
"files": ["index.md", "administration.md"],
"options": {
"tabWidth": 4
}
}
]
}
+19
View File
@@ -0,0 +1,19 @@
const config = {
// https://prettier.io/docs/en/options.html#semicolons
semi: false,
// https://prettier.io/docs/en/options.html#quotes
singleQuote: true,
// https://prettier.io/docs/en/options.html#trailing-commas
trailingComma: 'es5',
overrides: [
{
files: ['docs/*.md'],
options: {
tabWidth: 4,
},
},
],
plugins: [require('prettier-plugin-organize-imports')],
}
module.exports = config
+1 -1
View File
@@ -1 +1 @@
3.9.19
3.10.15
+48 -4
View File
@@ -2,7 +2,7 @@ fix = true
line-length = 88
respect-gitignore = true
src = ["src"]
target-version = "py39"
target-version = "py310"
output-format = "grouped"
show-fixes = true
@@ -31,17 +31,61 @@ extend-select = [
"PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
"PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
]
# TODO PTH https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
ignore = ["DJ001", "SIM105", "RUF012"]
[lint.per-file-ignores]
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
"docker/wait-for-redis.py" = ["INP001", "T201"]
"src/documents/barcodes.py" = ["PTH"] # TODO Enable & remove
"src/documents/classifier.py" = ["PTH"] # TODO Enable & remove
"src/documents/consumer.py" = ["PTH"] # TODO Enable & remove
"src/documents/file_handling.py" = ["PTH"] # TODO Enable & remove
"src/documents/index.py" = ["PTH"] # TODO Enable & remove
"src/documents/management/commands/decrypt_documents.py" = ["PTH"] # TODO Enable & remove
"src/documents/management/commands/document_consumer.py" = ["PTH"] # TODO Enable & remove
"src/documents/management/commands/document_exporter.py" = ["PTH"] # TODO Enable & remove
"src/documents/management/commands/document_importer.py" = ["PTH"] # TODO Enable & remove
"src/documents/migrations/0012_auto_20160305_0040.py" = ["PTH"] # TODO Enable & remove
"src/documents/migrations/0014_document_checksum.py" = ["PTH"] # TODO Enable & remove
"src/documents/migrations/1003_mime_types.py" = ["PTH"] # TODO Enable & remove
"src/documents/migrations/1012_fix_archive_files.py" = ["PTH"] # TODO Enable & remove
"src/documents/migrations/1037_webp_encrypted_thumbnail_conversion.py" = ["PTH"] # TODO Enable & remove
"src/documents/models.py" = ["SIM115", "PTH"] # TODO PTH Enable & remove
"src/documents/parsers.py" = ["PTH"] # TODO Enable & remove
"src/documents/signals/handlers.py" = ["PTH"] # TODO Enable & remove
"src/documents/tasks.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_api_app_config.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_api_bulk_download.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_api_documents.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_classifier.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_consumer.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_file_handling.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_management.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_management_consumer.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_management_exporter.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_management_thumbnails.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_migration_archive_files.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_migration_document_pages_count.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_migration_mime_type.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_sanity_check.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_tasks.py" = ["PTH"] # TODO Enable & remove
"src/documents/tests/test_views.py" = ["PTH"] # TODO Enable & remove
"src/documents/views.py" = ["PTH"] # TODO Enable & remove
"src/paperless/checks.py" = ["PTH"] # TODO Enable & remove
"src/paperless/settings.py" = ["PTH"] # TODO Enable & remove
"src/paperless/tests/test_checks.py" = ["PTH"] # TODO Enable & remove
"src/paperless/urls.py" = ["PTH"] # TODO Enable & remove
"src/paperless/views.py" = ["PTH"] # TODO Enable & remove
"src/paperless_mail/mail.py" = ["PTH"] # TODO Enable & remove
"src/paperless_mail/preprocessor.py" = ["PTH"] # TODO Enable & remove
"src/paperless_tesseract/parsers.py" = ["PTH"] # TODO Enable & remove
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001", "PTH"] # TODO PTH Enable & remove
"src/paperless_tika/tests/test_live_tika.py" = ["PTH"] # TODO Enable & remove
"src/paperless_tika/tests/test_tika_parser.py" = ["PTH"] # TODO Enable & remove
"*/tests/*.py" = ["E501", "SIM117"]
"*/migrations/*.py" = ["E501", "SIM", "T201"]
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001"]
"src/documents/models.py" = ["SIM115"]
[lint.isort]
force-single-line = true
+2 -2
View File
@@ -11,7 +11,7 @@ If you want to implement something big:
## Python
Paperless supports python 3.9 - 3.11 at this time. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
Paperless supports python 3.10 - 3.12 at this time. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
## Branches
@@ -147,7 +147,7 @@ community members. That said, in an effort to keep the repository organized and
- Issues, pull requests and discussions that are closed will be locked after 30 days of inactivity.
- Discussions with a marked answer will be automatically closed.
- Discussions in the 'General' or 'Support' categories will be closed after 180 days of inactivity.
- Feature requests that do not meet the following thresholds will be closed: 5 "up-votes" after 180 days of inactivity or 10 "up-votes" after 365 days.
- Feature requests that do not meet the following thresholds will be closed: 180 days of inactivity, < 5 "up-votes" after 180 days, < 20 "up-votes" after 1 year or < 80 "up-votes" at 2 years.
In all cases, threads can be re-opened by project maintainers and, of course, users can always create a new discussion for related concerns.
Finally, remember that all information remains searchable and 'closed' feature requests can still serve as inspiration for new features.
+8 -12
View File
@@ -18,7 +18,7 @@ ARG PNGX_TAG_VERSION=
# Add the tag to the environment file if its a tagged dev build
RUN set -eux && \
case "${PNGX_TAG_VERSION}" in \
dev|fix*|feature*) \
dev|beta|fix*|feature*) \
sed -i -E "s/version: '([0-9\.]+)'/version: '\1 #${PNGX_TAG_VERSION}'/g" /src/src-ui/src/environments/environment.prod.ts \
;; \
esac
@@ -31,7 +31,7 @@ RUN set -eux \
# Comments:
# - pipenv dependencies are not left in the final image
# - pipenv can't touch the final image somehow
FROM --platform=$BUILDPLATFORM docker.io/python:3.11-alpine AS pipenv-base
FROM --platform=$BUILDPLATFORM docker.io/python:3.12-alpine AS pipenv-base
WORKDIR /usr/src/pipenv
@@ -39,7 +39,7 @@ COPY Pipfile* ./
RUN set -eux \
&& echo "Installing pipenv" \
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.0.1 \
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.4.0 \
&& echo "Generating requirement.txt" \
&& pipenv requirements > requirements.txt
@@ -47,7 +47,7 @@ RUN set -eux \
# Purpose: The final image
# Comments:
# - Don't leave anything extra in here
FROM docker.io/python:3.11-slim-bookworm AS main-app
FROM docker.io/python:3.12-slim-bookworm AS main-app
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/"
@@ -233,16 +233,12 @@ RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
&& python3 -m pip install --no-cache-dir --upgrade wheel \
&& echo "Installing Python requirements" \
&& curl --fail --silent --show-error --location \
--output psycopg_c-3.2.1-cp311-cp311-linux_x86_64.whl \
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.1/psycopg_c-3.2.1-cp311-cp311-linux_x86_64.whl \
--output psycopg_c-3.2.3-cp312-cp312-linux_x86_64.whl \
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.3/psycopg_c-3.2.3-cp312-cp312-linux_x86_64.whl \
&& curl --fail --silent --show-error --location \
--output psycopg_c-3.2.1-cp311-cp311-linux_aarch64.whl \
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.1/psycopg_c-3.2.1-cp311-cp311-linux_aarch64.whl \
--output psycopg_c-3.2.3-cp312-cp312-linux_aarch64.whl \
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.3/psycopg_c-3.2.3-cp312-cp312-linux_aarch64.whl \
&& python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \
&& echo "Patching whitenoise for compression speedup" \
&& curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch \
&& patch -d /usr/local/lib/python3.11/site-packages --verbose -p2 < 484.patch \
&& rm 484.patch \
&& echo "Installing NLTK data" \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
+13 -9
View File
@@ -7,8 +7,8 @@ name = "pypi"
dateparser = "~=1.2"
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
django = "~=4.2.15"
django-allauth = {extras = ["socialaccount"], version = "*"}
django = "~=5.1.4"
django-allauth = {extras = ["mfa", "socialaccount"], version = "*"}
django-auditlog = "*"
django-celery-results = "*"
django-compression-middleware = "*"
@@ -18,24 +18,27 @@ django-filter = "~=24.3"
django-guardian = "*"
django-multiselectfield = "*"
django-soft-delete = "*"
djangorestframework = "==3.15.2"
djangorestframework = "~=3.15.2"
djangorestframework-guardian = "*"
drafthorse = "*"
drf-writable-nested = "*"
bleach = "*"
celery = {extras = ["redis"], version = "*"}
channels = "~=4.1"
channels = "~=4.2"
channels-redis = "*"
concurrent-log-handler = "*"
filelock = "*"
flower = "*"
gotenberg-client = "*"
gunicorn = "*"
httpx-oauth = "*"
imap-tools = "*"
inotifyrecursive = "~=0.3"
jinja2 = "~=3.1"
langdetect = "*"
mysqlclient = "*"
nltk = "*"
ocrmypdf = "~=15.4"
ocrmypdf = "~=16.7"
pathvalidate = "*"
pdf2image = "*"
psycopg = {version = "*", extras = ["c"]}
@@ -47,23 +50,24 @@ python-magic = "*"
pyzbar = "*"
rapidfuzz = "*"
redis = {extras = ["hiredis"], version = "*"}
scikit-learn = "~=1.5"
scikit-learn = "~=1.6"
setproctitle = "*"
tika-client = "*"
tqdm = "*"
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494
uvicorn = {extras = ["standard"], version = "==0.25.0"}
watchdog = "~=4.0"
whitenoise = "~=6.7"
watchdog = "~=6.0"
whitenoise = "~=6.8"
whoosh = "~=2.7"
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
[dev-packages]
# Linting
pre-commit = "*"
ruff = "*"
# Testing
factory-boy = "*"
# Testing
pytest = "*"
pytest-cov = "*"
pytest-django = "*"
Generated
+2365 -2093
View File
File diff suppressed because it is too large Load Diff
+4 -22
View File
@@ -19,6 +19,8 @@
#
# - Open portainer Stacks list and click 'Add stack'
# - Paste the contents of this file and assign a name, e.g. 'paperless'
# - Upload 'docker-compose.env' by clicking on 'Load variables from .env file'
# - Modify the environment variables as needed
# - Click 'Deploy the stack' and wait for it to be deployed
# - Open the list of containers, select paperless_webserver_1
# - Click 'Console' and then 'Connect' to open the command line inside the container
@@ -61,28 +63,8 @@ services:
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
# The UID and GID of the user used to run paperless in the container. Set this
# to your UID and GID on the host so that you have write access to the
# consumption directory.
USERMAP_UID: 1000
USERMAP_GID: 100
# Additional languages to install for text recognition, separated by a
# whitespace. Note that this is
# different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the
# language used for OCR.
# The container installs English, German, Italian, Spanish and French by
# default.
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
# for available languages.
#PAPERLESS_OCR_LANGUAGES: tur ces
# Adjust this key if you plan to make paperless available publicly. It should
# be a very long sequence of random characters. You don't need to remember it.
#PAPERLESS_SECRET_KEY: change-me
# Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC.
#PAPERLESS_TIME_ZONE: America/Los_Angeles
# The default language to use for OCR. Set this to the language most of your
# documents are written in.
#PAPERLESS_OCR_LANGUAGE: eng
env_file:
- stack.env
volumes:
data:
+23 -12
View File
@@ -122,27 +122,38 @@ install_languages() {
if [ ${#langs[@]} -eq 0 ]; then
return
fi
apt-get update
# Build list of packages to install
to_install=()
for lang in "${langs[@]}"; do
pkg="tesseract-ocr-$lang"
if dpkg --status "$pkg" &>/dev/null; then
echo "Package $pkg already installed!"
continue
fi
if ! apt-cache show "$pkg" &>/dev/null; then
echo "Package $pkg not found! :("
continue
fi
echo "Installing package $pkg..."
if ! apt-get --assume-yes install "$pkg" &>/dev/null; then
echo "Could not install $pkg"
exit 1
else
to_install+=("$pkg")
fi
done
# Use apt only when we install packages
if [ ${#to_install[@]} -gt 0 ]; then
apt-get update
for pkg in "${to_install[@]}"; do
if ! apt-cache show "$pkg" &>/dev/null; then
echo "Skipped $pkg: Package not found! :("
continue
fi
echo "Installing package $pkg..."
if ! apt-get --assume-yes install "$pkg" &>/dev/null; then
echo "Could not install $pkg"
exit 1
fi
done
fi
}
echo "Paperless-ngx docker container starting..."
+1 -1
View File
@@ -16,7 +16,7 @@ do
# Check if it starts with "PAPERLESS_" and ends in "_FILE"
if [[ ${env_name} == PAPERLESS_*_FILE ]]; then
# This should have been named different..
if [[ ${env_name} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" ]]; then
if [[ ${env_name} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || ${env_name} == "PAPERLESS_MODEL_FILE" ]]; then
continue
fi
# Extract the value of the environment
+3 -1
View File
@@ -14,7 +14,9 @@ for command in decrypt_documents \
document_thumbnails \
document_sanity_checker \
document_fuzzy_match \
manage_superuser;
manage_superuser \
convert_mariadb_uuid \
prune_audit_logs;
do
echo "installing $command..."
sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command
+14 -1
View File
@@ -19,6 +19,8 @@ Options available to any installation of paperless:
export. Therefore, incremental backups with `rsync` are entirely
possible.
The exporter does not include API tokens and they will need to be re-generated after importing.
!!! caution
You cannot import the export generated with one version of paperless in
@@ -239,6 +241,7 @@ document_exporter target [-c] [-d] [-f] [-na] [-nt] [-p] [-sm] [-z]
optional arguments:
-c, --compare-checksums
-cj, --compare-json
-d, --delete
-f, --use-filename-format
-na, --no-archive
@@ -267,7 +270,8 @@ only export changed and added files. Paperless determines whether a file
has changed by inspecting the file attributes "date/time modified" and
"size". If that does not work out for you, specify `-c` or
`--compare-checksums` and paperless will attempt to compare file
checksums instead. This is slower.
checksums instead. This is slower. The manifest and metadata json files
are always updated, unless `cj` or `--compare-json` is specified.
Paperless will not remove any existing files in the export directory. If
you want paperless to also remove files that do not belong to the
@@ -620,3 +624,12 @@ document_fuzzy_match [--ratio] [--processes N]
If providing the `--delete` option, it is highly recommended to have a backup.
While every effort has been taken to ensure proper operation, there is always the
chance of deletion of a file you want to keep.
### Prune history (audit log) entries {#prune-history}
If the audit log is enabled Paperless-ngx keeps an audit log of all changes made to documents. Functionality to automatically remove entries for deleted documents was added but
entries created prior to this are not removed. This command allows you to prune the audit log of entries that are no longer needed.
```shell
prune_audit_logs
```
+211 -115
View File
@@ -25,20 +25,20 @@ documents.
The following algorithms are available:
- **None:** No matching will be performed.
- **Any:** Looks for any occurrence of any word provided in match in
the PDF. If you define the match as `Bank1 Bank2`, it will match
documents containing either of these terms.
- **All:** Requires that every word provided appears in the PDF,
albeit not in the order provided.
- **Exact:** Matches only if the match appears exactly as provided
(i.e. preserve ordering) in the PDF.
- **Regular expression:** Parses the match as a regular expression and
tries to find a match within the document.
- **Fuzzy match:** Uses a partial matching based on locating the tag text
inside the document, using a [partial ratio](https://rapidfuzz.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
- **Auto:** Tries to automatically match new documents. This does not
require you to set a match. See the [notes below](#automatic-matching).
- **None:** No matching will be performed.
- **Any:** Looks for any occurrence of any word provided in match in
the PDF. If you define the match as `Bank1 Bank2`, it will match
documents containing either of these terms.
- **All:** Requires that every word provided appears in the PDF,
albeit not in the order provided.
- **Exact:** Matches only if the match appears exactly as provided
(i.e. preserve ordering) in the PDF.
- **Regular expression:** Parses the match as a regular expression and
tries to find a match within the document.
- **Fuzzy match:** Uses a partial matching based on locating the tag text
inside the document, using a [partial ratio](https://rapidfuzz.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
- **Auto:** Tries to automatically match new documents. This does not
require you to set a match. See the [notes below](#automatic-matching).
When using the _any_ or _all_ matching algorithms, you can search for
terms that consist of multiple words by enclosing them in double quotes.
@@ -69,33 +69,33 @@ Paperless tries to hide much of the involved complexity with this
approach. However, there are a couple caveats you need to keep in mind
when using this feature:
- Changes to your documents are not immediately reflected by the
matching algorithm. The neural network needs to be _trained_ on your
documents after changes. Paperless periodically (default: once each
hour) checks for changes and does this automatically for you.
- The Auto matching algorithm only takes documents into account which
are NOT placed in your inbox (i.e. have any inbox tags assigned to
them). This ensures that the neural network only learns from
documents which you have correctly tagged before.
- The matching algorithm can only work if there is a correlation
between the tag, correspondent, document type, or storage path and
the document itself. Your bank statements usually contain your bank
account number and the name of the bank, so this works reasonably
well, However, tags such as "TODO" cannot be automatically
assigned.
- The matching algorithm needs a reasonable number of documents to
identify when to assign tags, correspondents, storage paths, and
types. If one out of a thousand documents has the correspondent
"Very obscure web shop I bought something five years ago", it will
probably not assign this correspondent automatically if you buy
something from them again. The more documents, the better.
- Paperless also needs a reasonable amount of negative examples to
decide when not to assign a certain tag, correspondent, document
type, or storage path. This will usually be the case as you start
filling up paperless with documents. Example: If all your documents
are either from "Webshop" or "Bank", paperless will assign one
of these correspondents to ANY new document, if both are set to
automatic matching.
- Changes to your documents are not immediately reflected by the
matching algorithm. The neural network needs to be _trained_ on your
documents after changes. Paperless periodically (default: once each
hour) checks for changes and does this automatically for you.
- The Auto matching algorithm only takes documents into account which
are NOT placed in your inbox (i.e. have any inbox tags assigned to
them). This ensures that the neural network only learns from
documents which you have correctly tagged before.
- The matching algorithm can only work if there is a correlation
between the tag, correspondent, document type, or storage path and
the document itself. Your bank statements usually contain your bank
account number and the name of the bank, so this works reasonably
well, However, tags such as "TODO" cannot be automatically
assigned.
- The matching algorithm needs a reasonable number of documents to
identify when to assign tags, correspondents, storage paths, and
types. If one out of a thousand documents has the correspondent
"Very obscure web shop I bought something five years ago", it will
probably not assign this correspondent automatically if you buy
something from them again. The more documents, the better.
- Paperless also needs a reasonable amount of negative examples to
decide when not to assign a certain tag, correspondent, document
type, or storage path. This will usually be the case as you start
filling up paperless with documents. Example: If all your documents
are either from "Webshop" or "Bank", paperless will assign one
of these correspondents to ANY new document, if both are set to
automatic matching.
## Hooking into the consumption process {#consume-hooks}
@@ -242,12 +242,12 @@ webserver:
Troubleshooting:
- Monitor the Docker Compose log
`cd ~/paperless-ngx; docker compose logs -f`
- Check your script's permission e.g. in case of permission error
`sudo chmod 755 post-consumption-example.sh`
- Pipe your scripts's output to a log file e.g.
`echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`
- Monitor the Docker Compose log
`cd ~/paperless-ngx; docker compose logs -f`
- Check your script's permission e.g. in case of permission error
`sudo chmod 755 post-consumption-example.sh`
- Pipe your scripts's output to a log file e.g.
`echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`
## File name handling {#file-name-handling}
@@ -265,7 +265,7 @@ This variable allows you to configure the filename (folders are allowed)
using placeholders. For example, configuring this to
```bash
PAPERLESS_FILENAME_FORMAT={created_year}/{correspondent}/{title}
PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ correspondent }}/{{ title }}
```
will create a directory structure as follows:
@@ -298,39 +298,39 @@ will create a directory structure as follows:
when changing `PAPERLESS_FILENAME_FORMAT` you will need to manually run the
[`document renamer`](administration.md#renamer) to move any existing documents.
#### Placeholders
### Placeholders {#filename-format-variables}
Paperless provides the following placeholders within filenames:
Paperless provides the following variables for use within filenames:
- `{asn}`: The archive serial number of the document, or "none".
- `{correspondent}`: The name of the correspondent, or "none".
- `{document_type}`: The name of the document type, or "none".
- `{tag_list}`: A comma separated list of all tags assigned to the
document.
- `{title}`: The title of the document.
- `{created}`: The full date (ISO format) the document was created.
- `{created_year}`: Year created only, formatted as the year with
century.
- `{created_year_short}`: Year created only, formatted as the year
without century, zero padded.
- `{created_month}`: Month created only (number 01-12).
- `{created_month_name}`: Month created name, as per locale
- `{created_month_name_short}`: Month created abbreviated name, as per
locale
- `{created_day}`: Day created only (number 01-31).
- `{added}`: The full date (ISO format) the document was added to
paperless.
- `{added_year}`: Year added only.
- `{added_year_short}`: Year added only, formatted as the year without
century, zero padded.
- `{added_month}`: Month added only (number 01-12).
- `{added_month_name}`: Month added name, as per locale
- `{added_month_name_short}`: Month added abbreviated name, as per
locale
- `{added_day}`: Day added only (number 01-31).
- `{owner_username}`: Username of document owner, if any, or "none"
- `{original_name}`: Document original filename, minus the extension, if any, or "none"
- `{doc_pk}`: The paperless identifier (primary key) for the document.
- `{{ asn }}`: The archive serial number of the document, or "none".
- `{{ correspondent }}`: The name of the correspondent, or "none".
- `{{ document_type }}`: The name of the document type, or "none".
- `{{ tag_list }}`: A comma separated list of all tags assigned to the
document.
- `{{ title }}`: The title of the document.
- `{{ created }}`: The full date (ISO format) the document was created.
- `{{ created_year }}`: Year created only, formatted as the year with
century.
- `{{ created_year_short }}`: Year created only, formatted as the year
without century, zero padded.
- `{{ created_month }}`: Month created only (number 01-12).
- `{{ created_month_name }}`: Month created name, as per locale
- `{{ created_month_name_short }}`: Month created abbreviated name, as per
locale
- `{{ created_day }}`: Day created only (number 01-31).
- `{{ added }}`: The full date (ISO format) the document was added to
paperless.
- `{{ added_year }}`: Year added only.
- `{{ added_year_short }}`: Year added only, formatted as the year without
century, zero padded.
- `{{ added_month }}`: Month added only (number 01-12).
- `{{ added_month_name }}`: Month added name, as per locale
- `{{ added_month_name_short }}`: Month added abbreviated name, as per
locale
- `{{ added_day }}`: Day added only (number 01-31).
- `{{ owner_username }}`: Username of document owner, if any, or "none"
- `{{ original_name }}`: Document original filename, minus the extension, if any, or "none"
- `{{ doc_pk }}`: The paperless identifier (primary key) for the document.
!!! warning
@@ -338,6 +338,11 @@ Paperless provides the following placeholders within filenames:
you may run into the limits of your operating system's maximum path lengths.
In that case, files will retain the previous path instead and the issue logged.
!!! tip
These variables are all simple strings, but the format can be a full template.
See [Filename Templates](#filename-templates) for even more advanced formatting.
Paperless will try to conserve the information from your database as
much as possible. However, some characters that you can use in document
titles and correspondent names (such as `: \ /` and a couple more) are
@@ -363,7 +368,7 @@ paperless will fall back to using the default naming scheme instead.
However, keep in mind that inside docker, if files get stored outside of
the predefined volumes, they will be lost after a restart.
##### Empty placeholders
#### Empty placeholders
You can affect how empty placeholders are treated by changing the
[`PAPERLESS_FILENAME_FORMAT_REMOVE_NONE`](configuration.md#PAPERLESS_FILENAME_FORMAT_REMOVE_NONE) setting.
@@ -376,10 +381,10 @@ before empty placeholders are removed as well, empty directories are omitted.
When a single storage layout is not sufficient for your use case, storage paths allow for more complex
structure to set precisely where each document is stored in the file system.
- Each storage path is a [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) and
follows the rules described above
- Each document is assigned a storage path using the matching algorithms described above, but can be
overwritten at any time
- Each storage path is a [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) and
follows the rules described above
- Each document is assigned a storage path using the matching algorithms described above, but can be
overwritten at any time
For example, you could define the following two storage paths:
@@ -390,8 +395,8 @@ For example, you could define the following two storage paths:
the correspondence.
```
By Year = {created_year}/{correspondent}/{title}
Insurances = Insurances/{correspondent}/{created_year}-{created_month}-{created_day} {title}
By Year = {{ created_year }}/{{ correspondent }}/{{ title }}
Insurances = Insurances/{{ correspondent }}/{{ created_year }}-{{ created_month }}-{{ created_day }} {{ title }}
```
If you then map these storage paths to the documents, you might get the
@@ -418,6 +423,97 @@ Insurances/ # Insurances
Defining a storage path is optional. If no storage path is defined for a
document, the global [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) is applied.
### Filename Templates {#filename-templates}
The filename formatting uses [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/) to build the filename.
This allows for complex logic to be included in the format, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11) to manipulate the [variables](#filename-format-variables)
provided. The template is provided as a string, potentially multiline, and rendered into a single line.
In addition, the entire Document instance is available to be utilized in a more advanced way, as well as some variables which only make sense to be accessed
with more complex logic.
#### Additional Variables
- `{{ tag_name_list }}`: A list of tag names applied to the document, ordered by the tag name. Note this is a list, not a single string
- `{{ custom_fields }}`: A mapping of custom field names to their type and value. A user can access the mapping by field name or check if a field is applied by checking its existence in the variable.
!!! tip
To access a custom field which has a space in the name, use the `get_cf_value` filter. See the examples below.
This helps get fields by name and handle a default value if the named field is not attached to a Document.
#### Examples
This example will construct a path based on the archive serial number range:
```jinja
somepath/
{% if document.archive_serial_number >= 0 and document.archive_serial_number <= 200 %}
asn-000-200/{{title}}
{% elif document.archive_serial_number >= 201 and document.archive_serial_number <= 400 %}
asn-201-400
{% if document.archive_serial_number >= 201 and document.archive_serial_number < 300 %}
/asn-2xx
{% elif document.archive_serial_number >= 300 and document.archive_serial_number < 400 %}
/asn-3xx
{% endif %}
{% endif %}
/{{ title }}
```
For a document with an ASN of 205, it would result in `somepath/asn-201-400/asn-2xx/Title.pdf`, but
a document with an ASN of 355 would be placed in `somepath/asn-201-400/asn-3xx/Title.pdf`.
```jinja
{% if document.mime_type == "application/pdf" %}
pdfs
{% elif document.mime_type == "image/png" %}
pngs
{% else %}
others
{% endif %}
/{{ title }}
```
For a PDF document, it would result in `pdfs/Title.pdf`, but for a PNG document, the path would be `pngs/Title.pdf`.
To use custom fields:
```jinja
{% if "Invoice" in custom_fields %}
invoices/{{ custom_fields.Invoice.value }}
{% else %}
not-invoices/{{ title }}
{% endif %}
```
If the document has a custom field named "Invoice" with a value of 123, it would be filed into the `invoices/123.pdf`, but a document without the custom field
would be filed to `not-invoices/Title.pdf`
If the custom field is named "Invoice Number", you would access the value of it via the `get_cf_value` filter due to quirks of the Django Template Language:
```jinja
"invoices/{{ custom_fields|get_cf_value('Invoice Number') }}"
```
You can also use a custom `datetime` filter to format dates:
```jinja
invoices/
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%Y') }}/
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%m') }}/
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%d') }}/
Invoice_{{ custom_fields|get_cf_value("Select Field") }}_{{ custom_fields|get_cf_value("Date Field","2024-01-01")|replace("-", "") }}.pdf
```
This will create a path like `invoices/2022/01/01/Invoice_OptionTwo_20220101.pdf` if the custom field "Date Field" is set to January 1, 2022 and "Select Field" is set to `OptionTwo`.
## Automatic recovery of invalid PDFs {#pdf-recovery}
Paperless will attempt to "clean" certain invalid PDFs with `qpdf` before processing if, for example, the mime_type
detection is incorrect. This can happen if the PDF is not properly formatted or contains errors.
## Celery Monitoring {#celery-monitoring}
The monitoring tool
@@ -436,15 +532,15 @@ installation, you can use volumes to accomplish this:
```yaml
services:
# ...
webserver:
environment:
- PAPERLESS_ENABLE_FLOWER
ports:
- 5555:5555 # (2)!
# ...
volumes:
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro # (1)!
webserver:
environment:
- PAPERLESS_ENABLE_FLOWER
ports:
- 5555:5555 # (2)!
# ...
volumes:
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro # (1)!
```
1. Note the `:ro` tag means the file will be mounted as read only.
@@ -475,11 +571,11 @@ For example, using Docker Compose:
```yaml
services:
# ...
webserver:
# ...
volumes:
- /path/to/my/scripts:/custom-cont-init.d:ro # (1)!
webserver:
# ...
volumes:
- /path/to/my/scripts:/custom-cont-init.d:ro # (1)!
```
1. Note the `:ro` tag means the folder will be mounted as read only. This is for extra security against changes
@@ -527,16 +623,16 @@ Paperless is able to utilize barcodes for automatically performing some tasks.
At this time, the library utilized for detection of barcodes supports the following types:
- AN-13/UPC-A
- UPC-E
- EAN-8
- Code 128
- Code 93
- Code 39
- Codabar
- Interleaved 2 of 5
- QR Code
- SQ Code
- AN-13/UPC-A
- UPC-E
- EAN-8
- Code 128
- Code 93
- Code 39
- Codabar
- Interleaved 2 of 5
- QR Code
- SQ Code
You may check for updates on the [zbar library homepage](https://github.com/mchehab/zbar).
For usage in Paperless, the type of barcode does not matter, only the contents of it.
@@ -708,7 +804,7 @@ gpg --decrypt name_of_file.asc
### Setup
First, enable the [PAPERLESS_GPG_DECRYPTOR environment variable](configuration.md#PAPERLESS_GPG_DECRYPTOR).
First, enable the [PAPERLESS_ENABLE_GPG_DECRYPTOR environment variable](configuration.md#PAPERLESS_ENABLE_GPG_DECRYPTOR).
Then determine your local `gpg-agent.extra` socket by invoking
@@ -723,9 +819,9 @@ If using docker, you'll need to add the following volume mounts to your `docker-
```yaml
webserver:
volumes:
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
- <path to gpg-agent.extra socket>:/usr/src/paperless/.gnupg/S.gpg-agent
volumes:
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
- <path to gpg-agent.extra socket>:/usr/src/paperless/.gnupg/S.gpg-agent
```
For a 'bare-metal' installation no further configuration is necessary. If you
@@ -733,9 +829,9 @@ want to use a separate `GNUPG_HOME`, you can do so by configuring the [PAPERLESS
### Troubleshooting
- Make sure, that `gpg-agent` is running on your host machine
- Make sure, that encryption and decryption works from inside the container using the `gpg` commands from above.
- Check that all files in `/usr/src/paperless/.gnupg` have correct permissions
- Make sure, that `gpg-agent` is running on your host machine
- Make sure, that encryption and decryption works from inside the container using the `gpg` commands from above.
- Check that all files in `/usr/src/paperless/.gnupg` have correct permissions
```shell
paperless@9da1865df327:~/.gnupg$ ls -al
+217 -164
View File
@@ -8,23 +8,23 @@ most of the available filters and ordering fields.
The API provides the following main endpoints:
- `/api/correspondents/`: Full CRUD support.
- `/api/custom_fields/`: Full CRUD support.
- `/api/documents/`: Full CRUD support, except POSTing new documents.
See [below](#file-uploads).
- `/api/document_types/`: Full CRUD support.
- `/api/groups/`: Full CRUD support.
- `/api/logs/`: Read-Only.
- `/api/mail_accounts/`: Full CRUD support.
- `/api/mail_rules/`: Full CRUD support.
- `/api/profile/`: GET, PATCH
- `/api/share_links/`: Full CRUD support.
- `/api/storage_paths/`: Full CRUD support.
- `/api/tags/`: Full CRUD support.
- `/api/tasks/`: Read-only.
- `/api/users/`: Full CRUD support.
- `/api/workflows/`: Full CRUD support.
- `/api/search/` GET, see [below](#global-search).
- `/api/correspondents/`: Full CRUD support.
- `/api/custom_fields/`: Full CRUD support.
- `/api/documents/`: Full CRUD support, except POSTing new documents.
See [below](#file-uploads).
- `/api/document_types/`: Full CRUD support.
- `/api/groups/`: Full CRUD support.
- `/api/logs/`: Read-Only.
- `/api/mail_accounts/`: Full CRUD support.
- `/api/mail_rules/`: Full CRUD support.
- `/api/profile/`: GET, PATCH
- `/api/share_links/`: Full CRUD support.
- `/api/storage_paths/`: Full CRUD support.
- `/api/tags/`: Full CRUD support.
- `/api/tasks/`: Read-only.
- `/api/users/`: Full CRUD support.
- `/api/workflows/`: Full CRUD support.
- `/api/search/` GET, see [below](#global-search).
All of these endpoints except for the logging endpoint allow you to
fetch (and edit and delete where appropriate) individual objects by
@@ -33,31 +33,32 @@ appending their primary key to the path, e.g. `/api/documents/454/`.
The objects served by the document endpoint contain the following
fields:
- `id`: ID of the document. Read-only.
- `title`: Title of the document.
- `content`: Plain text content of the document.
- `tags`: List of IDs of tags assigned to this document, or empty
list.
- `document_type`: Document type of this document, or null.
- `correspondent`: Correspondent of this document or null.
- `created`: The date time at which this document was created.
- `created_date`: The date (YYYY-MM-DD) at which this document was
created. Optional. If also passed with created, this is ignored.
- `modified`: The date at which this document was last edited in
paperless. Read-only.
- `added`: The date at which this document was added to paperless.
Read-only.
- `archive_serial_number`: The identifier of this document in a
physical document archive.
- `original_file_name`: Verbose filename of the original document.
Read-only.
- `archived_file_name`: Verbose filename of the archived document.
Read-only. Null if no archived document is available.
- `notes`: Array of notes associated with the document.
- `set_permissions`: Allows setting document permissions. Optional,
write-only. See [below](#permissions).
- `custom_fields`: Array of custom fields & values, specified as
`{ field: CUSTOM_FIELD_ID, value: VALUE }`
- `id`: ID of the document. Read-only.
- `title`: Title of the document.
- `content`: Plain text content of the document.
- `tags`: List of IDs of tags assigned to this document, or empty
list.
- `document_type`: Document type of this document, or null.
- `correspondent`: Correspondent of this document or null.
- `created`: The date time at which this document was created.
- `created_date`: The date (YYYY-MM-DD) at which this document was
created. Optional. If also passed with created, this is ignored.
- `modified`: The date at which this document was last edited in
paperless. Read-only.
- `added`: The date at which this document was added to paperless.
Read-only.
- `archive_serial_number`: The identifier of this document in a
physical document archive.
- `original_file_name`: Verbose filename of the original document.
Read-only.
- `archived_file_name`: Verbose filename of the archived document.
Read-only. Null if no archived document is available.
- `notes`: Array of notes associated with the document.
- `page_count`: Number of pages.
- `set_permissions`: Allows setting document permissions. Optional,
write-only. See [below](#permissions).
- `custom_fields`: Array of custom fields & values, specified as
`{ field: CUSTOM_FIELD_ID, value: VALUE }`
!!! note
@@ -68,11 +69,11 @@ fields:
In addition to that, the document endpoint offers these additional
actions on individual documents:
- `/api/documents/<pk>/download/`: Download the document.
- `/api/documents/<pk>/preview/`: Display the document inline, without
downloading it.
- `/api/documents/<pk>/thumb/`: Download the PNG thumbnail of a
document.
- `/api/documents/<pk>/download/`: Download the document.
- `/api/documents/<pk>/preview/`: Display the document inline, without
downloading it.
- `/api/documents/<pk>/thumb/`: Download the PNG thumbnail of a
document.
Paperless generates archived PDF/A documents from consumed files and
stores both the original files as well as the archived files. By
@@ -106,30 +107,30 @@ Access the metadata of a document with an ID `id` at
The endpoint reports the following data:
- `original_checksum`: MD5 checksum of the original document.
- `original_size`: Size of the original document, in bytes.
- `original_mime_type`: Mime type of the original document.
- `media_filename`: Current filename of the document, under which it
is stored inside the media directory.
- `has_archive_version`: True, if this document is archived, false
otherwise.
- `original_metadata`: A list of metadata associated with the original
document. See below.
- `archive_checksum`: MD5 checksum of the archived document, or null.
- `archive_size`: Size of the archived document in bytes, or null.
- `archive_metadata`: Metadata associated with the archived document,
or null. See below.
- `original_checksum`: MD5 checksum of the original document.
- `original_size`: Size of the original document, in bytes.
- `original_mime_type`: Mime type of the original document.
- `media_filename`: Current filename of the document, under which it
is stored inside the media directory.
- `has_archive_version`: True, if this document is archived, false
otherwise.
- `original_metadata`: A list of metadata associated with the original
document. See below.
- `archive_checksum`: MD5 checksum of the archived document, or null.
- `archive_size`: Size of the archived document in bytes, or null.
- `archive_metadata`: Metadata associated with the archived document,
or null. See below.
File metadata is reported as a list of objects in the following form:
```json
[
{
"namespace": "http://ns.adobe.com/pdf/1.3/",
"prefix": "pdf",
"key": "Producer",
"value": "SparklePDF, Fancy edition"
}
{
"namespace": "http://ns.adobe.com/pdf/1.3/",
"prefix": "pdf",
"key": "Producer",
"value": "SparklePDF, Fancy edition"
}
]
```
@@ -139,9 +140,9 @@ document. Paperless only reports PDF metadata at this point.
## Documents additional endpoints
- `/api/documents/<id>/notes/`: Retrieve notes for a document.
- `/api/documents/<id>/share_links/`: Retrieve share links for a document.
- `/api/documents/<id>/history/`: Retrieve history of changes for a document.
- `/api/documents/<id>/notes/`: Retrieve notes for a document.
- `/api/documents/<id>/share_links/`: Retrieve share links for a document.
- `/api/documents/<id>/history/`: Retrieve history of changes for a document.
## Authorization
@@ -227,20 +228,14 @@ Full text searching is available on the `/api/documents/` endpoint. Two
specific query parameters cause the API to return full text search
results:
- `/api/documents/?query=your%20search%20query`: Search for a document
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage.md#basic-usage_searching).
- `/api/documents/?more_like_id=1234`: Search for documents similar to
the document with id 1234.
- `/api/documents/?query=your%20search%20query`: Search for a document
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage.md#basic-usage_searching).
- `/api/documents/?more_like_id=1234`: Search for documents similar to
the document with id 1234.
Pagination works exactly the same as it does for normal requests on this
endpoint.
Certain limitations apply to full text queries:
- Results are always sorted by search score. The results matching the
query best will show up first.
- Only a small subset of filtering parameters are supported.
Furthermore, each returned document has an additional `__search_hit__`
attribute with various information about the search results:
@@ -273,12 +268,57 @@ attribute with various information about the search results:
}
```
- `score` is an indication how well this document matches the query
relative to the other search results.
- `highlights` is an excerpt from the document content and highlights
the search terms with `<span>` tags as shown above.
- `rank` is the index of the search results. The first result will
have rank 0.
- `score` is an indication how well this document matches the query
relative to the other search results.
- `highlights` is an excerpt from the document content and highlights
the search terms with `<span>` tags as shown above.
- `rank` is the index of the search results. The first result will
have rank 0.
### Filtering by custom fields
You can filter documents by their custom field values by specifying the
`custom_field_query` query parameter. Here are some recipes for common
use cases:
1. Documents with a custom field "due" (date) between Aug 1, 2024 and
Sept 1, 2024 (inclusive):
`?custom_field_query=["due", "range", ["2024-08-01", "2024-09-01"]]`
2. Documents with a custom field "customer" (text) that equals "bob"
(case sensitive):
`?custom_field_query=["customer", "exact", "bob"]`
3. Documents with a custom field "answered" (boolean) set to `true`:
`?custom_field_query=["answered", "exact", true]`
4. Documents with a custom field "favorite animal" (select) set to either
"cat" or "dog":
`?custom_field_query=["favorite animal", "in", ["cat", "dog"]]`
5. Documents with a custom field "address" (text) that is empty:
`?custom_field_query=["OR", ["address", "isnull", true], ["address", "exact", ""]]`
6. Documents that don't have a field called "foo":
`?custom_field_query=["foo", "exists", false]`
7. Documents that have document links "references" to both document 3 and 7:
`?custom_field_query=["references", "contains", [3, 7]]`
All field types support basic operations including `exact`, `in`, `isnull`,
and `exists`. String, URL, and monetary fields support case-insensitive
substring matching operations including `icontains`, `istartswith`, and
`iendswith`. Integer, float, and date fields support arithmetic comparisons
including `gt` (>), `gte` (>=), `lt` (<), `lte` (<=), and `range`.
Lastly, document link fields support a `contains` operator that behaves
like a "is superset of" check.
### `/api/search/autocomplete/`
@@ -286,8 +326,8 @@ Get auto completions for a partial search term.
Query parameters:
- `term`: The incomplete term.
- `limit`: Amount of results. Defaults to 10.
- `term`: The incomplete term.
- `limit`: Amount of results. Defaults to 10.
Results returned by the endpoint are ordered by importance of the term
in the document index. The first result is the term that has the highest
@@ -311,19 +351,19 @@ from there.
The endpoint supports the following optional form fields:
- `title`: Specify a title that the consumer should use for the
document.
- `created`: Specify a DateTime where the document was created (e.g.
"2016-04-19" or "2016-04-19 06:15:00+02:00").
- `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.
- `custom_fields`: An array of custom field ids to assign (with an empty
value) to the document.
- `title`: Specify a title that the consumer should use for the
document.
- `created`: Specify a DateTime where the document was created (e.g.
"2016-04-19" or "2016-04-19 06:15:00+02:00").
- `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.
- `custom_fields`: An array of custom field ids to assign (with an empty
value) to the document.
The endpoint will immediately return HTTP 200 if the document consumption
process was started successfully, with the UUID of the consumption task
@@ -389,50 +429,55 @@ a json payload of the format:
The following methods are supported:
- `set_correspondent`
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
- `set_document_type`
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
- `set_storage_path`
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
- `add_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `remove_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `modify_tags`
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
- `delete`
- No `parameters` required
- `reprocess`
- No `parameters` required
- `set_permissions`
- Requires `parameters`:
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
- `"owner": OWNER_ID or null`
- `"merge": true or false` (defaults to false)
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
removing them) or be merged with existing permissions.
- `merge`
- No additional `parameters` required.
- The ordering of the merged document is determined by the list of IDs.
- Optional `parameters`:
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
- `"delete_originals": true` to delete the original documents. This requires the calling user being the owner of
all documents that are merged.
- `split`
- Requires `parameters`:
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
- Optional `parameters`:
- `"delete_originals": true` to delete the original document after consumption. This requires the calling user being the owner of
the document.
- The split operation only accepts a single document.
- `rotate`
- Requires `parameters`:
- `"degrees": DEGREES`. Must be an integer i.e. 90, 180, 270
- `delete_pages`
- Requires `parameters`:
- `"pages": [..]` The list should be a list of integers e.g. `"[2,3,4]"`
- The delete_pages operation only accepts a single document.
- `set_correspondent`
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
- `set_document_type`
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
- `set_storage_path`
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
- `add_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `remove_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `modify_tags`
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
- `delete`
- No `parameters` required
- `reprocess`
- No `parameters` required
- `set_permissions`
- Requires `parameters`:
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
- `"owner": OWNER_ID or null`
- `"merge": true or false` (defaults to false)
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
removing them) or be merged with existing permissions.
- `merge`
- No additional `parameters` required.
- The ordering of the merged document is determined by the list of IDs.
- Optional `parameters`:
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
- `"delete_originals": true` to delete the original documents. This requires the calling user being the owner of
all documents that are merged.
- `split`
- Requires `parameters`:
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
- Optional `parameters`:
- `"delete_originals": true` to delete the original document after consumption. This requires the calling user being the owner of
the document.
- The split operation only accepts a single document.
- `rotate`
- Requires `parameters`:
- `"degrees": DEGREES`. Must be an integer i.e. 90, 180, 270
- `delete_pages`
- Requires `parameters`:
- `"pages": [..]` The list should be a list of integers e.g. `"[2,3,4]"`
- The delete_pages operation only accepts a single document.
- `modify_custom_fields`
- Requires `parameters`:
- `"add_custom_fields": { CUSTOM_FIELD_ID: VALUE }`: JSON object consisting of custom field id:value pairs to add to the document, can also be a list of custom field IDs
to add with empty values.
- `"remove_custom_fields": [CUSTOM_FIELD_ID]`: custom field ids to remove from the document.
### Objects
@@ -454,16 +499,16 @@ operations, using the endpoint: `/api/bulk_edit_objects/`, which requires a json
The REST API is versioned since Paperless-ngx 1.3.0.
- Versioning ensures that changes to the API don't break older
clients.
- Clients specify the specific version of the API they wish to use
with every request and Paperless will handle the request using the
specified API version.
- Even if the underlying data model changes, older API versions will
always serve compatible data.
- If no version is specified, Paperless will serve version 1 to ensure
compatibility with older clients that do not request a specific API
version.
- Versioning ensures that changes to the API don't break older
clients.
- Clients specify the specific version of the API they wish to use
with every request and Paperless will handle the request using the
specified API version.
- Even if the underlying data model changes, older API versions will
always serve compatible data.
- If no version is specified, Paperless will serve version 1 to ensure
compatibility with older clients that do not request a specific API
version.
API versions are specified by submitting an additional HTTP `Accept`
header with every request:
@@ -500,19 +545,27 @@ Initial API version.
#### Version 2
- Added field `Tag.color`. This read/write string field contains a hex
color such as `#a6cee3`.
- Added read-only field `Tag.text_color`. This field contains the text
color to use for a specific tag, which is either black or white
depending on the brightness of `Tag.color`.
- Removed field `Tag.colour`.
- Added field `Tag.color`. This read/write string field contains a hex
color such as `#a6cee3`.
- Added read-only field `Tag.text_color`. This field contains the text
color to use for a specific tag, which is either black or white
depending on the brightness of `Tag.color`.
- Removed field `Tag.colour`.
#### Version 3
- Permissions endpoints have been added.
- The format of the `/api/ui_settings/` has changed.
- Permissions endpoints have been added.
- The format of the `/api/ui_settings/` has changed.
#### Version 4
- Consumption templates were refactored to workflows and API endpoints
changed as such.
- Consumption templates were refactored to workflows and API endpoints
changed as such.
#### Version 5
- Added bulk deletion methods for documents and objects.
#### Version 6
- Moved acknowledge tasks endpoint to be under `/api/tasks/acknowledge/`.
+1 -1
View File
@@ -10,7 +10,7 @@
--md-hue: 222;
}
@media (min-width: 400px) {
@media (min-width: 768px) {
.grid-left {
width: 33%;
float: left;
+4086 -3715
View File
File diff suppressed because it is too large Load Diff
+76 -19
View File
@@ -8,17 +8,17 @@ common [OCR](#ocr) related settings and some frontend settings. If set, these wi
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`.
- If you run paperless on docker, `paperless.conf` is not used.
Rather, configure paperless by copying necessary options to
`docker-compose.env`.
- If you are running paperless on anything else, paperless will search
for the configuration file in these locations and use the first one
it finds:
- The environment variable `PAPERLESS_CONFIGURATION_PATH`
- `/path/to/paperless/paperless.conf`
- `/etc/paperless.conf`
- `/usr/local/etc/paperless.conf`
- If you are running paperless on anything else, paperless will search
for the configuration file in these locations and use the first one
it finds:
- The environment variable `PAPERLESS_CONFIGURATION_PATH`
- `/path/to/paperless/paperless.conf`
- `/etc/paperless.conf`
- `/usr/local/etc/paperless.conf`
## Required services
@@ -38,7 +38,7 @@ matcher.
`redis://<username>:<password>@<host>:<port>/<DBIndex>`
[More information on securing your Redis
Instance](https://redis.io/docs/getting-started/#securing-redis).
Instance](https://redis.io/docs/latest/operate/oss_and_stack/management/security).
Defaults to `redis://localhost:6379`.
@@ -198,6 +198,12 @@ Docker, this may be the `environment` key of the webserver or a
containing the configuration parameters. Be sure to use the correct format
and watch out for indentation if editing the YAML file.
#### [`PAPERLESS_EINVOICE_PARSER_ENABLED=<bool>`](#PAPERLESS_EINVOICE_PARSER_ENABLED) {#PAPERLESS_EINVOICE_PARSER_ENABLED}
: Enable (or disable) the e-invoice parser. Note that this feature requires Tika to be enabled.
Defaults to false.
## Paths and folders
#### [`PAPERLESS_CONSUMPTION_DIR=<path>`](#PAPERLESS_CONSUMPTION_DIR) {#PAPERLESS_CONSUMPTION_DIR}
@@ -608,9 +614,18 @@ You can optionally also automatically redirect users to the SSO login with [PAPE
#### [`PAPERLESS_ACCOUNT_SESSION_REMEMBER=<bool>`](#PAPERLESS_ACCOUNT_SESSION_REMEMBER) {#PAPERLESS_ACCOUNT_SESSION_REMEMBER}
: See the corresponding
: If false, sessions will expire at browser close, if true will use `PAPERLESS_SESSION_COOKIE_AGE` for expiration. See the corresponding
[django-allauth documentation](https://docs.allauth.org/en/latest/account/configuration.html)
Defaults to True
#### [`PAPERLESS_SESSION_COOKIE_AGE=<int>`](#PAPERLESS_SESSION_COOKIE_AGE) {#PAPERLESS_SESSION_COOKIE_AGE}
: Login session cookie expiration. Applies if `PAPERLESS_ACCOUNT_SESSION_REMEMBER` is enabled. See the corresponding
[django documentation](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE)
Defaults to 1209600 (2 weeks)
## OCR settings {#ocr}
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
@@ -1149,18 +1164,12 @@ within your documents.
second, and year last order. Characters D, M, or Y can be shuffled
to meet the required order.
#### [`PAPERLESS_GPG_DECRYPTOR=<bool>`](#PAPERLESS_GPG_DECRYPTOR) {#PAPERLESS_GPG_DECRYPTOR}
#### [`PAPERLESS_ENABLE_GPG_DECRYPTOR=<bool>`](#PAPERLESS_ENABLE_GPG_DECRYPTOR) {#PAPERLESS_ENABLE_GPG_DECRYPTOR}
: Enable or disable the GPG decryptor for encrypted emails. See [GPG Decryptor](advanced_usage.md#gpg-decryptor) for more information.
Defaults to false.
#### [`PAPERLESS_EMAIL_GNUPG_HOME=<str>`](#PAPERLESS_EMAIL_GNUPG_HOME) {#PAPERLESS_EMAIL_GNUPG_HOME}
: Optional, sets the `GNUPG_HOME` path to use with GPG decryptor for encrypted emails. See [GPG Decryptor](advanced_usage.md#gpg-decryptor) for more information. If not set, defaults to the default `GNUPG_HOME` path.
Defaults to <not set>.
### Polling {#polling}
#### [`PAPERLESS_CONSUMER_POLLING=<num>`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING}
@@ -1204,6 +1213,48 @@ consumers working on the same file. Configure this to prevent that.
Defaults to 0.5 seconds.
## Incoming Mail {#incoming_mail}
### Email OAuth {#email_oauth}
#### [`PAPERLESS_OAUTH_CALLBACK_BASE_URL=<str>`](#PAPERLESS_OAUTH_CALLBACK_BASE_URL) {#PAPERLESS_OAUTH_CALLBACK_BASE_URL}
: The base URL for the OAuth callback. This is used to construct the full URL for the OAuth callback. This should be the URL that the Paperless instance is accessible at. If not set, defaults to the `PAPERLESS_URL` setting. At least one of these settings must be set to enable OAuth Email setup.
Defaults to none (thus will use [PAPERLESS_URL](#PAPERLESS_URL)).
#### [`PAPERLESS_GMAIL_OAUTH_CLIENT_ID=<str>`](#PAPERLESS_GMAIL_OAUTH_CLIENT_ID) {#PAPERLESS_GMAIL_OAUTH_CLIENT_ID}
: The OAuth client ID for Gmail. This is required for Gmail OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
Defaults to none.
#### [`PAPERLESS_GMAIL_OAUTH_CLIENT_SECRET=<str>`](#PAPERLESS_GMAIL_OAUTH_CLIENT_SECRET) {#PAPERLESS_GMAIL_OAUTH_CLIENT_SECRET}
: The OAuth client secret for Gmail. This is required for Gmail OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
Defaults to none.
#### [`PAPERLESS_OUTLOOK_OAUTH_CLIENT_ID=<str>`](#PAPERLESS_OUTLOOK_OAUTH_CLIENT_ID) {#PAPERLESS_OUTLOOK_OAUTH_CLIENT_ID}
: The OAuth client ID for Outlook. This is required for Outlook OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
Defaults to none.
#### [`PAPERLESS_OUTLOOK_OAUTH_CLIENT_SECRET=<str>`](#PAPERLESS_OUTLOOK_OAUTH_CLIENT_SECRET) {#PAPERLESS_OUTLOOK_OAUTH_CLIENT_SECRET}
: The OAuth client secret for Outlook. This is required for Outlook OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
Defaults to none.
### Encrypted Emails {#encrypted_emails}
#### [`PAPERLESS_EMAIL_GNUPG_HOME=<str>`](#PAPERLESS_EMAIL_GNUPG_HOME) {#PAPERLESS_EMAIL_GNUPG_HOME}
: Optional, sets the `GNUPG_HOME` path to use with GPG decryptor for encrypted emails. See [GPG Decryptor](advanced_usage.md#gpg-decryptor) for more information. If not set, defaults to the default `GNUPG_HOME` path.
Defaults to <not set>.
## Barcodes {#barcodes}
#### [`PAPERLESS_CONSUMER_ENABLE_BARCODES=<bool>`](#PAPERLESS_CONSUMER_ENABLE_BARCODES) {#PAPERLESS_CONSUMER_ENABLE_BARCODES}
@@ -1242,6 +1293,12 @@ change this.
Defaults to "PATCHT"
#### [`PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES=<bool>`](#PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES) {#PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES}
: If set to true, all pages that are split by a barcode (such as PATCHT) will be kept.
Defaults to false.
#### [`PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=<bool>`](#PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE) {#PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE}
: Enables the detection of barcodes in the scanned document and
+64 -64
View File
@@ -6,23 +6,23 @@ on Paperless-ngx.
Check out the source from GitHub. The repository is organized in the
following way:
- `main` always represents the latest release and will only see
changes when a new release is made.
- `dev` contains the code that will be in the next release.
- `feature-X` contains bigger changes that will be in some release, but
not necessarily the next one.
- `main` always represents the latest release and will only see
changes when a new release is made.
- `dev` contains the code that will be in the next release.
- `feature-X` contains bigger changes that will be in some release, but
not necessarily the next one.
When making functional changes to Paperless-ngx, _always_ make your changes
on the `dev` branch.
Apart from that, the folder structure is as follows:
- `docs/` - Documentation.
- `src-ui/` - Code of the front end.
- `src/` - Code of the back end.
- `scripts/` - Various scripts that help with different parts of
development.
- `docker/` - Files required to build the docker image.
- `docs/` - Documentation.
- `src-ui/` - Code of the front end.
- `src/` - Code of the back end.
- `scripts/` - Various scripts that help with different parts of
development.
- `docker/` - Files required to build the docker image.
## Contributing to Paperless-ngx
@@ -99,17 +99,17 @@ first-time setup.
7. You can now either ...
- install redis or
- install redis or
- use the included `scripts/start_services.sh` to use docker to fire
up a redis instance (and some other services such as tika,
gotenberg and a database server) or
- use the included `scripts/start_services.sh` to use docker to fire
up a redis instance (and some other services such as tika,
gotenberg and a database server) or
- spin up a bare redis container
- spin up a bare redis container
```
$ docker run -d -p 6379:6379 --restart unless-stopped redis:latest
```
```
$ docker run -d -p 6379:6379 --restart unless-stopped redis:latest
```
8. Continue with either back-end or front-end development or both :-).
@@ -122,9 +122,9 @@ work well for development, but you can use whatever you want.
Configure the IDE to use the `src/`-folder as the base source folder.
Configure the following launch configurations in your IDE:
- `python3 manage.py runserver`
- `python3 manage.py document_consumer`
- `celery --app paperless worker -l DEBUG` (or any other log level)
- `python3 manage.py runserver`
- `python3 manage.py document_consumer`
- `celery --app paperless worker -l DEBUG` (or any other log level)
To start them all:
@@ -150,11 +150,11 @@ $ ng build --configuration production
### Testing
- Run `pytest` in the `src/` directory to execute all tests. This also
generates a HTML coverage report. When runnings test, `paperless.conf`
is loaded as well. However, the tests rely on the default
configuration. This is not ideal. But for now, make sure no settings
except for DEBUG are overridden when testing.
- Run `pytest` in the `src/` directory to execute all tests. This also
generates a HTML coverage report. When runnings test, `paperless.conf`
is loaded as well. However, the tests rely on the default
configuration. This is not ideal. But for now, make sure no settings
except for DEBUG are overridden when testing.
!!! note
@@ -245,14 +245,14 @@ these parts have to be translated separately.
### Front end localization
- The AngularJS front end does localization according to the [Angular
documentation](https://angular.io/guide/i18n).
- The source language of the project is "en_US".
- The source strings end up in the file `src-ui/messages.xlf`.
- The translated strings need to be placed in the
`src-ui/src/locale/` folder.
- In order to extract added or changed strings from the source files,
call `ng extract-i18n`.
- The AngularJS front end does localization according to the [Angular
documentation](https://angular.io/guide/i18n).
- The source language of the project is "en_US".
- The source strings end up in the file `src-ui/messages.xlf`.
- The translated strings need to be placed in the
`src-ui/src/locale/` folder.
- In order to extract added or changed strings from the source files,
call `ng extract-i18n`.
Adding new languages requires adding the translated files in the
`src-ui/src/locale/` folder and adjusting a couple files.
@@ -298,18 +298,18 @@ A majority of the strings that appear in the back end appear only when
the admin is used. However, some of these are still shown on the front
end (such as error messages).
- The django application does localization according to the [Django
documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
- The source language of the project is "en_US".
- Localization files end up in the folder `src/locale/`.
- In order to extract strings from the application, call
`python3 manage.py makemessages -l en_US`. This is important after
making changes to translatable strings.
- The message files need to be compiled for them to show up in the
application. Call `python3 manage.py compilemessages` to do this.
The generated files don't get committed into git, since these are
derived artifacts. The build pipeline takes care of executing this
command.
- The django application does localization according to the [Django
documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
- The source language of the project is "en_US".
- Localization files end up in the folder `src/locale/`.
- In order to extract strings from the application, call
`python3 manage.py makemessages -l en_US`. This is important after
making changes to translatable strings.
- The message files need to be compiled for them to show up in the
application. Call `python3 manage.py compilemessages` to do this.
The generated files don't get committed into git, since these are
derived artifacts. The build pipeline takes care of executing this
command.
Adding new languages requires adding the translated files in the
`src/locale/`-folder and adjusting the file
@@ -360,10 +360,10 @@ If you want to build the documentation locally, this is how you do it:
The docker image is primarily built by the GitHub actions workflow, but
it can be faster when developing to build and tag an image locally.
Building the image works as with any image:
Make sure you have the `docker-buildx` package installed. Building the image works as with any image:
```
docker build --file Dockerfile --tag paperless:local --progress simple .
docker build --file Dockerfile --tag paperless:local .
```
## Extending Paperless-ngx
@@ -378,10 +378,10 @@ base code.
Paperless-ngx uses parsers to add documents. A parser is
responsible for:
- Retrieving the content from the original
- Creating a thumbnail
- _optional:_ Retrieving a created date from the original
- _optional:_ Creating an archived document from the original
- Retrieving the content from the original
- Creating a thumbnail
- _optional:_ Retrieving a created date from the original
- _optional:_ Creating an archived document from the original
Custom parsers can be added to Paperless-ngx to support more file types. In
order to do that, you need to write the parser itself and announce its
@@ -439,14 +439,14 @@ def myparser_consumer_declaration(sender, **kwargs):
}
```
- `parser` is a reference to a class that extends `DocumentParser`.
- `weight` is used whenever two or more parsers are able to parse a
file: The parser with the higher weight wins. This can be used to
override the parsers provided by Paperless-ngx.
- `mime_types` is a dictionary. The keys are the mime types your
parser supports and the value is the default file extension that
Paperless-ngx should use when storing files and serving them for
download. We could guess that from the file extensions, but some
mime types have many extensions associated with them and the Python
methods responsible for guessing the extension do not always return
the same value.
- `parser` is a reference to a class that extends `DocumentParser`.
- `weight` is used whenever two or more parsers are able to parse a
file: The parser with the higher weight wins. This can be used to
override the parsers provided by Paperless-ngx.
- `mime_types` is a dictionary. The keys are the mime types your
parser supports and the value is the default file extension that
Paperless-ngx should use when storing files and serving them for
download. We could guess that from the file extensions, but some
mime types have many extensions associated with them and the Python
methods responsible for guessing the extension do not always return
the same value.
+30 -22
View File
@@ -40,28 +40,28 @@ system. On Linux, chances are high that this location is
You can always drag those files out of that folder to use them
elsewhere. Here are a couple notes about that.
- Paperless-ngx never modifies your original documents. It keeps
checksums of all documents and uses a scheduled sanity checker to
check that they remain the same.
- By default, paperless uses the internal ID of each document as its
filename. This might not be very convenient for export. However, you
can adjust the way files are stored in paperless by
[configuring the filename format](advanced_usage.md#file-name-handling).
- [The exporter](administration.md#exporter) is
another easy way to get your files out of paperless with reasonable
file names.
- Paperless-ngx never modifies your original documents. It keeps
checksums of all documents and uses a scheduled sanity checker to
check that they remain the same.
- By default, paperless uses the internal ID of each document as its
filename. This might not be very convenient for export. However, you
can adjust the way files are stored in paperless by
[configuring the filename format](advanced_usage.md#file-name-handling).
- [The exporter](administration.md#exporter) is
another easy way to get your files out of paperless with reasonable
file names.
## _What file types does paperless-ngx support?_
**A:** Currently, the following files are supported:
- PDF documents, PNG images, JPEG images, TIFF images, GIF images and
WebP images are processed with OCR and converted into PDF documents.
- Plain text documents are supported as well and are added verbatim to
paperless.
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
Paperless also supports various Office documents (.docx, .doc, odt,
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
- PDF documents, PNG images, JPEG images, TIFF images, GIF images and
WebP images are processed with OCR and converted into PDF documents.
- Plain text documents are supported as well and are added verbatim to
paperless.
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
Paperless also supports various Office documents (.docx, .doc, odt,
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
Paperless-ngx determines the type of a file by inspecting its content.
The file extensions do not matter.
@@ -127,8 +127,16 @@ ASGI-enabled web server as well that processes WebSocket connections,
and configure Apache to redirect WebSocket connections to this server.
Multiple options for ASGI servers exist:
- `gunicorn` with `uvicorn` as the worker implementation (the default
of paperless)
- `daphne` as a standalone server, which is the reference
implementation for ASGI.
- `uvicorn` as a standalone server
- `gunicorn` with `uvicorn` as the worker implementation (the default
of paperless)
- `daphne` as a standalone server, which is the reference
implementation for ASGI.
- `uvicorn` as a standalone server
## _What about the Redis licensing change and using one of the open source forks_?
Currently (October 2024), forks of Redis such as Valkey or Redirect are not officially supported by our upstream
libraries, so using one of these to replace Redis is not officially supported.
However, they do claim to be compatible with the Redis protocol and will likely work, but we will
not be updating from using Redis as the broker officially just yet.
+113 -113
View File
@@ -2,11 +2,11 @@
You can go multiple routes to setup and run Paperless:
- [Use the easy install docker script](#docker_script)
- [Pull the image from Docker Hub](#docker_hub)
- [Build the Docker image yourself](#docker_build)
- [Install Paperless directly on your system manually (bare metal)](#bare_metal)
- A user-maintained list of commercial hosting providers can be found [in the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects)
- [Use the easy install docker script](#docker_script)
- [Pull the image from Docker Hub](#docker_hub)
- [Build the Docker image yourself](#docker_build)
- [Install Paperless directly on your system manually (bare metal)](#bare_metal)
- A user-maintained list of commercial hosting providers can be found [in the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects)
The Docker routes are quick & easy. These are the recommended routes.
This configures all the stuff from the above automatically so that it
@@ -105,14 +105,14 @@ steps described in [Docker setup](#docker_hub) automatically.
```yaml
ports:
- 8000:8000
- 8000:8000
```
Replace the part BEFORE the colon with a port of your choice:
```yaml
ports:
- 8010:8000
- 8010:8000
```
Don't change the part after the colon or edit other lines that
@@ -129,11 +129,11 @@ steps described in [Docker setup](#docker_hub) automatically.
If you want to run Paperless as a rootless container, you will need
to do the following in your `docker-compose.yml`:
- set the `user` running the container to map to the `paperless`
user in the container. This value (`user_id` below), should be
the same id that `USERMAP_UID` and `USERMAP_GID` are set to in
the next step. See `USERMAP_UID` and `USERMAP_GID`
[here](configuration.md#docker).
- set the `user` running the container to map to the `paperless`
user in the container. This value (`user_id` below), should be
the same id that `USERMAP_UID` and `USERMAP_GID` are set to in
the next step. See `USERMAP_UID` and `USERMAP_GID`
[here](configuration.md#docker).
Your entry for Paperless should contain something like:
@@ -222,7 +222,7 @@ steps described in [Docker setup](#docker_hub) automatically.
```yaml
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
image: ghcr.io/paperless-ngx/paperless-ngx:latest
```
and replace it with a line that instructs Docker Compose to build
@@ -230,8 +230,8 @@ steps described in [Docker setup](#docker_hub) automatically.
```yaml
webserver:
build:
context: .
build:
context: .
```
4. Follow steps 3 to 8 of [Docker Setup](#docker_hub). When asked to run
@@ -250,49 +250,48 @@ a minimal installation of Debian/Buster, which is the current stable
release at the time of writing. Windows is not and will never be
supported.
Paperless requires Python 3. At this time, 3.9 - 3.11 are tested versions.
Paperless requires Python 3. At this time, 3.10 - 3.12 are tested versions.
Newer versions may work, but some dependencies may not fully support newer versions.
Support for older Python versions may be dropped as they reach end of life or as newer versions
are released, dependency support is confirmed, etc.
1. Install dependencies. Paperless requires the following packages.
- `python3`
- `python3-pip`
- `python3-dev`
- `default-libmysqlclient-dev` for MariaDB
- `pkg-config` for mysqlclient (python dependency)
- `fonts-liberation` for generating thumbnails for plain text
files
- `imagemagick` >= 6 for PDF conversion
- `gnupg` for handling encrypted documents
- `libpq-dev` for PostgreSQL
- `libmagic-dev` for mime type detection
- `mariadb-client` for MariaDB compile time
- `mime-support` for mime type detection
- `libzbar0` for barcode detection
- `poppler-utils` for barcode detection
- `python3`
- `python3-pip`
- `python3-dev`
- `default-libmysqlclient-dev` for MariaDB
- `pkg-config` for mysqlclient (python dependency)
- `fonts-liberation` for generating thumbnails for plain text
files
- `imagemagick` >= 6 for PDF conversion
- `gnupg` for handling encrypted documents
- `libpq-dev` for PostgreSQL
- `libmagic-dev` for mime type detection
- `mariadb-client` for MariaDB compile time
- `libzbar0` for barcode detection
- `poppler-utils` for barcode detection
Use this list for your preferred package management:
```
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev pkg-config libmagic-dev mime-support libzbar0 poppler-utils
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev pkg-config libmagic-dev libzbar0 poppler-utils
```
These dependencies are required for OCRmyPDF, which is used for text
recognition.
- `unpaper`
- `ghostscript`
- `icc-profiles-free`
- `qpdf`
- `liblept5`
- `libxml2`
- `pngquant` (suggested for certain PDF image optimizations)
- `zlib1g`
- `tesseract-ocr` >= 4.0.0 for OCR
- `tesseract-ocr` language packs (`tesseract-ocr-eng`,
`tesseract-ocr-deu`, etc)
- `unpaper`
- `ghostscript`
- `icc-profiles-free`
- `qpdf`
- `liblept5`
- `libxml2`
- `pngquant` (suggested for certain PDF image optimizations)
- `zlib1g`
- `tesseract-ocr` >= 4.0.0 for OCR
- `tesseract-ocr` language packs (`tesseract-ocr-eng`,
`tesseract-ocr-deu`, etc)
Use this list for your preferred package management:
@@ -302,14 +301,15 @@ are released, dependency support is confirmed, etc.
On Raspberry Pi, these libraries are required as well:
- `libatlas-base-dev`
- `libxslt1-dev`
- `libatlas-base-dev`
- `libxslt1-dev`
- `mime-support`
You will also need these for installing some of the python dependencies:
- `build-essential`
- `python3-setuptools`
- `python3-wheel`
- `build-essential`
- `python3-setuptools`
- `python3-wheel`
Use this list for your preferred package management:
@@ -361,33 +361,33 @@ are released, dependency support is confirmed, etc.
needs. Required settings for getting
paperless running are:
- [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) should point to your redis server, such as
<redis://localhost:6379>.
- [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) optional, and should be one of `postgres`,
`mariadb`, or `sqlite`
- [`PAPERLESS_DBHOST`](configuration.md#PAPERLESS_DBHOST) should be the hostname on which your
PostgreSQL server is running. Do not configure this to use
SQLite instead. Also configure port, database name, user and
password as necessary.
- [`PAPERLESS_CONSUMPTION_DIR`](configuration.md#PAPERLESS_CONSUMPTION_DIR) should point to a folder which
paperless should watch for documents. You might want to have
this somewhere else. Likewise, [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) and
[`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) define where paperless stores its data.
If you like, you can point both to the same directory.
- [`PAPERLESS_SECRET_KEY`](configuration.md#PAPERLESS_SECRET_KEY) should be a random sequence of
characters. It's used for authentication. Failure to do so
allows third parties to forge authentication credentials.
- [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) if you are behind a reverse proxy. This should
point to your domain. Please see
[configuration](configuration.md) for more
information.
- [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) should point to your redis server, such as
<redis://localhost:6379>.
- [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) optional, and should be one of `postgres`,
`mariadb`, or `sqlite`
- [`PAPERLESS_DBHOST`](configuration.md#PAPERLESS_DBHOST) should be the hostname on which your
PostgreSQL server is running. Do not configure this to use
SQLite instead. Also configure port, database name, user and
password as necessary.
- [`PAPERLESS_CONSUMPTION_DIR`](configuration.md#PAPERLESS_CONSUMPTION_DIR) should point to a folder which
paperless should watch for documents. You might want to have
this somewhere else. Likewise, [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) and
[`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) define where paperless stores its data.
If you like, you can point both to the same directory.
- [`PAPERLESS_SECRET_KEY`](configuration.md#PAPERLESS_SECRET_KEY) should be a random sequence of
characters. It's used for authentication. Failure to do so
allows third parties to forge authentication credentials.
- [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) if you are behind a reverse proxy. This should
point to your domain. Please see
[configuration](configuration.md) for more
information.
Many more adjustments can be made to paperless, especially the OCR
part. The following options are recommended for everyone:
- Set [`PAPERLESS_OCR_LANGUAGE`](configuration.md#PAPERLESS_OCR_LANGUAGE) to the language most of your
documents are written in.
- Set [`PAPERLESS_TIME_ZONE`](configuration.md#PAPERLESS_TIME_ZONE) to your local time zone.
- Set [`PAPERLESS_OCR_LANGUAGE`](configuration.md#PAPERLESS_OCR_LANGUAGE) to the language most of your
documents are written in.
- Set [`PAPERLESS_TIME_ZONE`](configuration.md#PAPERLESS_TIME_ZONE) to your local time zone.
!!! warning
@@ -395,9 +395,9 @@ are released, dependency support is confirmed, etc.
7. Create the following directories if they are missing:
- `/opt/paperless/media`
- `/opt/paperless/data`
- `/opt/paperless/consume`
- `/opt/paperless/media`
- `/opt/paperless/data`
- `/opt/paperless/consume`
Adjust as necessary if you configured different folders.
Ensure that the paperless user has write permissions for every one
@@ -586,21 +586,21 @@ your setup depending on how you installed paperless.
This setup describes how to update an existing paperless Docker
installation. The important things to keep in mind are as follows:
- Read the [changelog](changelog.md) and
take note of breaking changes.
- You should decide if you want to stick with SQLite or want to
migrate your database to PostgreSQL. See [documentation](#sqlite_to_psql)
for details on
how to move your data from SQLite to PostgreSQL. Both work fine with
paperless. However, if you already have a database server running
for other services, you might as well use it for paperless as well.
- The task scheduler of paperless, which is used to execute periodic
tasks such as email checking and maintenance, requires a
[redis](https://redis.io/) message broker instance. The
Docker Compose route takes care of that.
- The layout of the folder structure for your documents and data
remains the same, so you can just plug your old docker volumes into
paperless-ngx and expect it to find everything where it should be.
- Read the [changelog](changelog.md) and
take note of breaking changes.
- You should decide if you want to stick with SQLite or want to
migrate your database to PostgreSQL. See [documentation](#sqlite_to_psql)
for details on
how to move your data from SQLite to PostgreSQL. Both work fine with
paperless. However, if you already have a database server running
for other services, you might as well use it for paperless as well.
- The task scheduler of paperless, which is used to execute periodic
tasks such as email checking and maintenance, requires a
[redis](https://redis.io/) message broker instance. The
Docker Compose route takes care of that.
- The layout of the folder structure for your documents and data
remains the same, so you can just plug your old docker volumes into
paperless-ngx and expect it to find everything where it should be.
Migration to paperless-ngx is then performed in a few simple steps:
@@ -763,30 +763,30 @@ Paperless runs on Raspberry Pi. However, some things are rather slow on
the Pi and configuring some options in paperless can help improve
performance immensely:
- Stick with SQLite to save some resources.
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will
only OCR the first page of your documents. In most cases, this page
contains enough information to be able to find it.
- [`PAPERLESS_TASK_WORKERS`](configuration.md#PAPERLESS_TASK_WORKERS) and [`PAPERLESS_THREADS_PER_WORKER`](configuration.md#PAPERLESS_THREADS_PER_WORKER) are
configured to use all cores. The Raspberry Pi models 3 and up have 4
cores, meaning that paperless will use 2 workers and 2 threads per
worker. This may result in sluggish response times during
consumption, so you might want to lower these settings (example: 2
workers and 1 thread to always have some computing power left for
other tasks).
- Keep [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) at its default value `skip` and consider
OCR'ing your documents before feeding them into paperless. Some
scanners are able to do this!
- Set [`PAPERLESS_OCR_SKIP_ARCHIVE_FILE`](configuration.md#PAPERLESS_OCR_SKIP_ARCHIVE_FILE) to `with_text` to skip archive
file generation for already ocr'ed documents, or `always` to skip it
for all documents.
- If you want to perform OCR on the device, consider using
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
less memory at the expense of slightly worse OCR results.
- If using docker, consider setting [`PAPERLESS_WEBSERVER_WORKERS`](configuration.md#PAPERLESS_WEBSERVER_WORKERS) to 1. This will save some memory.
- Consider setting [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) to false, to disable the
more advanced language processing, which can take more memory and
processing time.
- Stick with SQLite to save some resources.
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will
only OCR the first page of your documents. In most cases, this page
contains enough information to be able to find it.
- [`PAPERLESS_TASK_WORKERS`](configuration.md#PAPERLESS_TASK_WORKERS) and [`PAPERLESS_THREADS_PER_WORKER`](configuration.md#PAPERLESS_THREADS_PER_WORKER) are
configured to use all cores. The Raspberry Pi models 3 and up have 4
cores, meaning that paperless will use 2 workers and 2 threads per
worker. This may result in sluggish response times during
consumption, so you might want to lower these settings (example: 2
workers and 1 thread to always have some computing power left for
other tasks).
- Keep [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) at its default value `skip` and consider
OCR'ing your documents before feeding them into paperless. Some
scanners are able to do this!
- Set [`PAPERLESS_OCR_SKIP_ARCHIVE_FILE`](configuration.md#PAPERLESS_OCR_SKIP_ARCHIVE_FILE) to `with_text` to skip archive
file generation for already ocr'ed documents, or `always` to skip it
for all documents.
- If you want to perform OCR on the device, consider using
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
less memory at the expense of slightly worse OCR results.
- If using docker, consider setting [`PAPERLESS_WEBSERVER_WORKERS`](configuration.md#PAPERLESS_WEBSERVER_WORKERS) to 1. This will save some memory.
- Consider setting [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) to false, to disable the
more advanced language processing, which can take more memory and
processing time.
For details, refer to [configuration](configuration.md).
+40 -26
View File
@@ -4,27 +4,27 @@
Check for the following issues:
- Ensure that the directory you're putting your documents in is the
folder paperless is watching. With docker, this setting is performed
in the `docker-compose.yml` file. Without Docker, look at the
`CONSUMPTION_DIR` setting. Don't adjust this setting if you're
using docker.
- Ensure that the directory you're putting your documents in is the
folder paperless is watching. With docker, this setting is performed
in the `docker-compose.yml` file. Without Docker, look at the
`CONSUMPTION_DIR` setting. Don't adjust this setting if you're
using docker.
- Ensure that redis is up and running. Paperless does its task
processing asynchronously, and for documents to arrive at the task
processor, it needs redis to run.
- Ensure that redis is up and running. Paperless does its task
processing asynchronously, and for documents to arrive at the task
processor, it needs redis to run.
- Ensure that the task processor is running. Docker does this
automatically. Manually invoke the task processor by executing
- Ensure that the task processor is running. Docker does this
automatically. Manually invoke the task processor by executing
```shell-session
$ celery --app paperless worker
```
```shell-session
$ celery --app paperless worker
```
- Look at the output of paperless and inspect it for any errors.
- Look at the output of paperless and inspect it for any errors.
- Go to the admin interface, and check if there are failed tasks. If
so, the tasks will contain an error message.
- Go to the admin interface, and check if there are failed tasks. If
so, the tasks will contain an error message.
## Consumer warns `OCR for XX failed`
@@ -78,12 +78,12 @@ Ensure that `chown` is possible on these directories.
This indicates that the Auto matching algorithm found no documents to
learn from. This may have two reasons:
- You don't use the Auto matching algorithm: The error can be safely
ignored in this case.
- You are using the Auto matching algorithm: The classifier explicitly
excludes documents with Inbox tags. Verify that there are documents
in your archive without inbox tags. The algorithm will only learn
from documents not in your inbox.
- You don't use the Auto matching algorithm: The error can be safely
ignored in this case.
- You are using the Auto matching algorithm: The classifier explicitly
excludes documents with Inbox tags. Verify that there are documents
in your archive without inbox tags. The algorithm will only learn
from documents not in your inbox.
## UserWarning in sklearn on every single document
@@ -127,10 +127,10 @@ change in the `docker-compose.yml` file:
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
command:
- 'gotenberg'
- '--chromium-disable-javascript=true'
- '--chromium-allow-list=file:///tmp/.*'
- '--api-timeout=60'
- 'gotenberg'
- '--chromium-disable-javascript=true'
- '--chromium-allow-list=file:///tmp/.*'
- '--api-timeout=60'
```
## Permission denied errors in the consumption directory
@@ -353,6 +353,20 @@ ways from the original. As the logs indicate, if you encounter this error you ca
`PAPERLESS_OCR_USER_ARGS: '{"continue_on_soft_render_error": true}'` to try to 'force'
processing documents with this issue.
## Logs show "possible incompatible database column" when deleting documents {#convert-uuid-field}
You may see errors when deleting documents like:
```
Data too long for column 'transaction_id' at row 1
```
This error can occur in installations which have upgraded from a version of Paperless-ngx that used Django 4 (Paperless-ngx versions prior to v2.13.0) with a MariaDB/MySQL database. Due to the backawards-incompatible change in Django 5, the column "documents_document.transaction_id" will need to be re-created, which can be done with a one-time run of the following management command:
```shell-session
$ python3 manage.py convert_mariadb_uuid
```
## Platform-Specific Deployment Troubleshooting
A user-maintained wiki page is available to help troubleshoot issues that may arise when trying to deploy Paperless-ngx on specific platforms, for example SELinux. Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Platform%E2%80%90Specific-Troubleshooting).
+220 -176
View File
@@ -10,37 +10,37 @@ and provides many utilities for finding and managing your documents.
Paperless essentially consists of two different parts for managing your
documents:
- The _consumer_ watches a specified folder and adds all documents in
that folder to paperless.
- The _web server_ provides a UI that you use to manage and search for
your scanned documents.
- The _consumer_ watches a specified folder and adds all documents in
that folder to paperless.
- The _web server_ provides a UI that you use to manage and search for
your scanned documents.
Each document has a couple of fields that you can assign to them:
- A _Document_ is a piece of paper that sometimes contains valuable
information.
- The _correspondent_ of a document is the person, institution or
company that a document either originates from, or is sent to.
- A _tag_ is a label that you can assign to documents. Think of labels
as more powerful folders: Multiple documents can be grouped together
with a single tag, however, a single document can also have multiple
tags. This is not possible with folders. The reason folders are not
implemented in paperless is simply that tags are much more versatile
than folders.
- A _document type_ is used to demarcate the type of a document such
as letter, bank statement, invoice, contract, etc. It is used to
identify what a document is about.
- The _date added_ of a document is the date the document was scanned
into paperless. You cannot and should not change this date.
- The _date created_ of a document is the date the document was
initially issued. This can be the date you bought a product, the
date you signed a contract, or the date a letter was sent to you.
- The _archive serial number_ (short: ASN) of a document is the
identifier of the document in your physical document binders. See
[recommended workflow](#usage-recommended-workflow) below.
- The _content_ of a document is the text that was OCR'ed from the
document. This text is fed into the search engine and is used for
matching tags, correspondents and document types.
- A _Document_ is a piece of paper that sometimes contains valuable
information.
- The _correspondent_ of a document is the person, institution or
company that a document either originates from, or is sent to.
- A _tag_ is a label that you can assign to documents. Think of labels
as more powerful folders: Multiple documents can be grouped together
with a single tag, however, a single document can also have multiple
tags. This is not possible with folders. The reason folders are not
implemented in paperless is simply that tags are much more versatile
than folders.
- A _document type_ is used to demarcate the type of a document such
as letter, bank statement, invoice, contract, etc. It is used to
identify what a document is about.
- The _date added_ of a document is the date the document was scanned
into paperless. You cannot and should not change this date.
- The _date created_ of a document is the date the document was
initially issued. This can be the date you bought a product, the
date you signed a contract, or the date a letter was sent to you.
- The _archive serial number_ (short: ASN) of a document is the
identifier of the document in your physical document binders. See
[recommended workflow](#usage-recommended-workflow) below.
- The _content_ of a document is the text that was OCR'ed from the
document. This text is fed into the search engine and is used for
matching tags, correspondents and document types.
## Adding documents to paperless
@@ -112,7 +112,7 @@ process.
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects) for a user-maintained list of related projects and
software (e.g. for mobile devices) that is compatible with Paperless-ngx.
### IMAP (Email) {#usage-email}
### Email {#usage-email}
You can tell paperless-ngx to consume documents from your email
accounts. This is a very flexible and powerful feature, if you regularly
@@ -142,21 +142,21 @@ patterns can include wildcards and multiple patterns separated by a comma.
The actions all ensure that the same mail is not consumed twice by
different means. These are as follows:
- **Delete:** Immediately deletes mail that paperless has consumed
documents from. Use with caution.
- **Mark as read:** Mark consumed mail as read. Paperless will not
consume documents from already read mails. If you read a mail before
paperless sees it, it will be ignored.
- **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 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
this feature!
- **Delete:** Immediately deletes mail that paperless has consumed
documents from. Use with caution.
- **Mark as read:** Mark consumed mail as read. Paperless will not
consume documents from already read mails. If you read a mail before
paperless sees it, it will be ignored.
- **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 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
this feature!
- **Apple Mail support:** Apple Mail clients allow differently colored tags. For this to work use `apple:<color>` (e.g. _apple:green_) as a custom tag. Available colors are _red_, _orange_, _yellow_, _blue_, _green_, _violet_ and _grey_.
- **Apple Mail support:** Apple Mail clients allow differently colored tags. For this to work use `apple:<color>` (e.g. _apple:green_) as a custom tag. Available colors are _red_, _orange_, _yellow_, _blue_, _green_, _violet_ and _grey_.
!!! warning
@@ -200,6 +200,14 @@ different means. These are as follows:
Paperless is set up to check your mails every 10 minutes. This can be
configured via [`PAPERLESS_EMAIL_TASK_CRON`](configuration.md#PAPERLESS_EMAIL_TASK_CRON)
#### OAuth Email Setup
Paperless-ngx supports OAuth2 authentication for Gmail and Outlook email accounts. To set up an email account with OAuth2, you will need to create a 'developer' app with the respective provider and obtain the client ID and client secret and set the appropriate [configuration variables](configuration.md#email_oauth). You will also need to set either [`PAPERLESS_OAUTH_CALLBACK_BASE_URL`](configuration.md#PAPERLESS_OAUTH_CALLBACK_BASE_URL) or [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) to the correct value for the OAuth2 flow to work correctly.
Specific instructions for setting up the required 'developer' app with Google or Microsoft are beyond the scope of this documentation, but you can find user-maintained instructions in [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Email-OAuth-App-Setup) or by searching the web.
Once setup, navigating to the email settings page in Paperless-ngx will allow you to add an email account for Gmail or Outlook using OAuth2. After authenticating, you will be presented with the newly-created account where you will need to enter and save your email address. After this, the account will work as any other email account in Paperless-ngx and refreshing tokens will be handled automatically.
### REST API
You can also submit a document using the REST API, see [POSTing documents](api.md#file-uploads)
@@ -291,6 +299,12 @@ In order to enable the password reset feature you will need to setup an SMTP bac
[`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST). If your installation does not have
[`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) set, the reset link included in emails will use the server host.
### Two-factor authentication
Users can enable two-factor authentication (2FA) for their accounts from the 'My Profile' dialog. Opening the dropdown reveals a QR code that can be scanned by a 2FA app (e.g. Google Authenticator) to generate a code. The code must then be entered in the dialog to enable 2FA. If the code is accepted and 2FA is enabled, the user will be shown a set of 10 recovery codes that can be used to login in the event that the 2FA device is lost or unavailable. These codes should be stored securely and cannot be retrieved again. Once enabled, users will be required to enter a code from their 2FA app when logging in.
Should a user lose access to their 2FA device and all recovery codes, a superuser can disable 2FA for the user from the 'Users & Groups' management screen.
## Workflows
!!! note
@@ -308,6 +322,8 @@ fields and permissions, which will be merged.
### Workflow Triggers
#### Types
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
@@ -317,8 +333,10 @@ Currently, there are three events that correspond to workflow trigger 'types':
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.
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date.
The following flow diagram illustrates the three trigger types:
The following flow diagram illustrates the three document trigger types:
```mermaid
flowchart TD
@@ -352,62 +370,88 @@ flowchart TD
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 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
- 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 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
### Workflow Actions
There are currently two types of workflow actions, "Assignment", which can assign:
#### Types
- Title, see [title placeholders](usage.md#title-placeholders) below
- Tags, correspondent, document type and storage path
- Document owner
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
The following workflow action types are available:
and "Removal" actions, which can remove either all of or specific sets of the following:
##### Assignment
- Tags, correspondents, document types or storage paths
- Document owner
- View and / or edit permissions
- Custom fields
"Assignment" actions can assign:
#### Title placeholders
- Title, see [workflow placeholders](usage.md#workflow-placeholders) below
- Tags, correspondent, document type and storage path
- Document owner
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
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
##### Removal
"Removal" actions can remove either all of or specific sets of the following:
- Tags, correspondents, document types or storage paths
- Document owner
- View and / or edit permissions
- Custom fields
##### Email
"Email" actions can send documents via email. This action requires a mail server to be [configured](configuration.md#email-sending). You can specify:
- The recipient email address(es) separated by commas
- The subject and body of the email, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below
- Whether to include the document as an attachment
##### Webhook
"Webhook" actions send a POST request to a specified URL. You can specify:
- The URL to send the request to
- The request body as text or as key-value pairs, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below.
- The request headers as key-value pairs
#### Workflow placeholders
Some workflow text 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 text 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
- `{owner_username}`: assigned owner username
- `{added}`: added datetime
- `{added_year}`: added year
- `{added_year_short}`: added year
- `{added_month}`: added month
- `{added_month_name}`: added month name
- `{added_month_name_short}`: added month short name
- `{added_day}`: added day
- `{added_time}`: added time in HH:MM format
- `{original_filename}`: original file name without extension
- `{correspondent}`: assigned correspondent name
- `{document_type}`: assigned document type name
- `{owner_username}`: assigned owner username
- `{added}`: added datetime
- `{added_year}`: added year
- `{added_year_short}`: added year
- `{added_month}`: added month
- `{added_month_name}`: added month name
- `{added_month_name_short}`: added month short name
- `{added_day}`: added day
- `{added_time}`: added time in HH:MM format
- `{original_filename}`: original file name without extension
- `{filename}`: current 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
- `{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
- `{doc_url}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
### Workflow permissions
@@ -442,24 +486,24 @@ Multiple fields may be attached to a document but the same field name cannot be
The following custom field types are supported:
- `Text`: any text
- `Boolean`: true / false (check / unchecked) field
- `Date`: date
- `URL`: a valid url
- `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456
- `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
- `Select`: a pre-defined list of strings from which the user can choose
- `Text`: any text
- `Boolean`: true / false (check / unchecked) field
- `Date`: date
- `URL`: a valid url
- `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456
- `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
- `Select`: a pre-defined list of strings from which the user can choose
## Share Links
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}`.
- Links can optionally have an expiration time set.
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
- 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}`.
- Links can optionally have an expiration time set.
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
!!! tip
@@ -469,10 +513,10 @@ Paperless-ngx added the ability to create shareable links to files in version 2.
Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):
- Merging documents: available when selecting multiple documents for 'bulk editing'.
- Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
- Splitting documents: available from an individual document's details page.
- Deleting pages: available from an individual document's details page.
- Merging documents: available when selecting multiple documents for 'bulk editing'.
- Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
- Splitting documents: available from an individual document's details page.
- Deleting pages: available from an individual document's details page.
!!! important
@@ -550,18 +594,18 @@ the system.
Here are a couple examples of tags and types that you could use in your
collection.
- An `inbox` tag for newly added documents that you haven't manually
edited yet.
- A tag `car` for everything car related (repairs, registration,
insurance, etc)
- A tag `todo` for documents that you still need to do something with,
such as reply, or perform some task online.
- A tag `bank account x` for all bank statement related to that
account.
- A tag `mail` for anything that you added to paperless via its mail
processing capabilities.
- A tag `missing_metadata` when you still need to add some metadata to
a document, but can't or don't want to do this right now.
- An `inbox` tag for newly added documents that you haven't manually
edited yet.
- A tag `car` for everything car related (repairs, registration,
insurance, etc)
- A tag `todo` for documents that you still need to do something with,
such as reply, or perform some task online.
- A tag `bank account x` for all bank statement related to that
account.
- A tag `mail` for anything that you added to paperless via its mail
processing capabilities.
- A tag `missing_metadata` when you still need to add some metadata to
a document, but can't or don't want to do this right now.
## Searching {#basic-usage_searching}
@@ -650,8 +694,8 @@ The following diagram shows how easy it is to manage your documents.
### Preparations in paperless
- Create an inbox tag that gets assigned to all new documents.
- Create a TODO tag.
- Create an inbox tag that gets assigned to all new documents.
- Create a TODO tag.
### Processing of the physical documents
@@ -725,78 +769,78 @@ Some documents require attention and require you to act on the document.
You may take two different approaches to handle these documents based on
how regularly you intend to scan documents and use paperless.
- If you scan and process your documents in paperless regularly,
assign a TODO tag to all scanned documents that you need to process.
Create a saved view on the dashboard that shows all documents with
this tag.
- If you do not scan documents regularly and use paperless solely for
archiving, create a physical todo box next to your physical inbox
and put documents you need to process in the TODO box. When you
performed the task associated with the document, move it to the
inbox.
- If you scan and process your documents in paperless regularly,
assign a TODO tag to all scanned documents that you need to process.
Create a saved view on the dashboard that shows all documents with
this tag.
- If you do not scan documents regularly and use paperless solely for
archiving, create a physical todo box next to your physical inbox
and put documents you need to process in the TODO box. When you
performed the task associated with the document, move it to the
inbox.
## Architecture
Paperless-ngx consists of the following components:
- **The webserver:** This serves the administration pages, the API,
and the new frontend. This is the main tool you'll be using to interact
with paperless. You may start the webserver directly with
- **The webserver:** This serves the administration pages, the API,
and the new frontend. This is the main tool you'll be using to interact
with paperless. You may start the webserver directly with
```shell-session
$ cd /path/to/paperless/src/
$ gunicorn -c ../gunicorn.conf.py paperless.wsgi
```
```shell-session
$ cd /path/to/paperless/src/
$ gunicorn -c ../gunicorn.conf.py paperless.wsgi
```
or by any other means such as Apache `mod_wsgi`.
or by any other means such as Apache `mod_wsgi`.
- **The consumer:** This is what watches your consumption folder for
documents. However, the consumer itself does not really consume your
documents. Now it notifies a task processor that a new file is ready
for consumption. I suppose it should be named differently. This was
also used to check your emails, but that's now done elsewhere as
well.
- **The consumer:** This is what watches your consumption folder for
documents. However, the consumer itself does not really consume your
documents. Now it notifies a task processor that a new file is ready
for consumption. I suppose it should be named differently. This was
also used to check your emails, but that's now done elsewhere as
well.
Start the consumer with the management command `document_consumer`:
Start the consumer with the management command `document_consumer`:
```shell-session
$ cd /path/to/paperless/src/
$ python3 manage.py document_consumer
```
```shell-session
$ cd /path/to/paperless/src/
$ python3 manage.py document_consumer
```
- **The task processor:** Paperless relies on [Celery - Distributed
Task Queue](https://docs.celeryq.dev/en/stable/index.html) for doing
most of the heavy lifting. This is a task queue that accepts tasks
from multiple sources and processes these in parallel. It also comes
with a scheduler that executes certain commands periodically.
- **The task processor:** Paperless relies on [Celery - Distributed
Task Queue](https://docs.celeryq.dev/en/stable/index.html) for doing
most of the heavy lifting. This is a task queue that accepts tasks
from multiple sources and processes these in parallel. It also comes
with a scheduler that executes certain commands periodically.
This task processor is responsible for:
This task processor is responsible for:
- Consuming documents. When the consumer finds new documents, it
notifies the task processor to start a consumption task.
- The task processor also performs the consumption of any
documents you upload through the web interface.
- Consuming emails. It periodically checks your configured
accounts for new emails and notifies the task processor to
consume the attachment of an email.
- Maintaining the search index and the automatic matching
algorithm. These are things that paperless needs to do from time
to time in order to operate properly.
- Consuming documents. When the consumer finds new documents, it
notifies the task processor to start a consumption task.
- The task processor also performs the consumption of any
documents you upload through the web interface.
- Consuming emails. It periodically checks your configured
accounts for new emails and notifies the task processor to
consume the attachment of an email.
- Maintaining the search index and the automatic matching
algorithm. These are things that paperless needs to do from time
to time in order to operate properly.
This allows paperless to process multiple documents from your
consumption folder in parallel! On a modern multi core system, this
makes the consumption process with full OCR blazingly fast.
This allows paperless to process multiple documents from your
consumption folder in parallel! On a modern multi core system, this
makes the consumption process with full OCR blazingly fast.
The task processor comes with a built-in admin interface that you
can use to check whenever any of the tasks fail and inspect the
errors (i.e., wrong email credentials, errors during consuming a
specific file, etc).
The task processor comes with a built-in admin interface that you
can use to check whenever any of the tasks fail and inspect the
errors (i.e., wrong email credentials, errors during consuming a
specific file, etc).
- A [redis](https://redis.io/) message broker: This is a really
lightweight service that is responsible for getting the tasks from
the webserver and the consumer to the task scheduler. These run in a
different process (maybe even on different machines!), and
therefore, this is necessary.
- A [redis](https://redis.io/) message broker: This is a really
lightweight service that is responsible for getting the tasks from
the webserver and the consumer to the task scheduler. These run in a
different process (maybe even on different machines!), and
therefore, this is necessary.
- Optional: A database server. Paperless supports PostgreSQL, MariaDB
and SQLite for storing its data.
- Optional: A database server. Paperless supports PostgreSQL, MariaDB
and SQLite for storing its data.
+10 -5
View File
@@ -330,8 +330,13 @@ SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!#$%&()*+,-./:;<=>?@[\]^_`{|}~' < /dev/ur
DEFAULT_LANGUAGES=("deu eng fra ita spa")
_split_langs="${OCR_LANGUAGE//+/ }"
read -r -a OCR_LANGUAGES_ARRAY <<< "${_split_langs}"
# OCR_LANG requires underscores, replace dashes if the user gave them with underscores
readonly ocr_langs=${OCR_LANGUAGE//-/_}
# OCR_LANGS (the install version) uses dashes, not underscores, so convert underscore to dash and plus to space
install_langs=${OCR_LANGUAGE//_/-} # First convert any underscores to dashes
install_langs=${install_langs//+/ } # Then convert plus signs to spaces
read -r -a install_langs_array <<< "${install_langs}"
{
if [[ ! $URL == "" ]] ; then
@@ -344,10 +349,10 @@ read -r -a OCR_LANGUAGES_ARRAY <<< "${_split_langs}"
echo "USERMAP_GID=$USERMAP_GID"
fi
echo "PAPERLESS_TIME_ZONE=$TIME_ZONE"
echo "PAPERLESS_OCR_LANGUAGE=$OCR_LANGUAGE"
echo "PAPERLESS_OCR_LANGUAGE=$ocr_langs"
echo "PAPERLESS_SECRET_KEY='$SECRET_KEY'"
if [[ ! ${DEFAULT_LANGUAGES[*]} =~ ${OCR_LANGUAGES_ARRAY[*]} ]] ; then
echo "PAPERLESS_OCR_LANGUAGES=${OCR_LANGUAGES_ARRAY[*]}"
if [[ ! ${DEFAULT_LANGUAGES[*]} =~ ${install_langs_array[*]} ]] ; then
echo "PAPERLESS_OCR_LANGUAGES=${install_langs_array[*]}"
fi
} > docker-compose.env
+1
View File
@@ -87,6 +87,7 @@
#PAPERLESS_TIKA_ENABLED=false
#PAPERLESS_TIKA_ENDPOINT=http://localhost:9998
#PAPERLESS_TIKA_GOTENBERG_ENDPOINT=http://localhost:3000
#PAPERLESS_EINVOICE_PARSER_ENABLED=true
# Binaries
+1 -2
View File
@@ -11,8 +11,7 @@
],
"parserOptions": {
"project": [
"tsconfig.json",
"e2e/tsconfig.json"
"tsconfig.json"
],
"createDefaultProgram": true
},
+6 -3
View File
@@ -52,8 +52,11 @@
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./extra-webpack.config.ts"
},
"outputPath": "dist/paperless-ui",
"outputHashing": "none",
"index": "src/index.html",
@@ -125,7 +128,7 @@
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"buildTarget": "paperless-ui:build:en-US"
},
@@ -136,7 +139,7 @@
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"builder": "@angular-builders/custom-webpack:extract-i18n",
"options": {
"buildTarget": "paperless-ui:build"
}
+3 -23
View File
@@ -1,6 +1,7 @@
import { test, expect } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'node:path'
const REQUESTS_HAR = 'e2e/admin/requests/api-settings.har'
const REQUESTS_HAR = path.join(__dirname, 'requests/api-settings.har')
test('should activate / deactivate save button when settings change', async ({
page,
@@ -33,24 +34,3 @@ test('should apply appearance changes when set', async ({ page }) => {
await page.getByLabel('Enable dark mode').click()
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', /dark/)
})
test('should toggle saved view options when set & saved', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/settings/savedviews')
await page.getByLabel('Show on dashboard').first().click()
await page.getByLabel('Show in sidebar').first().click()
const updatePromise = page.waitForRequest((request) => {
if (!request.url().includes('8')) return true // skip other saved views
const data = request.postDataJSON()
const isValid =
data['show_on_dashboard'] === true && data['show_in_sidebar'] === true
return (
isValid &&
request.method() === 'PATCH' &&
request.url().includes('/api/saved_views/')
)
})
await page.getByRole('button', { name: 'Save' }).scrollIntoViewIfNeeded()
await page.getByRole('button', { name: 'Save' }).click()
await updatePromise
})
+6 -5
View File
@@ -1,9 +1,10 @@
import { test, expect } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'node:path'
const REQUESTS_HAR1 = 'e2e/dashboard/requests/api-dashboard1.har'
const REQUESTS_HAR2 = 'e2e/dashboard/requests/api-dashboard2.har'
const REQUESTS_HAR3 = 'e2e/dashboard/requests/api-dashboard3.har'
const REQUESTS_HAR4 = 'e2e/dashboard/requests/api-dashboard4.har'
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-dashboard1.har')
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-dashboard2.har')
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-dashboard3.har')
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-dashboard4.har')
test('dashboard inbox link', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
@@ -1,7 +1,8 @@
import { test, expect } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'node:path'
const REQUESTS_HAR = 'e2e/document-detail/requests/api-document-detail.har'
const REQUESTS_HAR2 = 'e2e/document-detail/requests/api-document-detail2.har'
const REQUESTS_HAR = path.join(__dirname, 'requests/api-document-detail.har')
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-detail2.har')
test('should activate / deactivate save button when changes are saved', async ({
page,
@@ -1,11 +1,12 @@
import { test, expect } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'node:path'
const REQUESTS_HAR1 = 'e2e/document-list/requests/api-document-list1.har'
const REQUESTS_HAR2 = 'e2e/document-list/requests/api-document-list2.har'
const REQUESTS_HAR3 = 'e2e/document-list/requests/api-document-list3.har'
const REQUESTS_HAR4 = 'e2e/document-list/requests/api-document-list4.har'
const REQUESTS_HAR5 = 'e2e/document-list/requests/api-document-list5.har'
const REQUESTS_HAR6 = 'e2e/document-list/requests/api-document-list6.har'
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-document-list1.har')
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-list2.har')
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-document-list3.har')
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-document-list4.har')
const REQUESTS_HAR5 = path.join(__dirname, 'requests/api-document-list5.har')
const REQUESTS_HAR6 = path.join(__dirname, 'requests/api-document-list6.har')
test('basic filtering', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
@@ -1,6 +1,7 @@
import { test, expect } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'node:path'
const REQUESTS_HAR = 'e2e/permissions/requests/api-global-permissions.har'
const REQUESTS_HAR = path.join(__dirname, 'requests/api-global-permissions.har')
test('should not allow user to edit settings', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
+24
View File
@@ -0,0 +1,24 @@
import {
CustomWebpackBrowserSchema,
TargetOptions,
} from '@angular-builders/custom-webpack'
import * as webpack from 'webpack'
const { codecovWebpackPlugin } = require('@codecov/webpack-plugin')
export default (
config: webpack.Configuration,
options: CustomWebpackBrowserSchema,
targetOptions: TargetOptions
) => {
if (config.plugins) {
config.plugins.push(
codecovWebpackPlugin({
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
bundleName: 'paperless-ngx',
uploadToken: process.env.CODECOV_TOKEN,
})
)
}
return config
}
+2131 -1279
View File
File diff suppressed because it is too large Load Diff
+2409 -3629
View File
File diff suppressed because it is too large Load Diff
+34 -29
View File
@@ -11,58 +11,63 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^18.2.2",
"@angular/common": "~18.2.2",
"@angular/compiler": "~18.2.2",
"@angular/core": "~18.2.2",
"@angular/forms": "~18.2.2",
"@angular/localize": "~18.2.2",
"@angular/platform-browser": "~18.2.2",
"@angular/platform-browser-dynamic": "~18.2.2",
"@angular/router": "~18.2.2",
"@angular/cdk": "^18.2.11",
"@angular/common": "~18.2.10",
"@angular/compiler": "~18.2.10",
"@angular/core": "~18.2.10",
"@angular/forms": "~18.2.10",
"@angular/localize": "~18.2.10",
"@angular/platform-browser": "~18.2.10",
"@angular/platform-browser-dynamic": "~18.2.10",
"@angular/router": "~18.2.10",
"@ng-bootstrap/ng-bootstrap": "^17.0.1",
"@ng-select/ng-select": "^13.7.0",
"@ng-select/ng-select": "^13.9.1",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.3",
"file-saver": "^2.0.5",
"mime-names": "^1.0.0",
"ng2-pdf-viewer": "^10.3.0",
"ng2-pdf-viewer": "^10.4.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^18.0.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^15.0.0",
"rxjs": "^7.8.1",
"tslib": "^2.7.0",
"uuid": "^10.0.0",
"tslib": "^2.8.1",
"utif": "^3.1.0",
"uuid": "^11.0.2",
"zone.js": "^0.14.8"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^18.0.0",
"@angular-builders/jest": "^18.0.0",
"@angular-devkit/build-angular": "^18.2.2",
"@angular-devkit/core": "^18.2.2",
"@angular-devkit/schematics": "^18.2.2",
"@angular-eslint/builder": "18.3.0",
"@angular-eslint/eslint-plugin": "18.3.0",
"@angular-eslint/eslint-plugin-template": "18.3.0",
"@angular-eslint/schematics": "18.3.0",
"@angular-eslint/template-parser": "18.3.0",
"@angular/cli": "~18.2.2",
"@angular-devkit/core": "^18.2.11",
"@angular-devkit/schematics": "^18.2.11",
"@angular-eslint/builder": "18.4.0",
"@angular-eslint/eslint-plugin": "18.4.0",
"@angular-eslint/eslint-plugin-template": "18.4.0",
"@angular-eslint/schematics": "18.4.0",
"@angular-eslint/template-parser": "18.4.0",
"@angular/cli": "~18.2.11",
"@angular/compiler-cli": "~18.2.2",
"@playwright/test": "^1.46.1",
"@types/jest": "^29.5.12",
"@types/node": "^22.0.2",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@codecov/webpack-plugin": "^1.2.1",
"@playwright/test": "^1.48.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.8.6",
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"@typescript-eslint/utils": "^8.0.0",
"eslint": "^9.9.1",
"eslint": "^9.14.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^14.2.2",
"jest-preset-angular": "^14.2.4",
"jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0",
"prettier-plugin-organize-imports": "^4.1.0",
"ts-node": "~10.9.1",
"typescript": "^5.5.4"
}
},
"typings": "./src/typings.d.ts"
}
+2 -2
View File
@@ -1,9 +1,9 @@
import '@angular/localize/init'
import { jest } from '@jest/globals'
import { TextDecoder, TextEncoder } from 'util'
if (process.env.NODE_ENV === 'test') {
require('jest-preset-angular/setup-jest')
}
import '@angular/localize/init'
import { TextEncoder, TextDecoder } from 'util'
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
+31 -15
View File
@@ -1,32 +1,33 @@
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { RouterModule, Routes } from '@angular/router'
import { ConfigComponent } from './components/admin/config/config.component'
import { LogsComponent } from './components/admin/logs/logs.component'
import { SettingsComponent } from './components/admin/settings/settings.component'
import { TasksComponent } from './components/admin/tasks/tasks.component'
import { TrashComponent } from './components/admin/trash/trash.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { AppFrameComponent } from './components/app-frame/app-frame.component'
import { DashboardComponent } from './components/dashboard/dashboard.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DocumentListComponent } from './components/document-list/document-list.component'
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { LogsComponent } from './components/admin/logs/logs.component'
import { SettingsComponent } from './components/admin/settings/settings.component'
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
import { NotFoundComponent } from './components/not-found/not-found.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DirtyFormGuard } from './guards/dirty-form.guard'
import { MailComponent } from './components/manage/mail/mail.component'
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
import { TasksComponent } from './components/admin/tasks/tasks.component'
import { PermissionsGuard } from './guards/permissions.guard'
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
import { NotFoundComponent } from './components/not-found/not-found.component'
import { DirtyDocGuard } from './guards/dirty-doc.guard'
import { DirtyFormGuard } from './guards/dirty-form.guard'
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
import { PermissionsGuard } from './guards/permissions.guard'
import {
PermissionAction,
PermissionType,
} from './services/permissions.service'
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'
import { TrashComponent } from './components/admin/trash/trash.component'
export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
@@ -165,6 +166,10 @@ export const routes: Routes = [
path: 'settings/usersgroups',
redirectTo: '/usersgroups',
},
{
path: 'settings/savedviews',
redirectTo: '/savedviews',
},
{
path: 'settings',
component: SettingsComponent,
@@ -255,6 +260,17 @@ export const routes: Routes = [
},
},
},
{
path: 'savedviews',
component: SavedViewsComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.SavedView,
},
},
},
],
},
+10 -10
View File
@@ -1,3 +1,4 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
@@ -6,25 +7,24 @@ import {
tick,
} from '@angular/core/testing'
import { Router, RouterModule } from '@angular/router'
import { TourService, TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { Subject } from 'rxjs'
import { routes } from './app-routing.module'
import { AppComponent } from './app.component'
import { ToastsComponent } from './components/common/toasts/toasts.component'
import { FileDropComponent } from './components/file-drop/file-drop.component'
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
import { PermissionsGuard } from './guards/permissions.guard'
import {
ConsumerStatusService,
FileStatus,
} from './services/consumer-status.service'
import { PermissionsService } from './services/permissions.service'
import { ToastService, Toast } from './services/toast.service'
import { SettingsService } from './services/settings.service'
import { FileDropComponent } from './components/file-drop/file-drop.component'
import { NgxFileDropModule } from 'ngx-file-drop'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { HotKeyService } from './services/hot-key.service'
import { PermissionsGuard } from './guards/permissions.guard'
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { PermissionsService } from './services/permissions.service'
import { SettingsService } from './services/settings.service'
import { Toast, ToastService } from './services/toast.service'
describe('AppComponent', () => {
let component: AppComponent
+9 -9
View File
@@ -1,18 +1,18 @@
import { SettingsService } from './services/settings.service'
import { SETTINGS_KEYS } from './data/ui-settings'
import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
import { Router } from '@angular/router'
import { Subscription, first } from 'rxjs'
import { ConsumerStatusService } from './services/consumer-status.service'
import { ToastService } from './services/toast.service'
import { TasksService } from './services/tasks.service'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import { first, Subscription } from 'rxjs'
import { SETTINGS_KEYS } from './data/ui-settings'
import { ConsumerStatusService } from './services/consumer-status.service'
import { HotKeyService } from './services/hot-key.service'
import {
PermissionAction,
PermissionsService,
PermissionType,
} from './services/permissions.service'
import { HotKeyService } from './services/hot-key.service'
import { SettingsService } from './services/settings.service'
import { TasksService } from './services/tasks.service'
import { ToastService } from './services/toast.service'
@Component({
selector: 'pngx-root',
@@ -165,7 +165,7 @@ export class AppComponent implements OnInit, OnDestroy {
[
{
anchorId: 'tour.dashboard',
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some.`,
content: $localize`The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some.`,
route: '/dashboard',
delayAfterNavigation: 500,
isOptional: false,
@@ -227,7 +227,7 @@ export class AppComponent implements OnInit, OnDestroy {
},
{
anchorId: 'tour.settings',
content: $localize`Check out the settings for various tweaks to the web app and toggle settings for saved views.`,
content: $localize`Check out the settings for various tweaks to the web app.`,
route: '/settings',
backdropConfig: {
offset: 0,
+150 -124
View File
@@ -1,134 +1,20 @@
import { BrowserModule } from '@angular/platform-browser'
import { APP_INITIALIZER, NgModule } from '@angular/core'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import {
NgbDateAdapter,
NgbDateParserFormatter,
NgbModule,
} from '@ng-bootstrap/ng-bootstrap'
import { DragDropModule } from '@angular/cdk/drag-drop'
import { DatePipe, registerLocaleData } from '@angular/common'
import {
HTTP_INTERCEPTORS,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http'
import { DocumentListComponent } from './components/document-list/document-list.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DashboardComponent } from './components/dashboard/dashboard.component'
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
import { LogsComponent } from './components/admin/logs/logs.component'
import { SettingsComponent } from './components/admin/settings/settings.component'
import { APP_INITIALIZER, NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { DatePipe, registerLocaleData } from '@angular/common'
import { NotFoundComponent } from './components/not-found/not-found.component'
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { TagComponent } from './components/common/tag/tag.component'
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
import { AppFrameComponent } from './components/app-frame/app-frame.component'
import { ToastsComponent } from './components/common/toasts/toasts.component'
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
import { DatesDropdownComponent } from './components/common/dates-dropdown/dates-dropdown.component'
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TextComponent } from './components/common/input/text/text.component'
import { SelectComponent } from './components/common/input/select/select.component'
import { CheckComponent } from './components/common/input/check/check.component'
import { UrlComponent } from './components/common/input/url/url.component'
import { PasswordComponent } from './components/common/input/password/password.component'
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
import { TagsComponent } from './components/common/input/tags/tags.component'
import { IfPermissionsDirective } from './directives/if-permissions.directive'
import { SortableDirective } from './directives/sortable.directive'
import { CookieService } from 'ngx-cookie-service'
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
import { YesNoPipe } from './pipes/yes-no.pipe'
import { FileSizePipe } from './pipes/file-size.pipe'
import { FilterPipe } from './pipes/filter.pipe'
import { DocumentTitlePipe } from './pipes/document-title.pipe'
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'
import { BrowserModule } from '@angular/platform-browser'
import {
NgbDateAdapter,
NgbDateParserFormatter,
NgbModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { NumberComponent } from './components/common/input/number/number.component'
import { SafeUrlPipe } from './pipes/safeurl.pipe'
import { SafeHtmlPipe } from './pipes/safehtml.pipe'
import { CustomDatePipe } from './pipes/custom-date.pipe'
import { DateComponent } from './components/common/input/date/date.component'
import { ISODateAdapter } from './utils/ngb-iso-date-adapter'
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
import { ColorSliderModule } from 'ngx-color/slider'
import { ColorComponent } from './components/common/input/color/color.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DocumentNotesComponent } from './components/document-notes/document-notes.component'
import { PermissionsGuard } from './guards/permissions.guard'
import { DirtyDocGuard } from './guards/dirty-doc.guard'
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { SettingsService } from './services/settings.service'
import { TasksComponent } from './components/admin/tasks/tasks.component'
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
import { PermissionsUserComponent } from './components/common/input/permissions/permissions-user/permissions-user.component'
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
import { IfOwnerDirective } from './directives/if-owner.directive'
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
import { PermissionsFilterDropdownComponent } from './components/common/permissions-filter-dropdown/permissions-filter-dropdown.component'
import { UsernamePipe } from './pipes/username.pipe'
import { 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 { 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'
import { FileDropComponent } from './components/file-drop/file-drop.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
import { PdfViewerModule } from 'ng2-pdf-viewer'
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 { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
import { DocumentHistoryComponent } from './components/document-history/document-history.component'
import { DragDropSelectComponent } from './components/common/input/drag-drop-select/drag-drop-select.component'
import { CustomFieldDisplayComponent } from './components/common/custom-field-display/custom-field-display.component'
import { GlobalSearchComponent } from './components/app-frame/global-search/global-search.component'
import { HotkeyDialogComponent } from './components/common/hotkey-dialog/hotkey-dialog.component'
import { DeletePagesConfirmDialogComponent } from './components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
import { TrashComponent } from './components/admin/trash/trash.component'
import {
airplane,
archive,
@@ -145,6 +31,7 @@ import {
boxArrowUp,
boxArrowUpRight,
boxes,
braces,
calendar,
calendarEvent,
calendarEventFill,
@@ -172,6 +59,7 @@ import {
download,
envelope,
envelopeAt,
envelopeAtFill,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
@@ -181,6 +69,7 @@ import {
fileEarmarkFill,
fileEarmarkLock,
fileEarmarkMinus,
fileEarmarkRichtext,
files,
fileText,
filter,
@@ -188,6 +77,7 @@ import {
folderFill,
funnel,
gear,
google,
grid,
gripVertical,
hash,
@@ -198,6 +88,9 @@ import {
link,
listTask,
listUl,
microsoft,
NgxBootstrapIconsModule,
nodePlus,
pencil,
people,
peopleFill,
@@ -216,8 +109,8 @@ import {
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
tagFill,
tag,
tagFill,
tags,
textIndentLeft,
textLeft,
@@ -226,9 +119,129 @@ import {
trash,
uiRadios,
upcScan,
windowStack,
x,
xCircle,
xLg,
} from 'ngx-bootstrap-icons'
import { ColorSliderModule } from 'ngx-color/slider'
import { CookieService } from 'ngx-cookie-service'
import { NgxFileDropModule } from 'ngx-file-drop'
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { ConfigComponent } from './components/admin/config/config.component'
import { LogsComponent } from './components/admin/logs/logs.component'
import { SettingsComponent } from './components/admin/settings/settings.component'
import { TasksComponent } from './components/admin/tasks/tasks.component'
import { TrashComponent } from './components/admin/trash/trash.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { AppFrameComponent } from './components/app-frame/app-frame.component'
import { GlobalSearchComponent } from './components/app-frame/global-search/global-search.component'
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
import { ConfirmDialogComponent } from './components/common/confirm-dialog/confirm-dialog.component'
import { DeletePagesConfirmDialogComponent } from './components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
import { CustomFieldDisplayComponent } from './components/common/custom-field-display/custom-field-display.component'
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
import { CustomFieldsQueryDropdownComponent } from './components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component'
import { DatesDropdownComponent } from './components/common/dates-dropdown/dates-dropdown.component'
import { CorrespondentEditDialogComponent } from './components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { GroupEditDialogComponent } from './components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { UserEditDialogComponent } from './components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { WorkflowEditDialogComponent } from './components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
import { FilterableDropdownComponent } from './components/common/filterable-dropdown/filterable-dropdown.component'
import { ToggleableDropdownButtonComponent } from './components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
import { HotkeyDialogComponent } from './components/common/hotkey-dialog/hotkey-dialog.component'
import { CheckComponent } from './components/common/input/check/check.component'
import { ColorComponent } from './components/common/input/color/color.component'
import { DateComponent } from './components/common/input/date/date.component'
import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
import { DragDropSelectComponent } from './components/common/input/drag-drop-select/drag-drop-select.component'
import { EntriesComponent } from './components/common/input/entries/entries.component'
import { FileComponent } from './components/common/input/file/file.component'
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
import { NumberComponent } from './components/common/input/number/number.component'
import { PasswordComponent } from './components/common/input/password/password.component'
import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component'
import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component'
import { PermissionsUserComponent } from './components/common/input/permissions/permissions-user/permissions-user.component'
import { SelectComponent } from './components/common/input/select/select.component'
import { SwitchComponent } from './components/common/input/switch/switch.component'
import { TagsComponent } from './components/common/input/tags/tags.component'
import { TextComponent } from './components/common/input/text/text.component'
import { TextAreaComponent } from './components/common/input/textarea/textarea.component'
import { UrlComponent } from './components/common/input/url/url.component'
import { LogoComponent } from './components/common/logo/logo.component'
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
import { PermissionsFilterDropdownComponent } from './components/common/permissions-filter-dropdown/permissions-filter-dropdown.component'
import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component'
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'
import { ShareLinksDropdownComponent } from './components/common/share-links-dropdown/share-links-dropdown.component'
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
import { TagComponent } from './components/common/tag/tag.component'
import { ToastsComponent } from './components/common/toasts/toasts.component'
import { DashboardComponent } from './components/dashboard/dashboard.component'
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component'
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component'
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component'
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component'
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'
import { DocumentHistoryComponent } from './components/document-history/document-history.component'
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
import { CustomFieldsBulkEditDialogComponent } from './components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component'
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component'
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'
import { DocumentListComponent } from './components/document-list/document-list.component'
import { FilterEditorComponent } from './components/document-list/filter-editor/filter-editor.component'
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
import { DocumentNotesComponent } from './components/document-notes/document-notes.component'
import { FileDropComponent } from './components/file-drop/file-drop.component'
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { MailComponent } from './components/manage/mail/mail.component'
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
import { NotFoundComponent } from './components/not-found/not-found.component'
import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive'
import { IfOwnerDirective } from './directives/if-owner.directive'
import { IfPermissionsDirective } from './directives/if-permissions.directive'
import { SortableDirective } from './directives/sortable.directive'
import { DirtyDocGuard } from './guards/dirty-doc.guard'
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
import { PermissionsGuard } from './guards/permissions.guard'
import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
import { CsrfInterceptor } from './interceptors/csrf.interceptor'
import { CustomDatePipe } from './pipes/custom-date.pipe'
import { DocumentTitlePipe } from './pipes/document-title.pipe'
import { FileSizePipe } from './pipes/file-size.pipe'
import { FilterPipe } from './pipes/filter.pipe'
import { IsNumberPipe } from './pipes/is-number.pipe'
import { SafeHtmlPipe } from './pipes/safehtml.pipe'
import { SafeUrlPipe } from './pipes/safeurl.pipe'
import { UsernamePipe } from './pipes/username.pipe'
import { YesNoPipe } from './pipes/yes-no.pipe'
import { SettingsService } from './services/settings.service'
import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'
import { ISODateAdapter } from './utils/ngb-iso-date-adapter'
const icons = {
airplane,
@@ -242,6 +255,7 @@ const icons = {
arrowRightShort,
arrowUpRight,
asterisk,
braces,
bodyText,
boxArrowUp,
boxArrowUpRight,
@@ -273,6 +287,7 @@ const icons = {
download,
envelope,
envelopeAt,
envelopeAtFill,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
@@ -282,6 +297,7 @@ const icons = {
fileEarmarkFill,
fileEarmarkLock,
fileEarmarkMinus,
fileEarmarkRichtext,
files,
fileText,
filter,
@@ -289,6 +305,7 @@ const icons = {
folderFill,
funnel,
gear,
google,
grid,
gripVertical,
hash,
@@ -299,6 +316,8 @@ const icons = {
link,
listTask,
listUl,
microsoft,
nodePlus,
pencil,
people,
peopleFill,
@@ -327,7 +346,9 @@ const icons = {
trash,
uiRadios,
upcScan,
windowStack,
x,
xCircle,
xLg,
}
@@ -433,6 +454,7 @@ function initializeApp(settings: SettingsService) {
DocumentCardSmallComponent,
BulkEditorComponent,
TextComponent,
TextAreaComponent,
SelectComponent,
CheckComponent,
UrlComponent,
@@ -485,6 +507,7 @@ function initializeApp(settings: SettingsService) {
CustomFieldsComponent,
CustomFieldEditDialogComponent,
CustomFieldsDropdownComponent,
CustomFieldsQueryDropdownComponent,
ProfileEditDialogComponent,
DocumentLinkComponent,
PreviewPopupComponent,
@@ -504,6 +527,9 @@ function initializeApp(settings: SettingsService) {
HotkeyDialogComponent,
DeletePagesConfirmDialogComponent,
TrashComponent,
EntriesComponent,
SavedViewsComponent,
CustomFieldsBulkEditDialogComponent,
],
bootstrap: [AppComponent],
imports: [
@@ -1,24 +1,24 @@
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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
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'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { of, throwError } from 'rxjs'
import { OutputTypeConfig } from 'src/app/data/paperless-config'
import { ConfigService } from 'src/app/services/config.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { FileComponent } from '../../common/input/file/file.component'
import { NumberComponent } from '../../common/input/number/number.component'
import { SelectComponent } from '../../common/input/select/select.component'
import { SwitchComponent } from '../../common/input/switch/switch.component'
import { TextComponent } from '../../common/input/text/text.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { ConfigComponent } from './config.component'
describe('ConfigComponent', () => {
let component: ConfigComponent
@@ -1,25 +1,24 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import {
BehaviorSubject,
Observable,
Subject,
Subscription,
first,
takeUntil,
} from 'rxjs'
import {
PaperlessConfigOptions,
ConfigCategory,
ConfigOption,
ConfigOptionType,
PaperlessConfig,
PaperlessConfigOptions,
} 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'
import { ToastService } from 'src/app/services/toast.service'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
selector: 'pngx-config',
@@ -27,7 +26,7 @@ import { SettingsService } from 'src/app/services/settings.service'
styleUrl: './config.component.scss',
})
export class ConfigComponent
extends ComponentWithPermissions
extends LoadingComponentWithPermissions
implements OnInit, OnDestroy, DirtyComponent
{
public readonly ConfigOptionType = ConfigOptionType
@@ -45,15 +44,11 @@ export class ConfigComponent
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,
@@ -67,7 +62,6 @@ export class ConfigComponent
}
ngOnInit(): void {
this.loading = true
this.configService
.getConfig()
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -4,7 +4,7 @@
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">
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div>
</pngx-page-header>
@@ -17,7 +17,7 @@
</a>
</li>
}
@if (isLoading || !logFiles.length) {
@if (loading || !logFiles.length) {
<div class="ps-2 d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
@if (!logFiles.length) {
@@ -30,15 +30,13 @@
<div [ngbNavOutlet]="nav" class="mt-2"></div>
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
@if (isLoading && logFiles.length) {
@if (loading && logFiles.length) {
<div>
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
}
@for (log of logs; track log) {
<p
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
>{{log}}</p>
@for (log of logs; track $index) {
<p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
}
</div>
@@ -1,18 +1,14 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule, By } from '@angular/platform-browser'
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LogsComponent } from './logs.component'
import { of, throwError } from 'rxjs'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
import { BrowserModule, By } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const paperless_logs = [
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
@@ -42,6 +38,8 @@ describe('LogsComponent', () => {
BrowserModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
FormsModule,
ReactiveFormsModule,
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
@@ -90,8 +88,7 @@ describe('LogsComponent', () => {
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
component.toggleAutoRefresh()
expect(component.autoRefreshInterval).toBeNull()
component.autoRefreshEnabled = false
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
})
@@ -1,24 +1,30 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
ViewChild,
OnDestroy,
ChangeDetectorRef,
} from '@angular/core'
import { Subject, takeUntil } from 'rxjs'
import { filter, takeUntil, timer } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
selector: 'pngx-logs',
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.scss'],
})
export class LogsComponent implements OnInit, OnDestroy {
export class LogsComponent
extends LoadingComponentWithPermissions
implements OnInit, OnDestroy
{
constructor(
private logService: LogService,
private changedetectorRef: ChangeDetectorRef
) {}
) {
super()
}
public logs: string[] = []
@@ -26,50 +32,50 @@ export class LogsComponent implements OnInit, OnDestroy {
public activeLog: string
private unsubscribeNotifier: Subject<any> = new Subject()
public isLoading: boolean = false
public autoRefreshInterval: any
public autoRefreshEnabled: boolean = true
@ViewChild('logContainer') logContainer: ElementRef
ngOnInit(): void {
this.isLoading = true
this.logService
.list()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((result) => {
this.logFiles = result
this.isLoading = false
this.loading = false
if (this.logFiles.length > 0) {
this.activeLog = this.logFiles[0]
this.reloadLogs()
}
this.toggleAutoRefresh()
timer(5000, 5000)
.pipe(
filter(() => this.autoRefreshEnabled),
takeUntil(this.unsubscribeNotifier)
)
.subscribe(() => {
this.reloadLogs()
})
})
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
clearInterval(this.autoRefreshInterval)
super.ngOnDestroy()
}
reloadLogs() {
this.isLoading = true
this.loading = true
this.logService
.get(this.activeLog)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
this.logs = result
this.isLoading = false
this.loading = false
this.scrollToBottom()
},
error: () => {
this.logs = []
this.isLoading = false
this.loading = false
},
})
}
@@ -96,15 +102,4 @@ export class LogsComponent implements OnInit, OnDestroy {
behavior: 'auto',
})
}
toggleAutoRefresh(): void {
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval)
this.autoRefreshInterval = null
} else {
this.autoRefreshInterval = setInterval(() => {
this.reloadLogs()
}, 5000)
}
}
}
@@ -1,7 +1,7 @@
<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>."
info="Options to customize appearance, notifications and more. Settings apply to the <strong>current user only</strong>."
i18n-info
>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
@@ -39,193 +39,204 @@
<li [ngbNavItem]="SettingsNavIDs.General">
<a ngbNavLink i18n>General</a>
<ng-template ngbNavContent>
<h4 i18n>Appearance</h4>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Display language</span>
</div>
<div class="col">
<select class="form-select" formControlName="displayLanguage">
@for (lang of displayLanguageOptions; track lang) {
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code && currentLocale !== 'en-US') {
<span> - {{lang.englishName}}</span>
}</option>
}
</select>
@if (displayLanguageIsDirty) {
<small class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
}
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Date display</span>
</div>
<div class="col">
<select class="form-select" formControlName="dateLocale">
@for (lang of dateLocaleOptions; track lang) {
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code) {
<span> - {{today | customDate:'shortDate':null:lang.code}}</span>
}</option>
}
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Date format</span>
</div>
<div class="col">
<div class="form-check">
<input type="radio" id="dateFormatShort" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="shortDate">
<label class="form-check-label" for="dateFormatShort" i18n>Short: {{today | customDate:'shortDate':null:computedDateLocale}}</label>
</div>
<div class="form-check">
<input type="radio" id="dateFormatMedium" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="mediumDate">
<label class="form-check-label" for="dateFormatMedium" i18n>Medium: {{today | customDate:'mediumDate':null:computedDateLocale}}</label>
</div>
<div class="form-check">
<input type="radio" id="dateFormatLong" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="longDate">
<label class="form-check-label" for="dateFormatLong" i18n>Long: {{today | customDate:'longDate':null:computedDateLocale}}</label>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Items per page</span>
</div>
<div class="col">
<select class="form-select" formControlName="documentListItemPerPage">
<option [ngValue]="10">10</option>
<option [ngValue]="25">25</option>
<option [ngValue]="50">50</option>
<option [ngValue]="100">100</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Document editor</span>
</div>
<div class="col">
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Sidebar</span>
</div>
<div class="col">
<pngx-input-check i18n-title title="Use 'slim' sidebar (icons only)" formControlName="slimSidebarEnabled"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Dark mode</span>
</div>
<div class="col">
<pngx-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem"></pngx-input-check>
<pngx-input-check [hidden]="settingsForm.value.darkModeUseSystem" i18n-title title="Enable dark mode" formControlName="darkModeEnabled"></pngx-input-check>
<pngx-input-check i18n-title title="Invert thumbnails in dark mode" formControlName="darkModeInvertThumbs"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Theme Color</span>
</div>
<div class="col col-md-3">
<pngx-input-color i18n-title formControlName="themeColor" [error]="error?.color"></pngx-input-color>
</div>
<div class="col-2">
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
<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>
<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>Document editing</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
</div>
</div>
<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"></pngx-input-check>
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
</div>
</div>
<h4 class="mt-4" i18n>Global search</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Do not include advanced search results" formControlName="searchDbOnly"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<div class="row">
<div class="col-md-2 col-form-label pt-0">
<span i18n>Full search links to</span>
<div class="row">
<div class="col-xl-6 pe-xl-5">
<h4 i18n>Appearance</h4>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Display language</span>
</div>
<div class="col">
<select class="form-select" formControlName="searchLink">
<option [ngValue]="GlobalSearchType.TITLE_CONTENT" i18n>Title and content search</option>
<option [ngValue]="GlobalSearchType.ADVANCED" i18n>Advanced search</option>
<select class="form-select" formControlName="displayLanguage">
@for (lang of displayLanguageOptions; track lang) {
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code && currentLocale !== 'en-US') {
<span> - {{lang.englishName}}</span>
}</option>
}
</select>
@if (displayLanguageIsDirty) {
<small class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
}
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Date display</span>
</div>
<div class="col">
<select class="form-select" formControlName="dateLocale">
@for (lang of dateLocaleOptions; track lang) {
<option [ngValue]="lang.code">{{lang.name}}@if (lang.code) {
<span> - {{today | customDate:'shortDate':null:lang.code}}</span>
}</option>
}
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Date format</span>
</div>
<div class="col">
<div class="form-check">
<input type="radio" id="dateFormatShort" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="shortDate">
<label class="form-check-label" for="dateFormatShort" i18n>Short: {{today | customDate:'shortDate':null:computedDateLocale}}</label>
</div>
<div class="form-check">
<input type="radio" id="dateFormatMedium" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="mediumDate">
<label class="form-check-label" for="dateFormatMedium" i18n>Medium: {{today | customDate:'mediumDate':null:computedDateLocale}}</label>
</div>
<div class="form-check">
<input type="radio" id="dateFormatLong" name="dateFormat" class="form-check-input" formControlName="dateFormat" value="longDate">
<label class="form-check-label" for="dateFormatLong" i18n>Long: {{today | customDate:'longDate':null:computedDateLocale}}</label>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Items per page</span>
</div>
<div class="col">
<select class="form-select" formControlName="documentListItemPerPage">
<option [ngValue]="10">10</option>
<option [ngValue]="25">25</option>
<option [ngValue]="50">50</option>
<option [ngValue]="100">100</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Sidebar</span>
</div>
<div class="col">
<pngx-input-check i18n-title title="Use 'slim' sidebar (icons only)" formControlName="slimSidebarEnabled"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Dark mode</span>
</div>
<div class="col">
<pngx-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem"></pngx-input-check>
<pngx-input-check [hidden]="settingsForm.value.darkModeUseSystem" i18n-title title="Enable dark mode" formControlName="darkModeEnabled"></pngx-input-check>
<pngx-input-check i18n-title title="Invert thumbnails in dark mode" formControlName="darkModeInvertThumbs"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Theme Color</span>
</div>
<div class="col col-md-3">
<pngx-input-color i18n-title formControlName="themeColor" [error]="error?.color"></pngx-input-color>
</div>
<div class="col-2">
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
<i-bs width="1em" height="1em" name="x"></i-bs><ng-container i18n>Reset</ng-container>
</button>
</div>
</div>
<h4 class="mt-4" i18n>Document editing</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show document thumbnail during loading" formControlName="documentEditingOverlayThumbnail"></pngx-input-check>
</div>
</div>
</div>
</div>
<div class="col-xl-6 ps-xl-5">
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
<div class="row mb-3">
<div class="offset-md-3 col d-flex flex-row align-items-start">
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
<button class="btn btn-sm btn-link text-muted me-auto p-0 ms-2" title="What's this?" i18n-title type="button" triggers="mouseenter:mouseleave" [ngbPopover]="updatesPopover" [autoClose]="true">
<i-bs name="question-circle"></i-bs>
</button>
<ng-template #updatesPopover>
<p i18n>
Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually.
</p>
<p>
<em i18n>No tracking data is collected by the app in any way.</em>
</p>
</ng-template>
</div>
</div>
<h4 class="mt-4" i18n>Notes</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"></pngx-input-check>
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
</div>
</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>
<h4 class="mt-4" i18n>Global search</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Do not include advanced search results" formControlName="searchDbOnly"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<div class="row">
<div class="col-md-2 col-form-label pt-0">
<span i18n>Full search links to</span>
</div>
<div class="col">
<select class="form-select" formControlName="searchLink">
<option [ngValue]="GlobalSearchType.TITLE_CONTENT" i18n>Title and content search</option>
<option [ngValue]="GlobalSearchType.ADVANCED" i18n>Advanced search</option>
</select>
</div>
</div>
</div>
</div>
<h4 class="mt-4" i18n>Saved Views</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 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>
</div>
</div>
@@ -331,87 +342,6 @@
</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>
<ul class="list-group" formGroupName="savedViews">
@for (view of savedViews; track view) {
<li class="list-group-item py-3">
<div [formGroupName]="view.id">
<div class="row">
<div class="col">
<pngx-input-text title="Name" formControlName="name"></pngx-input-text>
</div>
<div class="col">
<div class="form-check form-switch mt-3">
<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="col-auto">
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
<pngx-confirm-button
label="Delete"
i18n-label
(confirm)="deleteSavedView(view)"
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }"
buttonClasses="btn-sm btn-outline-danger form-control"
iconName="trash">
</pngx-confirm-button>
</div>
</div>
<div class="row">
<div class="col">
<pngx-input-number i18n-title title="Documents page size" [showAdd]="false" formControlName="page_size"></pngx-input-number>
</div>
<div class="col">
<label class="form-label" for="display_mode_{{view.id}}" i18n>Display as</label>
<select class="form-select" formControlName="display_mode">
<option [ngValue]="DisplayMode.TABLE" i18n>Table</option>
<option [ngValue]="DisplayMode.SMALL_CARDS" i18n>Small Cards</option>
<option [ngValue]="DisplayMode.LARGE_CARDS" i18n>Large Cards</option>
</select>
</div>
@if (displayFields) {
<pngx-input-drag-drop-select i18n-title title="Show" i18n-emptyText emptyText="Default" [items]="displayFields" formControlName="display_fields"></pngx-input-drag-drop-select>
}
</div>
</div>
</li>
}
@if (savedViews && savedViews.length === 0) {
<li class="list-group-item">
<div i18n>No saved views defined.</div>
</li>
}
@if (!savedViews) {
<li class="list-group-item">
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
</li>
}
</ul>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
@@ -1,35 +1,45 @@
import { ViewportScroller, DatePipe } from '@angular/common'
import { DragDropModule } from '@angular/cdk/drag-drop'
import { DatePipe, ViewportScroller } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { Router, ActivatedRoute, convertToParamMap } from '@angular/router'
import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import {
NgbModule,
NgbAlertModule,
NgbNavLink,
NgbModal,
NgbModalModule,
NgbModule,
NgbNavLink,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import { SavedView } from 'src/app/data/saved-view'
import {
InstallType,
SystemStatus,
SystemStatusItemStatus,
} from 'src/app/data/system-status'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService, Toast } from 'src/app/services/toast.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CheckComponent } from '../../common/input/check/check.component'
import { ColorComponent } from '../../common/input/color/color.component'
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
import { NumberComponent } from '../../common/input/number/number.component'
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
@@ -37,25 +47,9 @@ import { SelectComponent } from '../../common/input/select/select.component'
import { TagsComponent } from '../../common/input/tags/tags.component'
import { TextComponent } from '../../common/input/text/text.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { SettingsComponent } from './settings.component'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service'
import {
SystemStatus,
InstallType,
SystemStatusItemStatus,
} from 'src/app/data/system-status'
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
import { DragDropModule } from '@angular/cdk/drag-drop'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { SettingsComponent } from './settings.component'
const savedViews = [
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
{ id: 2, name: 'view2', show_in_sidebar: false, show_on_dashboard: false },
]
const users = [
{ id: 1, username: 'user1', is_superuser: false },
{ id: 2, username: 'user2', is_superuser: false },
@@ -70,7 +64,6 @@ describe('SettingsComponent', () => {
let fixture: ComponentFixture<SettingsComponent>
let router: Router
let settingsService: SettingsService
let savedViewService: SavedViewService
let activatedRoute: ActivatedRoute
let viewportScroller: ViewportScroller
let toastService: ToastService
@@ -139,7 +132,6 @@ describe('SettingsComponent', () => {
.spyOn(permissionsService, 'currentUserOwnsObject')
.mockReturnValue(true)
groupService = TestBed.inject(GroupService)
savedViewService = TestBed.inject(SavedViewService)
})
function completeSetup(excludeService = null) {
@@ -161,15 +153,6 @@ describe('SettingsComponent', () => {
})
)
}
if (excludeService !== savedViewService) {
jest.spyOn(savedViewService, 'listAll').mockReturnValue(
of({
all: savedViews.map((v) => v.id),
count: savedViews.length,
results: (savedViews as SavedView[]).concat([]),
})
)
}
fixture = TestBed.createComponent(SettingsComponent)
component = fixture.componentInstance
@@ -184,8 +167,6 @@ describe('SettingsComponent', () => {
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions'])
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'notifications'])
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'savedviews'])
const initSpy = jest.spyOn(component, 'initialize')
component.isDirty = true // mock dirty
@@ -213,90 +194,8 @@ describe('SettingsComponent', () => {
expect(scrollSpy).toHaveBeenCalledWith('#notifications')
})
it('should enable organizing of sidebar saved views even on direct navigation', () => {
completeSetup()
jest
.spyOn(activatedRoute, 'paramMap', 'get')
.mockReturnValue(of(convertToParamMap({ section: 'savedviews' })))
activatedRoute.snapshot.fragment = '#savedviews'
component.ngOnInit()
expect(component.activeNavID).toEqual(4) // Saved Views
component.ngAfterViewInit()
expect(settingsService.organizingSidebarSavedViews).toBeTruthy()
})
it('should support save saved views, show error', () => {
completeSetup()
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
const toggle = fixture.debugElement.query(
By.css('.form-check.form-switch input')
)
toggle.nativeElement.checked = true
toggle.nativeElement.dispatchEvent(new Event('change'))
// saved views error first
savedViewPatchSpy.mockReturnValueOnce(
throwError(() => new Error('unable to save saved views'))
)
component.saveSettings()
expect(toastErrorSpy).toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
toastSpy.mockClear()
toastErrorSpy.mockClear()
savedViewPatchSpy.mockClear()
// succeed saved views
savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[]))
component.saveSettings()
expect(toastErrorSpy).not.toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
})
it('should update only patch saved views that have changed', () => {
completeSetup()
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
tabButtons[3].nativeElement.dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
component.saveSettings()
expect(patchSpy).not.toHaveBeenCalled()
const view = savedViews[0]
const toggle = fixture.debugElement.query(
By.css('.form-check.form-switch input')
)
toggle.nativeElement.checked = true
toggle.nativeElement.dispatchEvent(new Event('change'))
// register change
component.savedViewGroup.get(view.id.toString()).value[
'show_on_dashboard'
] = !view.show_on_dashboard
fixture.detectChanges()
component.saveSettings()
expect(patchSpy).toHaveBeenCalledWith([
{
id: view.id,
name: view.name,
show_in_sidebar: view.show_in_sidebar,
show_on_dashboard: !view.show_on_dashboard,
},
])
})
it('should support save local settings updating appearance settings and calling API, show error', () => {
completeSetup()
jest.spyOn(savedViewService, 'patchMany').mockReturnValue(of([]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const storeSpy = jest.spyOn(settingsService, 'storeSettings')
@@ -315,7 +214,7 @@ describe('SettingsComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(27)
expect(setSpy).toHaveBeenCalledTimes(28)
// succeed
storeSpy.mockReturnValueOnce(of(true))
@@ -326,7 +225,6 @@ describe('SettingsComponent', () => {
it('should offer reload if settings changes require', () => {
completeSetup()
jest.spyOn(savedViewService, 'patchMany').mockReturnValue(of([]))
let toast: Toast
toastService.getToasts().subscribe((t) => (toast = t[0]))
component.initialize(true) // reset
@@ -361,18 +259,6 @@ describe('SettingsComponent', () => {
component.clearThemeColor()
})
it('should support delete saved view', () => {
completeSetup()
const toastSpy = jest.spyOn(toastService, 'showInfo')
const deleteSpy = jest.spyOn(savedViewService, 'delete')
deleteSpy.mockReturnValue(of(true))
component.deleteSavedView(savedViews[0] as SavedView)
expect(deleteSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
`Saved view "${savedViews[0].name}" deleted.`
)
})
it('should show errors on load if load users failure', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
jest
@@ -1,13 +1,13 @@
import { ViewportScroller } from '@angular/common'
import {
Component,
OnInit,
AfterViewInit,
OnDestroy,
Component,
Inject,
LOCALE_ID,
OnDestroy,
OnInit,
} from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms'
import { FormControl, FormGroup } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import {
NgbModal,
@@ -18,39 +18,36 @@ import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import {
BehaviorSubject,
Subscription,
Observable,
Subject,
Subscription,
first,
takeUntil,
tap,
} from 'rxjs'
import { Group } from 'src/app/data/group'
import { SavedView } from 'src/app/data/saved-view'
import {
SystemStatus,
SystemStatusItemStatus,
} from 'src/app/data/system-status'
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import {
PermissionsService,
PermissionAction,
PermissionType,
PermissionsService,
} from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { UserService } from 'src/app/services/rest/user.service'
import {
SettingsService,
LanguageOption,
SettingsService,
} from 'src/app/services/settings.service'
import { ToastService, Toast } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service'
import {
SystemStatusItemStatus,
SystemStatus,
} from 'src/app/data/system-status'
import { DisplayMode } from 'src/app/data/document'
import { Toast, ToastService } from 'src/app/services/toast.service'
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
enum SettingsNavIDs {
General = 1,
@@ -75,9 +72,6 @@ export class SettingsComponent
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
{
activeNavID: number
DisplayMode = DisplayMode
savedViewGroup = new FormGroup({})
settingsForm = new FormGroup({
bulkEditConfirmationDialogs: new FormControl(null),
@@ -88,7 +82,6 @@ export class SettingsComponent
darkModeEnabled: new FormControl(null),
darkModeInvertThumbs: new FormControl(null),
themeColor: new FormControl(null),
useNativePdfViewer: new FormControl(null),
displayLanguage: new FormControl(null),
dateLocale: new FormControl(null),
dateFormat: new FormControl(null),
@@ -99,7 +92,9 @@ export class SettingsComponent
defaultPermsViewGroups: new FormControl(null),
defaultPermsEditUsers: new FormControl(null),
defaultPermsEditGroups: new FormControl(null),
useNativePdfViewer: new FormControl(null),
documentEditingRemoveInboxTags: new FormControl(null),
documentEditingOverlayThumbnail: new FormControl(null),
searchDbOnly: new FormControl(null),
searchLink: new FormControl(null),
@@ -109,14 +104,9 @@ export class SettingsComponent
notificationsConsumerSuppressOnDashboard: new FormControl(null),
savedViewsWarnOnUnsavedChange: new FormControl(null),
savedViews: this.savedViewGroup,
})
savedViews: SavedView[]
SettingsNavIDs = SettingsNavIDs
get displayFields() {
return this.settings.allDisplayFields
}
store: BehaviorSubject<any>
storeSub: Subscription
@@ -151,7 +141,6 @@ export class SettingsComponent
}
constructor(
public savedViewService: SavedViewService,
private documentListViewService: DocumentListViewService,
private toastService: ToastService,
private settings: SettingsService,
@@ -213,18 +202,6 @@ export class SettingsComponent
})
}
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.SavedView
)
) {
this.savedViewService.listAll().subscribe((r) => {
this.savedViews = r.results
this.initialize(false)
})
}
this.activatedRoute.paramMap.subscribe((paramMap) => {
const section = paramMap.get('section')
if (section) {
@@ -234,9 +211,6 @@ export class SettingsComponent
if (navIDKey) {
this.activeNavID = SettingsNavIDs[navIDKey]
}
if (this.activeNavID === SettingsNavIDs.SavedViews) {
this.settings.organizingSidebarSavedViews = true
}
}
})
}
@@ -308,9 +282,11 @@ export class SettingsComponent
documentEditingRemoveInboxTags: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
),
documentEditingOverlayThumbnail: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL
),
searchDbOnly: this.settings.get(SETTINGS_KEYS.SEARCH_DB_ONLY),
searchLink: this.settings.get(SETTINGS_KEYS.SEARCH_FULL_TYPE),
savedViews: {},
}
}
@@ -323,15 +299,11 @@ export class SettingsComponent
this.router
.navigate(['settings', foundNavIDkey.toLowerCase()])
.then((navigated) => {
this.settings.organizingSidebarSavedViews = false
if (!navigated && this.isDirty) {
this.activeNavID = navChangeEvent.activeId
} else if (navigated && this.isDirty) {
this.initialize()
}
if (this.activeNavID === SettingsNavIDs.SavedViews) {
this.settings.organizingSidebarSavedViews = true
}
})
}
@@ -342,34 +314,6 @@ export class SettingsComponent
let storeData = this.getCurrentSettings()
if (this.savedViews) {
this.emptyGroup(this.savedViewGroup)
for (let view of this.savedViews) {
storeData.savedViews[view.id.toString()] = {
id: view.id,
name: view.name,
show_on_dashboard: view.show_on_dashboard,
show_in_sidebar: view.show_in_sidebar,
page_size: view.page_size,
display_mode: view.display_mode,
display_fields: view.display_fields,
}
this.savedViewGroup.addControl(
view.id.toString(),
new FormGroup({
id: new FormControl(null),
name: new FormControl(null),
show_on_dashboard: new FormControl(null),
show_in_sidebar: new FormControl(null),
page_size: new FormControl(null),
display_mode: new FormControl(null),
display_fields: new FormControl([]),
})
)
}
}
this.store = new BehaviorSubject(storeData)
this.storeSub = this.store.asObservable().subscribe((state) => {
@@ -409,32 +353,12 @@ export class SettingsComponent
}
}
private emptyGroup(group: FormGroup) {
Object.keys(group.controls).forEach((key) => group.removeControl(key))
}
ngOnDestroy() {
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didn't save
this.storeSub && this.storeSub.unsubscribe()
this.settings.organizingSidebarSavedViews = false
}
deleteSavedView(savedView: SavedView) {
this.savedViewService.delete(savedView).subscribe(() => {
this.savedViewGroup.removeControl(savedView.id.toString())
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
this.toastService.showInfo(
$localize`Saved view "${savedView.name}" deleted.`
)
this.savedViewService.clearCache()
this.savedViewService.listAll().subscribe((r) => {
this.savedViews = r.results
this.initialize(true)
})
})
}
private saveLocalSettings() {
public saveSettings() {
this.savePending = true
const reloadRequired =
this.settingsForm.value.displayLanguage !=
@@ -539,6 +463,10 @@ export class SettingsComponent
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS,
this.settingsForm.value.documentEditingRemoveInboxTags
)
this.settings.set(
SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL,
this.settingsForm.value.documentEditingOverlayThumbnail
)
this.settings.set(
SETTINGS_KEYS.SEARCH_DB_ONLY,
this.settingsForm.value.searchDbOnly
@@ -592,31 +520,6 @@ export class SettingsComponent
return new Date()
}
saveSettings() {
// only patch views that have actually changed
const changed: SavedView[] = []
Object.values(this.savedViewGroup.controls)
.filter((g: FormGroup) => !g.pristine)
.forEach((group: FormGroup) => {
changed.push(group.value)
})
if (changed.length > 0) {
this.savedViewService.patchMany(changed).subscribe({
next: () => {
this.saveLocalSettings()
},
error: (error) => {
this.toastService.showError(
$localize`Error while storing settings on server.`,
error
)
},
})
} else {
this.saveLocalSettings()
}
}
reset() {
this.settingsForm.patchValue(this.store.getValue())
}
@@ -4,15 +4,40 @@
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">
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<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">
<button class="btn btn-sm btn-outline-primary me-2" (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">
<div class="form-inline d-flex align-items-center">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
<span class="input-group-text text-muted" i18n>Filter by</span>
@if (filterTargets.length > 1) {
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
@for (t of filterTargets; track t.id) {
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
}
</div>
</div>
} @else {
<span class="input-group-text">{{filterTargetName}}</span>
}
@if (filterText?.length) {
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()">
<i-bs width="1em" height="1em" name="x"></i-bs>
</button>
}
<input #filterInput class="form-control form-control-sm" type="text"
(keyup)="filterInputKeyup($event)"
[(ngModel)]="filterText">
</div>
</div>
<div class="form-check form-switch mb-0 ms-2">
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div>
</div>
@@ -43,7 +68,7 @@
</tr>
</thead>
<tbody>
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task.id) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">
@@ -118,13 +143,13 @@
</div>
</ng-template>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange($event)">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange()" (navChange)="beforeTabChange()">
<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-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="completed">
@@ -132,7 +157,7 @@
<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-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="started">
@@ -140,7 +165,7 @@
<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-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
@@ -148,7 +173,7 @@
<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-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
</ul>
@@ -26,3 +26,14 @@ pre {
max-width: 150px;
}
}
.input-group .dropdown .btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.z-10 {
z-index: 10;
}
@@ -1,36 +1,36 @@
import { DatePipe } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import {
NgbModal,
NgbModalRef,
NgbModule,
NgbNavItem,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { routes } from 'src/app/app-routing.module'
import {
PaperlessTask,
PaperlessTaskType,
PaperlessTaskStatus,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { TasksService } from 'src/app/services/tasks.service'
import { environment } from 'src/environments/environment'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { TasksComponent } from './tasks.component'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { FormsModule } from '@angular/forms'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { TasksComponent, TaskTab } from './tasks.component'
const tasks: PaperlessTask[] = [
{
@@ -167,7 +167,7 @@ describe('TasksComponent', () => {
let currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Failed
).length
component.activeTab = 'failed'
component.activeTab = TaskTab.Failed
fixture.detectChanges()
expect(tabButtons[0].nativeElement.textContent).toEqual(
`Failed${currentTasksLength}`
@@ -179,7 +179,7 @@ describe('TasksComponent', () => {
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Complete
).length
component.activeTab = 'completed'
component.activeTab = TaskTab.Completed
fixture.detectChanges()
expect(tabButtons[1].nativeElement.textContent).toEqual(
`Complete${currentTasksLength}`
@@ -188,7 +188,7 @@ describe('TasksComponent', () => {
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Started
).length
component.activeTab = 'started'
component.activeTab = TaskTab.Started
fixture.detectChanges()
expect(tabButtons[2].nativeElement.textContent).toEqual(
`Started${currentTasksLength}`
@@ -197,7 +197,7 @@ describe('TasksComponent', () => {
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Pending
).length
component.activeTab = 'queued'
component.activeTab = TaskTab.Queued
fixture.detectChanges()
expect(tabButtons[3].nativeElement.textContent).toEqual(
`Queued${currentTasksLength}`
@@ -206,7 +206,7 @@ describe('TasksComponent', () => {
it('should to go page 1 between tab switch', () => {
component.page = 10
component.duringTabChange(2)
component.duringTabChange()
expect(component.page).toEqual(1)
})
@@ -283,10 +283,63 @@ describe('TasksComponent', () => {
expect(reloadSpy).toHaveBeenCalledTimes(1)
jest.advanceTimersByTime(5000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
component.toggleAutoRefresh()
expect(component.autoRefreshInterval).toBeNull()
component.autoRefreshEnabled = false
jest.advanceTimersByTime(6000)
expect(reloadSpy).toHaveBeenCalledTimes(2)
})
it('should filter tasks by file name', () => {
const input = fixture.debugElement.query(
By.css('pngx-page-header input[type=text]')
)
input.nativeElement.value = '191092'
input.nativeElement.dispatchEvent(new Event('input'))
jest.advanceTimersByTime(150) // debounce time
fixture.detectChanges()
expect(component.filterText).toEqual('191092')
expect(
fixture.debugElement.queryAll(By.css('table tbody tr')).length
).toEqual(2) // 1 task x 2 lines
})
it('should filter tasks by result', () => {
component.activeTab = TaskTab.Failed
fixture.detectChanges()
component.filterTargetID = 1
const input = fixture.debugElement.query(
By.css('pngx-page-header input[type=text]')
)
input.nativeElement.value = 'duplicate'
input.nativeElement.dispatchEvent(new Event('input'))
jest.advanceTimersByTime(150) // debounce time
fixture.detectChanges()
expect(component.filterText).toEqual('duplicate')
expect(
fixture.debugElement.queryAll(By.css('table tbody tr')).length
).toEqual(4) // 2 tasks x 2 lines
})
it('should support keyboard events for filtering', () => {
const input = fixture.debugElement.query(
By.css('pngx-page-header input[type=text]')
)
input.nativeElement.value = '191092'
input.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Enter' })
)
expect(component.filterText).toEqual('191092') // no debounce needed
input.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Escape' })
)
expect(component.filterText).toEqual('')
})
it('should reset filter and target on tab switch', () => {
component.filterText = '191092'
component.filterTargetID = 1
component.activeTab = TaskTab.Completed
component.beforeTabChange()
expect(component.filterText).toEqual('')
expect(component.filterTargetID).toEqual(0)
})
})
@@ -1,11 +1,36 @@
import { Component, OnInit, OnDestroy } from '@angular/core'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import {
debounceTime,
distinctUntilChanged,
filter,
first,
Subject,
takeUntil,
timer,
} from 'rxjs'
import { PaperlessTask } from 'src/app/data/paperless-task'
import { TasksService } from 'src/app/services/tasks.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
export enum TaskTab {
Queued = 'queued',
Started = 'started',
Completed = 'completed',
Failed = 'failed',
}
enum TaskFilterTargetID {
Name,
Result,
}
const FILTER_TARGETS = [
{ id: TaskFilterTargetID.Name, name: $localize`Name` },
{ id: TaskFilterTargetID.Result, name: $localize`Result` },
]
@Component({
selector: 'pngx-tasks',
@@ -13,10 +38,10 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
styleUrls: ['./tasks.component.scss'],
})
export class TasksComponent
extends ComponentWithPermissions
extends LoadingComponentWithPermissions
implements OnInit, OnDestroy
{
public activeTab: string
public activeTab: TaskTab
public selectedTasks: Set<number> = new Set()
public togggleAll: boolean = false
public expandedTask: number
@@ -24,7 +49,27 @@ export class TasksComponent
public pageSize: number = 25
public page: number = 1
public autoRefreshInterval: any
public autoRefreshEnabled: boolean = true
private _filterText: string = ''
get filterText() {
return this._filterText
}
set filterText(value: string) {
this.filterDebounce.next(value)
}
public filterTargetID: TaskFilterTargetID = TaskFilterTargetID.Name
public get filterTargetName(): string {
return this.filterTargets.find((t) => t.id == this.filterTargetID).name
}
private filterDebounce: Subject<string> = new Subject<string>()
public get filterTargets(): Array<{ id: number; name: string }> {
return [TaskTab.Failed, TaskTab.Completed].includes(this.activeTab)
? FILTER_TARGETS
: FILTER_TARGETS.slice(0, 1)
}
get dismissButtonText(): string {
return this.selectedTasks.size > 0
@@ -42,12 +87,28 @@ export class TasksComponent
ngOnInit() {
this.tasksService.reload()
this.toggleAutoRefresh()
timer(5000, 5000)
.pipe(
filter(() => this.autoRefreshEnabled),
takeUntil(this.unsubscribeNotifier)
)
.subscribe(() => {
this.tasksService.reload()
})
this.filterDebounce
.pipe(
takeUntil(this.unsubscribeNotifier),
debounceTime(100),
distinctUntilChanged(),
filter((query) => !query.length || query.length > 2)
)
.subscribe((query) => (this._filterText = query))
}
ngOnDestroy() {
super.ngOnDestroy()
this.tasksService.cancelPending()
clearInterval(this.autoRefreshInterval)
}
dismissTask(task: PaperlessTask) {
@@ -96,19 +157,30 @@ export class TasksComponent
get currentTasks(): PaperlessTask[] {
let tasks: PaperlessTask[] = []
switch (this.activeTab) {
case 'queued':
case TaskTab.Queued:
tasks = this.tasksService.queuedFileTasks
break
case 'started':
case TaskTab.Started:
tasks = this.tasksService.startedFileTasks
break
case 'completed':
case TaskTab.Completed:
tasks = this.tasksService.completedFileTasks
break
case 'failed':
case TaskTab.Failed:
tasks = this.tasksService.failedFileTasks
break
}
if (this._filterText.length) {
tasks = tasks.filter((t) => {
if (this.filterTargetID == TaskFilterTargetID.Name) {
return t.task_file_name
.toLowerCase()
.includes(this._filterText.toLowerCase())
} else if (this.filterTargetID == TaskFilterTargetID.Result) {
return t.result.toLowerCase().includes(this._filterText.toLowerCase())
}
})
}
return tasks
}
@@ -125,31 +197,37 @@ export class TasksComponent
this.selectedTasks.clear()
}
duringTabChange(navID: number) {
duringTabChange() {
this.page = 1
}
beforeTabChange() {
this.resetFilter()
this.filterTargetID = TaskFilterTargetID.Name
}
get activeTabLocalized(): string {
switch (this.activeTab) {
case 'queued':
case TaskTab.Queued:
return $localize`queued`
case 'started':
case TaskTab.Started:
return $localize`started`
case 'completed':
case TaskTab.Completed:
return $localize`completed`
case 'failed':
case TaskTab.Failed:
return $localize`failed`
}
}
toggleAutoRefresh(): void {
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval)
this.autoRefreshInterval = null
} else {
this.autoRefreshInterval = setInterval(() => {
this.tasksService.reload()
}, 5000)
public resetFilter() {
this._filterText = ''
}
filterInputKeyup(event: KeyboardEvent) {
if (event.key == 'Enter') {
this._filterText = (event.target as HTMLInputElement).value
} else if (event.key === 'Escape') {
this.resetFilter()
}
}
}
@@ -38,7 +38,7 @@
</tr>
</thead>
<tbody>
@if (isLoading) {
@if (loading) {
<tr>
<td colspan="5">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
@@ -47,15 +47,20 @@
</tr>
}
@for (document of documentsInTrash; track document.id) {
<tr (click)="toggleSelected(document); $event.stopPropagation();">
<tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()" class="data-row fade" [class.show]="show">
<td>
<div class="form-check m-0 ms-2 me-n2">
<input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
<label class="form-check-label" for="{{document.id}}"></label>
</div>
</td>
<td scope="row">{{ document.title }}</td>
<td scope="row" i18n>{{ getDaysRemaining(document) }} days</td>
<td scope="row">
{{ document.title }}
<pngx-preview-popup [document]="document" linkClasses="btn btn-sm btn-link" #popupPreview>
<i-bs name="eye"></i-bs>
</pngx-preview-popup>
</td>
<td scope="row" class="d-none d-sm-table-cell" i18n>{{ getDaysRemaining(document) }} days</td>
<td scope="row">
<div class="btn-group d-block d-sm-none">
<div ngbDropdown container="body" class="d-inline-block">
@@ -83,7 +88,7 @@
</table>
</div>
@if (!isLoading) {
@if (!loading) {
<div class="d-flex mb-2">
<div>
<ng-container i18n>{totalDocuments, plural, =1 {One document in trash} other {{{totalDocuments || 0}} total documents in trash}}</ng-container>
@@ -1,21 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { TrashComponent } from './trash.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { Router } from '@angular/router'
import {
NgbModal,
NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TrashService } from 'src/app/services/trash.service'
import { of, throwError } from 'rxjs'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { By } from '@angular/platform-browser'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ToastService } from 'src/app/services/toast.service'
import { TrashService } from 'src/app/services/trash.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { TrashComponent } from './trash.component'
const documentsInTrash = [
{
@@ -38,6 +39,7 @@ describe('TrashComponent', () => {
let trashService: TrashService
let modalService: NgbModal
let toastService: ToastService
let router: Router
beforeEach(async () => {
await TestBed.configureTestingModule({
@@ -61,11 +63,13 @@ describe('TrashComponent', () => {
trashService = TestBed.inject(TrashService)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
router = TestBed.inject(Router)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should call correct service method on reload', () => {
jest.useFakeTimers()
const trashSpy = jest.spyOn(trashService, 'getTrash')
trashSpy.mockReturnValue(
of({
@@ -75,6 +79,7 @@ describe('TrashComponent', () => {
})
)
component.reload()
jest.advanceTimersByTime(100)
expect(trashSpy).toHaveBeenCalled()
expect(component.documentsInTrash).toEqual(documentsInTrash)
})
@@ -161,6 +166,22 @@ describe('TrashComponent', () => {
expect(restoreSpy).toHaveBeenCalledWith([1, 2])
})
it('should offer link to restored document', () => {
let toasts
const navigateSpy = jest.spyOn(router, 'navigate')
toastService.getToasts().subscribe((allToasts) => {
toasts = [...allToasts]
})
jest.spyOn(trashService, 'restoreDocuments').mockReturnValue(of('OK'))
component.restore(documentsInTrash[0])
expect(toasts.length).toEqual(1)
toasts[0].action()
expect(navigateSpy).toHaveBeenCalledWith([
'documents',
documentsInTrash[0].id,
])
})
it('should support toggle all items in view', () => {
component.documentsInTrash = documentsInTrash
expect(component.selectedDocuments.size).toEqual(0)
@@ -1,49 +1,57 @@
import { Component, OnDestroy } from '@angular/core'
import { Router } from '@angular/router'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { delay, takeUntil, tap } from 'rxjs'
import { Document } from 'src/app/data/document'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { TrashService } from 'src/app/services/trash.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { Subject, takeUntil } from 'rxjs'
import { SettingsService } from 'src/app/services/settings.service'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
selector: 'pngx-trash',
templateUrl: './trash.component.html',
styleUrl: './trash.component.scss',
})
export class TrashComponent implements OnDestroy {
export class TrashComponent
extends LoadingComponentWithPermissions
implements OnDestroy
{
public documentsInTrash: Document[] = []
public selectedDocuments: Set<number> = new Set()
public allToggled: boolean = false
public page: number = 1
public totalDocuments: number
public isLoading: boolean = false
unsubscribeNotifier: Subject<void> = new Subject()
constructor(
private trashService: TrashService,
private toastService: ToastService,
private modalService: NgbModal,
private settingsService: SettingsService
private settingsService: SettingsService,
private router: Router
) {
super()
this.reload()
}
ngOnDestroy() {
this.unsubscribeNotifier.next()
this.unsubscribeNotifier.complete()
}
reload() {
this.isLoading = true
this.trashService.getTrash(this.page).subscribe((r) => {
this.documentsInTrash = r.results
this.totalDocuments = r.count
this.isLoading = false
this.selectedDocuments.clear()
})
this.loading = true
this.trashService
.getTrash(this.page)
.pipe(
tap((r) => {
this.documentsInTrash = r.results
this.totalDocuments = r.count
this.selectedDocuments.clear()
this.loading = false
}),
delay(100)
)
.subscribe(() => {
this.show = true
})
}
delete(document: Document) {
@@ -110,7 +118,14 @@ export class TrashComponent implements OnDestroy {
restore(document: Document) {
this.trashService.restoreDocuments([document.id]).subscribe({
next: () => {
this.toastService.showInfo($localize`Document restored`)
this.toastService.show({
content: $localize`Document restored`,
delay: 5000,
actionName: $localize`Open document`,
action: () => {
this.router.navigate(['documents', document.id])
},
})
this.reload()
},
error: (err) => {
@@ -1,4 +1,5 @@
import { DatePipe } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
@@ -9,14 +10,17 @@ import {
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterTestingModule } from '@angular/router/testing'
import {
NgbModule,
NgbAlertModule,
NgbModal,
NgbModalRef,
NgbModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { throwError, of } from 'rxjs'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
@@ -41,10 +45,6 @@ import { TextComponent } from '../../common/input/text/text.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { SettingsComponent } from '../settings/settings.component'
import { UsersAndGroupsComponent } from './users-groups.component'
import { User } from 'src/app/data/user'
import { Group } from 'src/app/data/group'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const users = [
{ id: 1, username: 'user1', is_superuser: false },
@@ -6,13 +6,13 @@ import { User } from 'src/app/data/user'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { SettingsService } from 'src/app/services/settings.service'
@Component({
selector: 'pngx-users-groups',
@@ -1,4 +1,4 @@
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow-sm">
<nav class="navbar navbar-dark fixed-top bg-primary flex-md-nowrap p-0 shadow-sm">
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
(click)="isMenuCollapsed = !isMenuCollapsed">
@@ -43,7 +43,7 @@
<div class="dropdown-divider"></div>
</div>
<button ngbDropdownItem class="nav-link" (click)="editProfile()">
<i-bs class="me-2" name="person"></i-bs>&nbsp;<ng-container i18n>My Profile</ng-container>
<i-bs class="me-2" name="person"></i-bs><ng-container i18n>My Profile</ng-container>
</button>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
*pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }">
@@ -199,6 +199,13 @@
<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 app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="window-stack"></i-bs><span>&nbsp;<ng-container i18n>Saved Views</ng-container></span>
</a>
</li>
<li class="nav-item app-link"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Workflow }"
tourAnchor="tour.workflows">
@@ -327,7 +334,7 @@
</a>
}
} @else {
<a class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
<a *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" class="small text-decoration-none" routerLink="/settings" fragment="update-checking"
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
container="body">
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
@@ -12,6 +12,9 @@
z-index: 995; /* Behind the navbar */
padding: 50px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
overflow-y: auto;
--pngx-sidebar-width: 100%;
max-width: var(--pngx-sidebar-width);
.sidebar-heading .spinner-border {
width: 0.8em;
@@ -24,15 +27,15 @@
// These come from the col-* classes for non-slim sidebar, needed for animation
@media (min-width: 768px) {
max-width: 25%;
--pngx-sidebar-width: 25%;
}
@media (min-width: 992px) {
max-width: 16.66666667%;
--pngx-sidebar-width: 16.66666667%;
}
@media (min-width: 2400px) {
max-width: 8.33333333%;
--pngx-sidebar-width: 8.33333333%;
}
transition: all .2s ease;
@@ -45,6 +48,13 @@
main {
transition: all .2s ease;
padding-top: 110px;
}
@media (min-width: 768px) {
main {
padding-top: 56px;
}
}
.sidebar-slim-toggler {
@@ -109,12 +119,17 @@ main {
.sidebar-slim-toggler {
display: block;
position: absolute;
right: -12px;
position: fixed;
left: calc(var(--pngx-sidebar-width) - 12px);
top: 60px;
z-index: 996;
--bs-btn-padding-x: 0.35rem;
--bs-btn-padding-y: 0.125rem;
transition: all .2s ease;
}
.sidebar.slim .sidebar-slim-toggler {
--pngx-sidebar-width: 50px !important;
}
}
@@ -1,43 +1,43 @@
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import { AppFrameComponent } from './app-frame.component'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { BrowserModule } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing'
import { SettingsService } from 'src/app/services/settings.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service'
import { routes } from 'src/app/app-routing.module'
import { SavedView } from 'src/app/data/saved-view'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import {
DjangoMessageLevel,
DjangoMessagesService,
} from 'src/app/services/django-messages.service'
import { environment } from 'src/environments/environment'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { ActivatedRoute, Router } from '@angular/router'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
import { PermissionsService } from 'src/app/services/permissions.service'
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service'
import { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { SavedView } from 'src/app/data/saved-view'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
import { AppFrameComponent } from './app-frame.component'
import { GlobalSearchComponent } from './global-search/global-search.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const saved_views = [
{
@@ -115,7 +115,7 @@ describe('AppFrameComponent', () => {
{
provide: SavedViewService,
useValue: {
initialize: () => {},
reload: () => {},
listAll: () =>
of({
all: [saved_views.map((v) => v.id)],
@@ -170,7 +170,7 @@ describe('AppFrameComponent', () => {
.mockReturnValue('Hello World')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
savedViewSpy = jest.spyOn(savedViewService, 'initialize')
savedViewSpy = jest.spyOn(savedViewService, 'reload')
fixture = TestBed.createComponent(AppFrameComponent)
component = fixture.componentInstance
@@ -343,6 +343,7 @@ describe('AppFrameComponent', () => {
component.editProfile()
expect(modalSpy).toHaveBeenCalledWith(ProfileEditDialogComponent, {
backdrop: 'static',
size: 'xl',
})
})
@@ -1,40 +1,40 @@
import {
CdkDragDrop,
CdkDragEnd,
CdkDragStart,
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { Component, HostListener, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Observable } from 'rxjs'
import { first } from 'rxjs/operators'
import { Document } from 'src/app/data/document'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { SavedView } from 'src/app/data/saved-view'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
import {
DjangoMessageLevel,
DjangoMessagesService,
} from 'src/app/services/django-messages.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { environment } from 'src/environments/environment'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
import {
RemoteVersionService,
AppRemoteVersion,
} from 'src/app/services/rest/remote-version.service'
import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { ToastService } from 'src/app/services/toast.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import {
PermissionAction,
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { SavedView } from 'src/app/data/saved-view'
import {
CdkDragStart,
CdkDragEnd,
CdkDragDrop,
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
AppRemoteVersion,
RemoteVersionService,
} from 'src/app/services/rest/remote-version.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
@Component({
selector: 'pngx-app-frame',
@@ -73,7 +73,7 @@ export class AppFrameComponent
PermissionType.SavedView
)
) {
this.savedViewService.initialize()
this.savedViewService.reload()
}
}
@@ -81,7 +81,14 @@ export class AppFrameComponent
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
this.checkForUpdates()
}
this.tasksService.reload()
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.PaperlessTask
)
) {
this.tasksService.reload()
}
this.djangoMessagesService.get().forEach((message) => {
switch (message.level) {
@@ -136,6 +143,7 @@ export class AppFrameComponent
editProfile() {
this.modalService.open(ProfileEditDialogComponent, {
backdrop: 'static',
size: 'xl',
})
this.closeMenu()
}
@@ -49,14 +49,14 @@
[disabled]="disablePrimaryButton(type, item)"
(mouseenter)="onButtonHover($event)">
@if (type === DataType.Document) {
<i-bs width="1em" height="1em" name="pencil"></i-bs>
<i-bs width="1em" height="1em" name="file-earmark-richtext"></i-bs>
<span>&nbsp;<ng-container i18n>Open</ng-container></span>
} @else if (type === DataType.SavedView) {
<i-bs width="1em" height="1em" name="eye"></i-bs>
<span>&nbsp;<ng-container i18n>Open</ng-container></span>
} @else if (type === DataType.Workflow || type === DataType.CustomField || type === DataType.Group || type === DataType.User || type === DataType.MailAccount || type === DataType.MailRule) {
<i-bs width="1em" height="1em" name="pencil"></i-bs>
<span>&nbsp;<ng-container i18n>Edit</ng-container></span>
<span>&nbsp;<ng-container i18n>Open</ng-container></span>
} @else {
<i-bs width="1em" height="1em" name="filter"></i-bs>
<span>&nbsp;<ng-container i18n>Filter documents</ng-container></span>
@@ -72,8 +72,8 @@
<i-bs width="1em" height="1em" name="download"></i-bs>
<span>&nbsp;<ng-container i18n>Download</ng-container></span>
} @else {
<i-bs width="1em" height="1em" name="pencil"></i-bs>
<span>&nbsp;<ng-container i18n>Edit</ng-container></span>
<i-bs width="1em" height="1em" name="file-earmark-richtext"></i-bs>
<span>&nbsp;<ng-container i18n>Open</ng-container></span>
}
</button>
}
@@ -65,10 +65,6 @@ form {
--pngx-focus-alpha: 0;
}
.cursor-pointer {
cursor: pointer;
}
.mh-75 {
max-height: 75vh;
}
@@ -1,12 +1,13 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ElementRef } from '@angular/core'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { GlobalSearchComponent } from './global-search.component'
import { of } from 'rxjs'
import { SearchService } from 'src/app/services/rest/search.service'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router } from '@angular/router'
import {
NgbDropdownModule,
@@ -14,11 +15,9 @@ import {
NgbModalModule,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { DataType } from 'src/app/data/datatype'
import {
FILTER_FULLTEXT_QUERY,
FILTER_HAS_CORRESPONDENT_ANY,
@@ -27,20 +26,21 @@ import {
FILTER_HAS_TAGS_ALL,
FILTER_TITLE_CONTENT,
} from 'src/app/data/filter-rule-type'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { DocumentService } from 'src/app/services/rest/document.service'
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
import { ElementRef } from '@angular/core'
import { ToastService } from 'src/app/services/toast.service'
import { DataType } from 'src/app/data/datatype'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
import { SettingsService } from 'src/app/services/settings.service'
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SearchService } from 'src/app/services/rest/search.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
import { GlobalSearchComponent } from './global-search.component'
const searchResults = {
total: 11,
@@ -1,14 +1,15 @@
import {
Component,
ViewChild,
ElementRef,
ViewChildren,
QueryList,
OnInit,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core'
import { Router } from '@angular/router'
import { NgbDropdown, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { Subject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
import { Subject, debounceTime, distinctUntilChanged, filter } from 'rxjs'
import { DataType } from 'src/app/data/datatype'
import {
FILTER_FULLTEXT_QUERY,
FILTER_HAS_CORRESPONDENT_ANY,
@@ -17,19 +18,22 @@ import {
FILTER_HAS_TAGS_ALL,
FILTER_TITLE_CONTENT,
} from 'src/app/data/filter-rule-type'
import { DataType } from 'src/app/data/datatype'
import { ObjectWithId } from 'src/app/data/object-with-id'
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { HotKeyService } from 'src/app/services/hot-key.service'
import {
PermissionsService,
PermissionAction,
PermissionsService,
} from 'src/app/services/permissions.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import {
GlobalSearchResult,
SearchService,
} from 'src/app/services/rest/search.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
import { paramsFromViewState } from 'src/app/utils/query-params'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
@@ -41,10 +45,6 @@ import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
import { HotKeyService } from 'src/app/services/hot-key.service'
import { paramsFromViewState } from 'src/app/utils/query-params'
import { SettingsService } from 'src/app/services/settings.service'
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
@Component({
selector: 'pngx-global-search',
@@ -89,7 +89,6 @@ export class GlobalSearchComponent implements OnInit {
this.queryDebounce
.pipe(
debounceTime(400),
map((query) => query?.trim()),
filter((query) => !query?.length || query?.length > 2),
distinctUntilChanged()
)
@@ -109,7 +108,7 @@ export class GlobalSearchComponent implements OnInit {
private search(query: string) {
this.loading = true
this.searchService.globalSearch(query).subscribe((results) => {
this.searchService.globalSearch(query.trim()).subscribe((results) => {
this.searchResults = results
this.loading = false
this.resultsDropdown.open()
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ClearableBadgeComponent } from './clearable-badge.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ClearableBadgeComponent } from './clearable-badge.component'
describe('ClearableBadgeComponent', () => {
let component: ClearableBadgeComponent
@@ -1,4 +1,4 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'
import { Component, EventEmitter, Input, Output } from '@angular/core'
@Component({
selector: 'pngx-clearable-badge',
@@ -1,8 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ConfirmButtonComponent } from './confirm-button.component'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from './confirm-button.component'
describe('ConfirmButtonComponent', () => {
let component: ConfirmButtonComponent
@@ -1,14 +1,8 @@
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ConfirmDialogComponent } from './confirm-dialog.component'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { Subject } from 'rxjs'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ConfirmDialogComponent } from './confirm-dialog.component'
describe('ConfirmDialogComponent', () => {
let component: ConfirmDialogComponent
@@ -1,14 +1,17 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { interval, Subject, take } from 'rxjs'
import { Subject } from 'rxjs'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
selector: 'pngx-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss'],
})
export class ConfirmDialogComponent {
constructor(public activeModal: NgbActiveModal) {}
export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
constructor(public activeModal: NgbActiveModal) {
super()
}
@Output()
public confirmClicked = new EventEmitter()
@@ -1,6 +1,6 @@
.pdf-viewer-container {
background-color: gray;
height: 350px;
height: 550px;
pdf-viewer {
width: 100%;
@@ -1,12 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PdfViewerComponent } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { PdfViewerComponent } from 'ng2-pdf-viewer'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
describe('DeletePagesConfirmDialogComponent', () => {
let component: DeletePagesConfirmDialogComponent
@@ -1,8 +1,8 @@
import { Component, TemplateRef, ViewChild } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PDFDocumentProxy, PdfViewerComponent } from 'ng2-pdf-viewer'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
import { PDFDocumentProxy, PdfViewerComponent } from 'ng2-pdf-viewer'
@Component({
selector: 'pngx-delete-pages-confirm-dialog',
@@ -1,12 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
describe('MergeConfirmDialogComponent', () => {
let component: MergeConfirmDialogComponent
@@ -1,11 +1,11 @@
import { Component, OnInit } from '@angular/core'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { DocumentService } from 'src/app/services/rest/document.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
import { Subject, takeUntil } from 'rxjs'
import { Component, OnInit } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { takeUntil } from 'rxjs'
import { Document } from 'src/app/data/document'
import { PermissionsService } from 'src/app/services/permissions.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-merge-confirm-dialog',
@@ -25,8 +25,6 @@ export class MergeConfirmDialogComponent
public metadataDocumentID: number = -1
private unsubscribeNotifier: Subject<any> = new Subject()
constructor(
activeModal: NgbActiveModal,
private documentService: DocumentService,
@@ -1,10 +1,10 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
describe('RotateConfirmDialogComponent', () => {
let component: RotateConfirmDialogComponent
@@ -1,7 +1,7 @@
import { Component } from '@angular/core'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-rotate-confirm-dialog',
@@ -6,7 +6,7 @@
<div class="modal-body">
<p>{{message}}</p>
<div class="row mb-2">
<div class="col-8">
<div class="col-7">
<div class="input-group input-group-sm">
<div class="input-group-text" i18n>Page</div>
<input class="form-control" type="number" min="1" [(ngModel)]="page" />
@@ -21,7 +21,7 @@
</pdf-viewer>
</div>
</div>
<div class="col-4">
<div class="col-5">
<div class="d-grid">
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit">
<i-bs name="plus-circle"></i-bs>&nbsp;
@@ -44,12 +44,12 @@
</ul>
</div>
</div>
<div class="form-check form-switch mt-4">
</div>
<div class="modal-footer">
<div class="form-check form-switch me-auto">
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalSwitch" [(ngModel)]="deleteOriginal" [disabled]="!userOwnsDocument">
<label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button>
@@ -1,6 +1,6 @@
.pdf-viewer-container {
background-color: gray;
height: 350px;
height: 500px;
pdf-viewer {
width: 100%;
@@ -1,14 +1,14 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { SplitConfirmDialogComponent } from './split-confirm-dialog.component'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ReactiveFormsModule, FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { DocumentService } from 'src/app/services/rest/document.service'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { of } from 'rxjs'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SplitConfirmDialogComponent } from './split-confirm-dialog.component'
describe('SplitConfirmDialogComponent', () => {
let component: SplitConfirmDialogComponent
@@ -1,10 +1,10 @@
import { Component, OnInit } from '@angular/core'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
import { Document } from 'src/app/data/document'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { DocumentService } from 'src/app/services/rest/document.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { PDFDocumentProxy } from 'ng2-pdf-viewer'
import { Document } from 'src/app/data/document'
import { PermissionsService } from 'src/app/services/permissions.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-split-confirm-dialog',
@@ -18,9 +18,11 @@
@case (CustomFieldDataType.DocumentLink) {
<div [ngbTooltip]="nameTooltip" class="d-flex gap-1 flex-wrap">
@for (docId of value; track docId) {
<a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title>
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<span>{{ getDocumentTitle(docId) }}</span>
</a>
@if (getDocumentTitle(docId)) {
<a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title>
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<span>{{ getDocumentTitle(docId) }}</span>
</a>
}
}
</div>
}
@@ -1,12 +1,12 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { DisplayField, Document } from 'src/app/data/document'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { CustomFieldDisplayComponent } from './custom-field-display.component'
import { DisplayField, Document } from 'src/app/data/document'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const customFields: CustomField[] = [
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
@@ -17,7 +17,11 @@ const customFields: CustomField[] = [
name: 'Field 4',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: ['Option 1', 'Option 2', 'Option 3'],
select_options: [
{ label: 'Option 1', id: 'abc-123' },
{ label: 'Option 2', id: 'def-456' },
{ label: 'Option 3', id: 'ghi-789' },
],
},
},
{
@@ -131,6 +135,8 @@ describe('CustomFieldDisplayComponent', () => {
})
it('should show select value', () => {
expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
expect(component.getSelectValue(customFields[3], 'ghi-789')).toEqual(
'Option 3'
)
})
})
@@ -1,25 +1,22 @@
import { getLocaleCurrencyCode } from '@angular/common'
import {
Component,
Inject,
Input,
LOCALE_ID,
OnDestroy,
OnInit,
} from '@angular/core'
import { Subject, takeUntil } from 'rxjs'
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'
import { takeUntil } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { DisplayField, Document } from 'src/app/data/document'
import { Results } from 'src/app/data/results'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
selector: 'pngx-custom-field-display',
templateUrl: './custom-field-display.component.html',
styleUrl: './custom-field-display.component.scss',
})
export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
export class CustomFieldDisplayComponent
extends LoadingComponentWithPermissions
implements OnInit
{
CustomFieldDataType = CustomFieldDataType
private _document: Document
@@ -61,7 +58,6 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
private docLinkDocuments: Document[] = []
private unsubscribeNotifier: Subject<any> = new Subject()
private defaultCurrencyCode: any
constructor(
@@ -69,6 +65,7 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
private documentService: DocumentService,
@Inject(LOCALE_ID) currentLocale: string
) {
super()
this.defaultCurrencyCode = getLocaleCurrencyCode(currentLocale)
this.customFieldService.listAll().subscribe((r) => {
this.customFields = r.results
@@ -107,9 +104,9 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
.getFew(this.value, { fields: 'id,title' })
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((result: Results<Document>) => {
this.docLinkDocuments = this.value.map((id) =>
result.results.find((d) => d.id === id)
)
this.docLinkDocuments = this.value
.map((id) => result.results.find((d) => d.id === id))
.filter((d) => d)
})
}
@@ -117,12 +114,7 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
}
public getSelectValue(field: CustomField, index: number): string {
return field.extra_data.select_options[index]
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
public getSelectValue(field: CustomField, id: string): string {
return field.extra_data.select_options?.find((o) => o.id === id)?.label
}
}
@@ -1,28 +1,28 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ToastService } from 'src/app/services/toast.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { SelectComponent } from '../input/select/select.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import {
NgbDropdownModule,
NgbModal,
NgbModalModule,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { By } from '@angular/platform-browser'
import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { SelectComponent } from '../input/select/select.component'
import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component'
const fields: CustomField[] = [
{
@@ -3,31 +3,31 @@ import {
ElementRef,
EventEmitter,
Input,
OnDestroy,
Output,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, first, takeUntil } from 'rxjs'
import { first, takeUntil } from 'rxjs'
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import {
PermissionAction,
PermissionType,
PermissionsService,
} from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@Component({
selector: 'pngx-custom-fields-dropdown',
templateUrl: './custom-fields-dropdown.component.html',
styleUrls: ['./custom-fields-dropdown.component.scss'],
})
export class CustomFieldsDropdownComponent implements OnDestroy {
export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions {
@Input()
documentId: number
@@ -60,8 +60,6 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
public filterText: string
private unsubscribeNotifier: Subject<any> = new Subject()
get canCreateFields(): boolean {
return this.permissionsService.currentUserCan(
PermissionAction.Add,
@@ -75,14 +73,10 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
private toastService: ToastService,
private permissionsService: PermissionsService
) {
super()
this.getFields()
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(this)
this.unsubscribeNotifier.complete()
}
private getFields() {
this.customFieldsService
.listAll()
@@ -0,0 +1,167 @@
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
<button class="btn btn-sm btn-outline-primary" id="dropdown_toggle" ngbDropdownToggle [disabled]="disabled">
<i-bs name="{{icon}}"></i-bs>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
@if (isActive) {
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
}
</button>
<div class="px-3 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
<div class="list-group list-group-flush">
@for (element of selectionModel.queries; track element.id; let i = $index) {
<div class="list-group-item px-0 d-flex flex-nowrap">
@switch (element.type) {
@case (CustomFieldQueryComponentType.Atom) {
<ng-container *ngTemplateOutlet="queryAtom; context: { atom: element }"></ng-container>
}
@case (CustomFieldQueryComponentType.Expression) {
<ng-container *ngTemplateOutlet="queryExpression; context: { expression: element }"></ng-container>
}
}
</div>
}
</div>
</div>
</div>
<ng-template #comparisonValueTemplate let-atom="atom">
@if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Date) {
<input class="form-control" placeholder="yyyy-mm-dd"
[(ngModel)]="atom.value"
ngbDatepicker
#d="ngbDatepicker" />
<button class="btn btn-sm btn-outline-secondary rounded-end" (click)="d.toggle()" type="button">
<i-bs name="calendar-event"></i-bs>
</button>
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Float || getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Integer) {
<input class="w-25 form-control rounded-end" type="number" [(ngModel)]="atom.value" [disabled]="disabled">
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Boolean) {
<select class="w-25 form-select rounded-end" [(ngModel)]="atom.value" [disabled]="disabled">
<option value="true" i18n>True</option>
<option value="false" i18n>False</option>
</select>
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Select) {
<ng-select #fieldSelects
class="paperless-input-select rounded-end"
[items]="getSelectOptionsForField(atom.field)"
bindLabel="label"
bindValue="id"
[(ngModel)]="atom.value"
[disabled]="disabled"
(mousedown)="$event.stopImmediatePropagation()"
></ng-select>
} @else {
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="atom.value" [disabled]="disabled">
}
</ng-template>
<ng-template #queryAtom let-atom="atom">
<div class="input-group input-group-sm">
<ng-select
class="paperless-input-select"
[items]="customFields"
[(ngModel)]="atom.field"
[disabled]="disabled"
bindLabel="name"
bindValue="id"
(mousedown)="$event.stopImmediatePropagation()"
></ng-select>
<select class="w-25 form-select" [(ngModel)]="atom.operator" [disabled]="disabled">
<option *ngFor="let operator of getOperatorsForField(atom.field)" [ngValue]="operator.value">{{operator.label}}</option>
</select>
@switch (atom.operator) {
@case (CustomFieldQueryOperator.Exists) {
<select class="w-25 form-select rounded-end" [(ngModel)]="atom.value" [disabled]="disabled">
<option value="true" i18n>True</option>
<option value="false" i18n>False</option>
</select>
}
@case (CustomFieldQueryOperator.IsNull) {
<select class="w-25 form-select rounded-end" [(ngModel)]="atom.value" [disabled]="disabled">
<option value="true" i18n>True</option>
<option value="false" i18n>False</option>
</select>
}
@case (CustomFieldQueryOperator.GreaterThanOrEqual) {
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
}
@case (CustomFieldQueryOperator.LessThanOrEqual) {
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
}
@case (CustomFieldQueryOperator.GreaterThan) {
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
}
@case (CustomFieldQueryOperator.LessThan) {
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
}
@case (CustomFieldQueryOperator.Contains) {
<pngx-input-document-link [(ngModel)]="atom.value" class="w-25 form-select doc-link-select p-0" placeholder="Search docs..." i18n-placeholder [minimal]="true"></pngx-input-document-link>
}
@case (CustomFieldQueryOperator.In) {
<ng-select
class="paperless-input-select rounded-end"
[items]="getSelectOptionsForField(atom.field)"
bindLabel="label"
bindValue="id"
[(ngModel)]="atom.value"
[disabled]="disabled"
[multiple]="true"
(mousedown)="$event.stopImmediatePropagation()"
></ng-select>
}
@case (CustomFieldQueryOperator.Exact) {
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
}
@default {
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="atom.value" [disabled]="disabled">
}
}
<button class="btn btn-link btn-sm text-danger pe-0" type="button" (click)="removeElement(atom)" [disabled]="disabled">
<i-bs name="x-circle"></i-bs>
</button>
</div>
</ng-template>
<ng-template #queryExpression let-expression="expression">
<div class="d-flex w-100">
<div class="d-flex flex-grow-1 flex-column">
<div class="btn-group btn-group-xs" role="group">
<input [(ngModel)]="expression.operator" type="radio" class="btn-check" id="logicalOperatorOr_{{expression.id}}" name="logicalOperatorOr_{{expression.id}}" value="OR" [disabled]="expression.depth > 0 && expression.value.length < 2">
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{expression.id}}" i18n>Any</label>
<input [(ngModel)]="expression.operator" type="radio" class="btn-check" id="logicalOperatorAnd_{{expression.id}}" name="logicalOperatorAnd_{{expression.id}}" value="AND" [disabled]="expression.depth > 0 && expression.value.length < 2">
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{expression.id}}" i18n>All</label>
@if (expression.negatable) {
<input [(ngModel)]="expression.operator" type="radio" class="btn-check" id="logicalOperatorNot_{{expression.id}}" name="logicalOperatorNot_{{expression.id}}" value="NOT">
<label class="btn btn-outline-secondary" for="logicalOperatorNot_{{expression.id}}" i18n>Not</label>
}
</div>
<div class="list-group list-group-flush mb-n2">
@for (element of expression.value; track element.id; let i = $index) {
<div class="list-group-item px-0 d-flex flex-nowrap">
@switch (element.type) {
@case (CustomFieldQueryComponentType.Atom) {
<ng-container *ngTemplateOutlet="queryAtom; context: { atom: element }"></ng-container>
}
@case (CustomFieldQueryComponentType.Expression) {
<ng-container *ngTemplateOutlet="queryExpression; context: { expression: element }"></ng-container>
}
}
</div>
}
</div>
</div>
<div class="btn-group-vertical ms-2 ps-2 border-start" role="group" aria-label="Vertical button group">
<button type="button" class="btn btn-sm btn-outline-secondary text-primary" title="Add query" i18n-title (click)="addAtom(expression)" [disabled]="disabled || expression.value.length === CUSTOM_FIELD_QUERY_MAX_ATOMS">
<i-bs name="node-plus"></i-bs>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-primary" title="Add expression" i18n-title (click)="addExpression(expression)" [disabled]="disabled || expression.depth === CUSTOM_FIELD_QUERY_MAX_DEPTH">
<i-bs name="braces"></i-bs>
</button>
@if (expression.depth > 0) {
<button type="button" class="btn btn-sm btn-outline-secondary text-danger" (click)="removeElement(expression)" [disabled]="disabled">
<i-bs name="x-circle"></i-bs>
</button>
}
</div>
</div>
</ng-template>
@@ -0,0 +1,43 @@
.dropdown-menu {
width: 370px;
@media(min-width: 768px) {
width: 600px;
}
}
:host ::ng-deep .ng-select-container {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
height: 100% !important;
}
:host ::ng-deep .rounded-end .ng-select-container {
border-top-right-radius: var(--bs-border-radius) !important;
border-bottom-right-radius: var(--bs-border-radius) !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
:host ::ng-deep .ng-select {
max-width: 100px;
min-width: 35%;
font-size: 14px;
}
:host ::ng-deep .doc-link-select {
padding-top: 0 !important;
border-top-right-radius: var(--bs-border-radius) !important;
border-bottom-right-radius: var(--bs-border-radius) !important;
background-image: none !important;
.ng-select-container,
.ng-select.ng-select-opened > .ng-select-container {
border: none !important;
min-height: 34px !important;
background: none !important;
}
.ng-select {
max-width: 200px;
min-width: 140px;
}
}
@@ -0,0 +1,355 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import {
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
CustomFieldQueryLogicalOperator,
CustomFieldQueryOperatorGroups,
} from 'src/app/data/custom-field-query'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import {
CustomFieldQueryAtom,
CustomFieldQueryElement,
CustomFieldQueryExpression,
} from 'src/app/utils/custom-field-query-element'
import {
CustomFieldQueriesModel,
CustomFieldsQueryDropdownComponent,
} from './custom-fields-query-dropdown.component'
const customFields = [
{
id: 1,
name: 'Test Field',
data_type: CustomFieldDataType.String,
extra_data: {},
},
{
id: 2,
name: 'Test Select Field',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: [
{ label: 'Option 1', id: 'abc-123' },
{ label: 'Option 2', id: 'def-456' },
],
},
},
]
describe('CustomFieldsQueryDropdownComponent', () => {
let component: CustomFieldsQueryDropdownComponent
let fixture: ComponentFixture<CustomFieldsQueryDropdownComponent>
let customFieldsService: CustomFieldsService
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CustomFieldsQueryDropdownComponent],
imports: [
NgbDropdownModule,
NgxBootstrapIconsModule.pick(allIcons),
NgSelectModule,
FormsModule,
ReactiveFormsModule,
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents()
customFieldsService = TestBed.inject(CustomFieldsService)
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
count: customFields.length,
all: customFields.map((f) => f.id),
results: customFields,
})
)
fixture = TestBed.createComponent(CustomFieldsQueryDropdownComponent)
component = fixture.componentInstance
component.icon = 'ui-radios'
fixture.detectChanges()
})
it('should initialize custom fields on creation', () => {
expect(component.customFields).toEqual(customFields)
})
it('should add an expression when opened if queries are empty', () => {
component.selectionModel.clear()
component.onOpenChange(true)
expect(component.selectionModel.queries.length).toBe(1)
})
it('should support reset the selection model', () => {
component.selectionModel.addExpression()
component.reset()
expect(component.selectionModel.isEmpty()).toBeTruthy()
})
it('should get operators for a field', () => {
const field: CustomField = {
id: 1,
name: 'Test Field',
data_type: CustomFieldDataType.String,
extra_data: {},
}
component.customFields = [field]
const operators = component.getOperatorsForField(1)
expect(operators.length).toEqual(
[
...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
CustomFieldQueryOperatorGroups.Basic
],
...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
CustomFieldQueryOperatorGroups.String
],
].length
)
// Fallback to basic operators if field is not found
const operators2 = component.getOperatorsForField(2)
expect(operators2.length).toEqual(
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
CustomFieldQueryOperatorGroups.Basic
].length
)
})
it('should get select options for a field', () => {
const field: CustomField = {
id: 1,
name: 'Test Field',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: [
{ label: 'Option 1', id: 'abc-123' },
{ label: 'Option 2', id: 'def-456' },
],
},
}
component.customFields = [field]
const options = component.getSelectOptionsForField(1)
expect(options).toEqual([
{ label: 'Option 1', id: 'abc-123' },
{ label: 'Option 2', id: 'def-456' },
])
// Fallback to empty array if field is not found
const options2 = component.getSelectOptionsForField(2)
expect(options2).toEqual([])
})
it('should remove an element from the selection model', () => {
const expression = new CustomFieldQueryExpression()
const atom = new CustomFieldQueryAtom()
;(expression.value as CustomFieldQueryElement[]).push(atom)
component.selectionModel.addExpression(expression)
component.removeElement(atom)
expect(component.selectionModel.isEmpty()).toBeTruthy()
const expression2 = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And,
[
[1, 'icontains', 'test'],
[2, 'icontains', 'test'],
],
])
component.selectionModel.addExpression(expression2)
component.removeElement(expression2)
expect(component.selectionModel.isEmpty()).toBeTruthy()
})
it('should emit selectionModelChange when model changes', () => {
const nextSpy = jest.spyOn(component.selectionModelChange, 'next')
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
component.selectionModel.addAtom(atom)
atom.changed.next(atom)
expect(nextSpy).toHaveBeenCalled()
})
it('should complete selection model subscription when new selection model is set', () => {
const completeSpy = jest.spyOn(component.selectionModel.changed, 'complete')
const selectionModel = new CustomFieldQueriesModel()
component.selectionModel = selectionModel
expect(completeSpy).toHaveBeenCalled()
})
it('should support adding an atom', () => {
const expression = new CustomFieldQueryExpression()
component.addAtom(expression)
expect(expression.value.length).toBe(1)
})
it('should support adding an expression', () => {
const expression = new CustomFieldQueryExpression()
component.addExpression(expression)
expect(expression.value.length).toBe(1)
})
it('should support getting a custom field by ID', () => {
expect(component.getCustomFieldByID(1)).toEqual(customFields[0])
})
it('should sanitize name from title', () => {
component.title = 'Test Title'
expect(component.name).toBe('test_title')
})
it('should add a default atom on open and focus the select field', fakeAsync(() => {
expect(component.selectionModel.queries.length).toBe(0)
component.onOpenChange(true)
fixture.detectChanges()
tick()
expect(component.selectionModel.queries.length).toBe(1)
expect(window.document.activeElement.tagName).toBe('INPUT')
}))
describe('CustomFieldQueriesModel', () => {
let model: CustomFieldQueriesModel
beforeEach(() => {
model = new CustomFieldQueriesModel()
})
it('should initialize with empty queries', () => {
expect(model.queries).toEqual([])
})
it('should clear queries and fire event', () => {
const nextSpy = jest.spyOn(model.changed, 'next')
model.addExpression()
model.clear()
expect(model.queries).toEqual([])
expect(nextSpy).toHaveBeenCalledWith(model)
})
it('should clear queries without firing event', () => {
const nextSpy = jest.spyOn(model.changed, 'next')
model.addExpression()
model.clear(false)
expect(model.queries).toEqual([])
expect(nextSpy).not.toHaveBeenCalled()
})
it('should validate an empty model as invalid', () => {
expect(model.isValid()).toBeFalsy()
})
it('should validate a model with valid expression as valid', () => {
const expression = new CustomFieldQueryExpression()
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
const atom2 = new CustomFieldQueryAtom([2, 'icontains', 'test'])
const expression2 = new CustomFieldQueryExpression()
expression2.addAtom(atom)
expression2.addAtom(atom2)
expression.addExpression(expression2)
model.addExpression(expression)
expect(model.isValid()).toBeTruthy()
})
it('should validate a model with invalid expression as invalid', () => {
const expression = new CustomFieldQueryExpression()
model.addExpression(expression)
expect(model.isValid()).toBeFalsy()
})
it('should validate an atom with in or contains operator', () => {
const atom = new CustomFieldQueryAtom([1, 'in', '[1,2,3]'])
expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
atom.operator = 'contains'
atom.value = [1, 2, 3]
expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
atom.value = null
expect(model['validateAtom'].apply(null, [atom])).toBeFalsy()
})
it('should check if model is empty', () => {
expect(model.isEmpty()).toBeTruthy()
model.addExpression()
expect(model.isEmpty()).toBeTruthy()
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
model.addAtom(atom)
expect(model.isEmpty()).toBeFalsy()
})
it('should add an atom to the model', () => {
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
model.addAtom(atom)
expect(model.queries.length).toBe(1)
expect(
(model.queries[0] as CustomFieldQueryExpression).value.length
).toBe(1)
})
it('should add an expression to the model, propagate changes', () => {
const expression = new CustomFieldQueryExpression()
model.addExpression(expression)
expect(model.queries.length).toBe(1)
const expression2 = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And,
[
[1, 'icontains', 'test'],
[2, 'icontains', 'test'],
],
])
model.addExpression(expression2)
const nextSpy = jest.spyOn(model.changed, 'next')
expression2.changed.next(expression2)
expect(nextSpy).toHaveBeenCalled()
})
it('should remove an element from the model', () => {
const expression = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And,
[
[1, 'icontains', 'test'],
[2, 'icontains', 'test'],
],
])
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
const expression2 = new CustomFieldQueryExpression([
CustomFieldQueryLogicalOperator.And,
[
[3, 'icontains', 'test'],
[4, 'icontains', 'test'],
],
])
expression.addAtom(atom)
expression2.addExpression(expression)
model.addExpression(expression2)
model.removeElement(atom)
expect(model.queries.length).toBe(1)
model.removeElement(expression2)
})
it('should fire changed event when an atom changes', () => {
const nextSpy = jest.spyOn(model.changed, 'next')
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
model.addAtom(atom)
atom.changed.next(atom)
expect(nextSpy).toHaveBeenCalledWith(model)
})
it('should complete changed subject when element is removed', () => {
const expression = new CustomFieldQueryExpression()
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
;(expression.value as CustomFieldQueryElement[]).push(atom)
model.addExpression(expression)
const completeSpy = jest.spyOn(atom.changed, 'complete')
model.removeElement(atom)
expect(completeSpy).toHaveBeenCalled()
})
})
})
@@ -0,0 +1,317 @@
import {
Component,
EventEmitter,
Input,
Output,
QueryList,
ViewChild,
ViewChildren,
} from '@angular/core'
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectComponent } from '@ng-select/ng-select'
import { first, Subject, takeUntil } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import {
CUSTOM_FIELD_QUERY_MAX_ATOMS,
CUSTOM_FIELD_QUERY_MAX_DEPTH,
CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE,
CUSTOM_FIELD_QUERY_OPERATOR_LABELS,
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
CustomFieldQueryElementType,
CustomFieldQueryOperator,
CustomFieldQueryOperatorGroups,
} from 'src/app/data/custom-field-query'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import {
CustomFieldQueryAtom,
CustomFieldQueryElement,
CustomFieldQueryExpression,
} from 'src/app/utils/custom-field-query-element'
import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
export class CustomFieldQueriesModel {
public queries: CustomFieldQueryElement[] = []
public readonly changed = new Subject<CustomFieldQueriesModel>()
public clear(fireEvent = true) {
this.queries = []
if (fireEvent) {
this.changed.next(this)
}
}
public isValid(): boolean {
return (
this.queries.length > 0 &&
this.validateExpression(this.queries[0] as CustomFieldQueryExpression)
)
}
public isEmpty(): boolean {
return (
this.queries.length === 0 ||
(this.queries.length === 1 && this.queries[0].value.length === 0)
)
}
private validateAtom(atom: CustomFieldQueryAtom) {
let valid = !!(atom.field && atom.operator && atom.value !== null)
if (
[
CustomFieldQueryOperator.In.valueOf(),
CustomFieldQueryOperator.Contains.valueOf(),
].includes(atom.operator) &&
atom.value
) {
valid = valid && atom.value.length > 0
}
return valid
}
private validateExpression(expression: CustomFieldQueryExpression) {
return (
expression.operator &&
expression.value.length > 0 &&
(expression.value as CustomFieldQueryElement[]).every((e) =>
e.type === CustomFieldQueryElementType.Atom
? this.validateAtom(e as CustomFieldQueryAtom)
: this.validateExpression(e as CustomFieldQueryExpression)
)
)
}
public addAtom(atom: CustomFieldQueryAtom) {
if (this.queries.length === 0) {
this.addExpression()
}
;(this.queries[0].value as CustomFieldQueryElement[]).push(atom)
atom.changed.subscribe(() => {
if (atom.field && atom.operator && atom.value) {
this.changed.next(this)
}
})
}
public addExpression(
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
) {
if (this.queries.length > 0) {
;(
(this.queries[0] as CustomFieldQueryExpression)
.value as CustomFieldQueryElement[]
).push(expression)
} else {
this.queries.push(expression)
}
expression.changed.subscribe(() => {
this.changed.next(this)
})
}
private findElement(
queryElement: CustomFieldQueryElement,
elements: any[]
): CustomFieldQueryElement {
let foundElement
for (let i = 0; i < elements.length; i++) {
if (elements[i] === queryElement) {
foundElement = elements.splice(i, 1)[0]
} else if (elements[i].type === CustomFieldQueryElementType.Expression) {
foundElement = this.findElement(
queryElement,
elements[i].value as CustomFieldQueryElement[]
)
}
if (foundElement) {
break
}
}
return foundElement
}
public removeElement(queryElement: CustomFieldQueryElement) {
let foundComponent
for (let i = 0; i < this.queries.length; i++) {
let query = this.queries[i]
if (query === queryElement) {
foundComponent = this.queries.splice(i, 1)[0]
break
} else if (query.type === CustomFieldQueryElementType.Expression) {
foundComponent = this.findElement(queryElement, query.value as any[])
}
}
if (foundComponent) {
foundComponent.changed.complete()
if (this.isEmpty()) {
this.clear()
}
this.changed.next(this)
}
}
}
@Component({
selector: 'pngx-custom-fields-query-dropdown',
templateUrl: './custom-fields-query-dropdown.component.html',
styleUrls: ['./custom-fields-query-dropdown.component.scss'],
})
export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions {
public CustomFieldQueryComponentType = CustomFieldQueryElementType
public CustomFieldQueryOperator = CustomFieldQueryOperator
public CustomFieldDataType = CustomFieldDataType
public CUSTOM_FIELD_QUERY_MAX_DEPTH = CUSTOM_FIELD_QUERY_MAX_DEPTH
public CUSTOM_FIELD_QUERY_MAX_ATOMS = CUSTOM_FIELD_QUERY_MAX_ATOMS
public popperOptions = popperOptionsReenablePreventOverflow
@Input()
title: string
@Input()
filterPlaceholder: string = ''
@Input()
icon: string
@Input()
allowSelectNone: boolean = false
@Input()
editing = false
@Input()
applyOnClose = false
get name(): string {
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
}
@Input()
disabled: boolean = false
@ViewChild('dropdown') dropdown: NgbDropdown
@ViewChildren(NgSelectComponent) fieldSelects!: QueryList<NgSelectComponent>
private _selectionModel: CustomFieldQueriesModel
@Input()
set selectionModel(model: CustomFieldQueriesModel) {
if (this._selectionModel) {
this._selectionModel.changed.complete()
}
model.changed.subscribe(() => {
this.onModelChange()
})
this._selectionModel = model
}
get selectionModel(): CustomFieldQueriesModel {
return this._selectionModel
}
private onModelChange() {
if (this.selectionModel.isEmpty() || this.selectionModel.isValid()) {
this.selectionModelChange.next(this.selectionModel)
this.selectionModel.isEmpty() && this.dropdown?.close()
}
}
@Output()
selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
customFields: CustomField[] = []
constructor(protected customFieldsService: CustomFieldsService) {
super()
this.selectionModel = new CustomFieldQueriesModel()
this.getFields()
this.reset()
}
public onOpenChange(open: boolean) {
if (open) {
if (this.selectionModel.queries.length === 0) {
this.selectionModel.addAtom(
new CustomFieldQueryAtom([
null,
CustomFieldQueryOperator.Exists,
'true',
])
)
}
if (
this.selectionModel.queries.length === 1 &&
(
(this.selectionModel.queries[0] as CustomFieldQueryExpression)
?.value[0] as CustomFieldQueryAtom
)?.field === null
) {
setTimeout(() => {
this.fieldSelects.first?.focus()
}, 0)
}
}
}
public get isActive(): boolean {
return this.selectionModel.isValid()
}
private getFields() {
this.customFieldsService
.listAll()
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe((result) => {
this.customFields = result.results
})
}
public getCustomFieldByID(id: number): CustomField {
return this.customFields.find((field) => field.id === id)
}
public addAtom(expression: CustomFieldQueryExpression) {
expression.addAtom()
}
public addExpression(expression: CustomFieldQueryExpression) {
expression.addExpression()
}
public removeElement(element: CustomFieldQueryElement) {
this.selectionModel.removeElement(element)
}
public reset() {
this.selectionModel.clear(false)
this.selectionModel.changed.next(this.selectionModel)
}
getOperatorsForField(
fieldID: number
): Array<{ value: string; label: string }> {
const field = this.customFields.find((field) => field.id === fieldID)
const groups: CustomFieldQueryOperatorGroups[] = field
? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type]
: [CustomFieldQueryOperatorGroups.Basic]
const operators = groups.flatMap(
(group) => CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[group]
)
return operators.map((operator) => ({
value: operator,
label: CUSTOM_FIELD_QUERY_OPERATOR_LABELS[operator],
}))
}
getSelectOptionsForField(
fieldID: number
): Array<{ label: string; id: string }> {
const field = this.customFields.find((field) => field.id === fieldID)
if (field) {
return field.extra_data['select_options']
}
return []
}
}

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