Compare commits

..
Author SHA1 Message Date
shamoon 62648fffe8 Fix migration 2026-11-06 14:15:05 -08:00
shamoon b0c8ce25fd docs 2026-11-06 14:14:29 -08:00
shamoon 3c4f5d8959 Add to doc details 2026-11-06 14:14:29 -08:00
shamoon 85c52cc27f tests 2026-11-06 14:14:29 -08:00
shamoon 9b3d484e24 Bulk run workflow + dialog 2026-11-06 14:14:28 -08:00
shamoon 3a5ba814b6 frotnend use the manual filter 2026-11-06 14:14:28 -08:00
shamoon 26552f54b4 Ah, need a backend filter for wf type 2026-11-06 14:14:28 -08:00
shamoon de9d7e54bd WorkflowTriggerType.Manual 2026-11-06 14:14:27 -08:00
shamoon a5c460a567 Add some backend tests for the manual wf 2026-11-06 14:14:27 -08:00
shamoon cfca79e7fc api bulk edit run_workflow 2026-11-06 14:14:27 -08:00
shamoon 9d59f3aaa2 run_workflows task= with filter_documents option 2026-11-06 14:14:26 -08:00
shamoon 332c308f84 Add skip_matching to run_workflows 2026-11-06 14:14:26 -08:00
shamoon a0b8da7687 Add manual trigger 2026-11-06 14:14:23 -08:00
GitHub Actions 2642cefd35 Auto translate strings 2026-11-06 20:20:35 +00:00
shamoonandCodex 8aa99a381a Performance: replace django-treenode with adjacency-list tags (#14612)
---------

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-11-06 20:18:59 +00:00
Trenton H d14cfc93f4 Performance: use small vec0 chunks and larger pages for the LLM index (#14678)
The vec0 default of 1024 rows per vector chunk makes each chunk a
multi-megabyte blob. On spinning disks, indexing a document took about
2.5 seconds and re-indexing one about 13 seconds. With 32 rows per
chunk the same operations take about 35 and 180 milliseconds, with the
write-ahead log a third of the size and no cost in query speed or file
size.

Pair that with 64 KiB SQLite pages, which made cold scans about 35%
faster and warm ones about 45% faster at no write cost once the chunks
are small. The page size is set before the file enters WAL mode, so it
applies to new files.
2026-11-06 10:20:03 -08:00
shamoon 53e386ec81 Chore: avoid stale lock from failed apt process on CI (#14701) 2026-11-06 09:29:56 -08:00
shamoonandCodex 6fd91beae2 Chore: CI apt caching for flakiness (#14647)
---------

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
2026-11-06 16:36:46 +00:00
Trenton H a4543c03dc Fix: release the LLM index read lock before streaming the chat answer (#14674)
Document chat held the shared index read lock for the whole request,
including the LLM stream, which can take minutes. A compaction or
migration swap waiting for exclusive access could time out behind it
and be skipped until the next run.

Now that the answer is synthesized from the nodes already retrieved,
nothing after the vector search touches the store. Hold the lock only
while loading the index and searching, and release it before synthesis.
2026-11-06 07:03:44 -08:00
Trenton H 45a2ac4879 Fix: retrieve once per document chat question (#14673)
Document chat searched the vector store twice per question: once
explicitly to build the reference list, and again inside
RetrieverQueryEngine.query(), which retrieves before synthesizing. That
meant two query-embedding calls and two full vector searches each time.

The engine added nothing beyond the retriever call (no node
postprocessors are configured), so drop it and synthesize directly from
the nodes already retrieved. The streaming response is unchanged.
2026-11-06 07:03:43 -08:00
GitHub Actions 8bc9db816a Auto translate strings 2026-11-06 03:07:21 +00:00
shamoon c5239f9f74 Fixhancement: rework saved view widget queue processing to expected (#14699)
* activateSavedViewWithDocuments, move most of loadSavedView to a helper

* Consolidate the page size thing

* Pass widget's documents to list service fpr next / previous

* Tests for the page size + widget click

* Fix close from a dashboard widget list
2026-11-05 19:06:10 -08:00
shamoon e88303cc16 Fix: apply PDF/A conversion once for mail archives (#14698) 2026-11-05 18:03:36 -08:00
shamoon cb3513c6f3 Fix default MFA trusted AMR setting (#14693) 2026-11-04 23:46:15 +00:00
dependabot[bot] 4468a510a6 Chore(deps): Bump the actions group across 1 directory with 17 updates (#14691)
Bumps the actions group with 17 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-python](https://github.com/actions/setup-python) | `7.0.0` | `7.1.0` |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `10.3.2` | `10.3.3` |
| [docker/login-action](https://github.com/docker/login-action) | `4.6.0` | `4.7.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.1` | `7.0.2` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `8.0.1` | `8.0.2` |
| [actions/setup-node](https://github.com/actions/setup-node) | `7.0.0` | `7.1.0` |
| [j178/prek-action](https://github.com/j178/prek-action) | `3.0.0` | `3.1.0` |
| [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) | `7.7.0` | `7.10.0` |
| [shogo82148/actions-upload-release-asset](https://github.com/shogo82148/actions-upload-release-asset) | `1.10.4` | `1.10.7` |
| [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.6.4` | `0.8.0` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.38.2` | `4.39.1` |
| [stumpylog/image-cleaner-action/ephemeral](https://github.com/stumpylog/image-cleaner-action) | `0.13.0` | `0.14.0` |
| [stumpylog/image-cleaner-action/untagged](https://github.com/stumpylog/image-cleaner-action) | `0.13.0` | `0.14.0` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.2` | `4.39.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.2` | `4.39.1` |
| [crowdin/github-action](https://github.com/crowdin/github-action) | `3.3.0` | `3.5.0` |
| [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) | `7.2.0` | `7.2.1` |



Updates `actions/setup-python` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/5fda3b95a4ea91299a34e894583c3862153e4b97...6d4fd22b20465ccc0f7354476588e8df7c9ca0e0)

Updates `astral-sh/setup-uv` from 10.3.2 to 10.3.3
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/07a047e608db84b3d8c06cd9a7c7dcc9c355c18e...68559e99cc807c1eb7e9238fd43a15bba460eec2)

Updates `docker/login-action` from 4.6.0 to 4.7.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/dbcb813823bdd20940b903addbd779551569679f...81c6f93d4bc991e510b342a00b419b3bb4efc5c6)

Updates `actions/upload-artifact` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

Updates `actions/download-artifact` from 8.0.1 to 8.0.2
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c...9000827ccba6bdab643e8b6fd33ac0654aef8333)

Updates `actions/setup-node` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1)

Updates `j178/prek-action` from 3.0.0 to 3.1.0
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](https://github.com/j178/prek-action/compare/4e14d07f9231acabce116ccfca13b13dd9755ece...44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d)

Updates `release-drafter/release-drafter` from 7.7.0 to 7.10.0
- [Release notes](https://github.com/release-drafter/release-drafter/releases)
- [Commits](https://github.com/release-drafter/release-drafter/compare/34d80673e067bdc0c24568d3af899c216adcfaa9...978bb0b30c5ee8cdd332ced253758157150d43f0)

Updates `shogo82148/actions-upload-release-asset` from 1.10.4 to 1.10.7
- [Release notes](https://github.com/shogo82148/actions-upload-release-asset/releases)
- [Commits](https://github.com/shogo82148/actions-upload-release-asset/compare/aaba0f56bdbc1071f4af234d5cb16055e8a400de...73f83b114970a3485e1423ecc1fb93d887c9b096)

Updates `zizmorcore/zizmor-action` from 0.6.4 to 0.8.0
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/cc914d7f3750a2d13d75c7f184a1060aa0e9d482...53290badfa369cb0bf7ad16342cb62c2fa47ac7b)

Updates `github/codeql-action/upload-sarif` from 4.38.2 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `stumpylog/image-cleaner-action/ephemeral` from 0.13.0 to 0.14.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/21f875bab2376314e0525c614e923c669434f0e4...9796867b2a7994f0a3b6b1f2e38a6579f502c977)

Updates `stumpylog/image-cleaner-action/untagged` from 0.13.0 to 0.14.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/21f875bab2376314e0525c614e923c669434f0e4...9796867b2a7994f0a3b6b1f2e38a6579f502c977)

Updates `github/codeql-action/init` from 4.38.2 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `github/codeql-action/analyze` from 4.38.2 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

Updates `crowdin/github-action` from 3.3.0 to 3.5.0
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/9c23991700c0ec5256fd41089b9d9d7d540e424e...959dc74ac104447731cba99a016f85d0ae719788)

Updates `stefanzweifel/git-auto-commit-action` from 7.2.0 to 7.2.1
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/4a55954c782fc1ea30b9056cd3e7a2b40ca8887d...d0b01107b5db882071f96de5dd4b54961ea93f4c)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: j178/prek-action
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: release-drafter/release-drafter
  dependency-version: 7.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: shogo82148/actions-upload-release-asset
  dependency-version: 1.10.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: stumpylog/image-cleaner-action/ephemeral
  dependency-version: 0.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: stumpylog/image-cleaner-action/untagged
  dependency-version: 0.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: crowdin/github-action
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: 7.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-04 13:40:41 -08:00
dependabot[bot] 09d70ed7da docker(deps): bump astral-sh/uv (#14635)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.13.5-python3.14-trixie-slim to 0.13.7-python3.14-trixie-slim.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.13.5...0.13.7)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.13.6-python3.14-trixie-slim
  dependency-type: direct:production
  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>
2026-11-04 12:30:59 -08:00
GitHub Actions 057aee8bbd Auto translate strings 2026-11-04 19:07:00 +00:00
shamoon fa83a4c5dc Fix: check descendant tag permissions on delete (#14687) 2026-11-04 11:05:48 -08:00
shamoon 22746aa202 Fix: lock rows for first-user signup (#14688) 2026-11-04 16:33:28 +00:00
shamoon 137342c1f9 Documentation: clarify metadata exposure reporting scope 2026-11-04 07:29:41 -08:00
GitHub Actions 0ae5e2d3d4 Auto translate strings 2026-11-04 14:47:43 +00:00
shamoon b7b0de100f Fix: prevent ID badge wrapping under title on mobile (#14682) 2026-11-04 06:46:22 -08:00
Trenton H 8531d3e0c9 chore(deps): bump django-allauth to 65.21.1 (#14672)
django-allauth 65.20.1 -> 65.21.1. Security fixes:

- MFA: authentication records from an inactive user's passwordless WebAuthn login could carry over to a later login in the same session, skipping MFA
- Rate limits with multiple windows for the same scope shared a cache entry, allowing longer windows to be bypassed
- Passwordless WebAuthn registration and login now always require user verification (MFA_WEBAUTHN_USER_VERIFICATION)
- Headless: reaching the phone verification attempt limit now aborts pending flows

Released 2026-10-30, inside the usual 7 day dependency cooldown, taken deliberately for the security fixes.
2026-11-04 01:25:10 +00:00
Trenton H 05f7545a4a chore(deps): bump dev tooling (#14671)
- prek 0.5.3 -> 0.5.7
- ruff 0.16.9 -> 0.16.10
- faker 40.39.0 -> 40.43.0
- pytest-env 1.7.1 -> 1.8.1
- pytest-mock 3.15.1 -> 3.17.0
- time-machine 3.5.1 -> 3.6.0
- zensical 0.0.65 -> 0.0.73
- python-dotenv 1.2.3 -> 1.2.4 (pulled in by the above)
2026-11-04 00:53:22 +00:00
github-actions[bot]andshamoon fc29f3511f Documentation: Add v3.4.2 changelog (#14681)
* Changelog v3.4.2 - GHA

* Update changelog for paperless-ngx 3.4.2

Removed features and enhancements section for version 3.4.2.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-11-03 14:05:46 -08:00
shamoon d939d873e1 Bump version to 3.4.2 2026-11-03 13:37:33 -08:00
github-actions[bot]andCrowdin Bot f0cc7fa953 New Crowdin translations by GitHub Action (#14607)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-11-03 18:08:48 +00:00
GitHub Actions 43c73796ac Auto translate strings 2026-11-03 17:39:31 +00:00
shamoon ec47c0f11d Fix: mobile save button display obsessing (#14676) 2026-11-03 09:37:15 -08:00
Trenton H cc15aa0180 Fix: make index and llmindex init wait for tesseract language install (#14670)
* Fix: make index and llmindex init wait for tesseract language install

* Updates this out of date diagram and makes it Mermaid
2026-11-03 06:59:44 -08:00
GitHub Actions e6ea046e31 Auto translate strings 2026-11-03 06:12:15 +00:00
shamoon 04bae0af62 Fix: prevent loss of save view name on save + close (#14667) 2026-11-02 22:11:09 -08:00
GitHub Actions d2f4cfa783 Auto translate strings 2026-11-03 05:26:53 +00:00
shamoon 7cdf8ec3c2 Chore: ditch the abandoned ngx-bootstrap-icons, roll our own (#14665) 2026-11-02 21:25:35 -08:00
GitHub Actions 786eac0b22 Auto translate strings 2026-11-03 03:23:17 +00:00
shamoon 16b79fc023 Fixhancement: doc detail mobile ux tweaks (#14664) 2026-11-02 19:21:56 -08:00
Trenton H cd58cffd89 chore(deps): bump turbohtml to 1.15.3 (#14662)
turbohtml 1.10.0 -> 1.15.3. Includes sanitizer hardening (GHSA-9x48-74fj-x342, GHSA-wh2c-9vrv-w597, GHSA-pjrh-5m66-3wwf, URL schemes hidden behind character references in the bleach shim). Only the bleach clean shim and linkify are used, in the mail parser.
2026-11-02 16:15:36 -08:00
dependabot[bot] 2c82b06b93 docker-compose(deps): bump gotenberg/gotenberg in /docker/compose (#14634)
Bumps gotenberg/gotenberg from 8.37 to 8.38.

---
updated-dependencies:
- dependency-name: gotenberg/gotenberg
  dependency-version: '8.38'
  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>
2026-11-02 23:11:43 +00:00
Trenton H 5486fa7c40 chore(deps): bump Django, django-guardian, django-filter, drf-spectacular-sidecar (#14661)
- django 5.2.17 -> 5.2.18 (security release: DoS fixes in language variant lookup and header parameter parsing, plus other security fixes)
- django-guardian 3.5.0 -> 3.5.2 (GHSA-5729-mvhf-9gq4: get_objects_for_user could return objects missing required permissions; also fixes global permission handling in get_objects_for_group and get_40x_or_None)
- django-filter 26.1 -> 26.2 (LinkWidget label escaping hardening)
- drf-spectacular-sidecar 2026.9.1 -> 2026.10.1
2026-11-02 21:47:27 +00:00
Trenton H fefe4e5802 Fix: Bumps filelock to 4.2.6 to pick up ACL related fixes (#14656) 2026-11-02 08:53:17 -08:00
shamoon 936f9363a0 Fix: ensure import bulk_edit tasks in Celery workers (#14653) 2026-11-02 07:59:27 -08:00
dependabot[bot] 1b8b861a60 docker-compose(deps): bump greenmail/standalone in /docker/compose (#14631)
Bumps greenmail/standalone from 2.1.14 to 2.1.15.

---
updated-dependencies:
- dependency-name: greenmail/standalone
  dependency-version: 2.1.15
  dependency-type: direct:production
  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>
2026-11-02 03:51:52 +00:00
dependabot[bot] c2b25bdd93 docker-compose(deps): Bump nginx in /docker/compose (#14633)
Bumps nginx from 1.31.6-alpine to 1.31.7-alpine.

---
updated-dependencies:
- dependency-name: nginx
  dependency-version: 1.31.7-alpine
  dependency-type: direct:production
  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>
2026-11-02 03:37:27 +00:00
shamoon dddb1f9014 Chore: keep main and dev CI runs from canceling (#14611) 2026-11-02 00:24:42 +00:00
GitHub Actions 6ac609c64e Auto translate strings 2026-11-02 00:16:23 +00:00
dependabot[bot] 4d2c733f21 Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 13 updates (#14637)
Bumps the frontend-angular-dependencies group with 13 updates in the /src-ui directory:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `22.2.2` | `22.2.4` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `22.2.2` | `22.2.3` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `22.2.2` | `22.2.3` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `22.2.2` | `22.2.3` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `22.2.2` | `22.2.3` |
| [@angular/localize](https://github.com/angular/angular) | `22.2.2` | `22.2.3` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `22.2.2` | `22.2.3` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `22.2.2` | `22.2.3` |
| [@angular-devkit/core](https://github.com/angular/angular-cli) | `22.2.2` | `22.2.4` |
| [@angular-devkit/schematics](https://github.com/angular/angular-cli) | `22.2.2` | `22.2.4` |
| [@angular/build](https://github.com/angular/angular-cli) | `22.2.2` | `22.2.4` |
| [@angular/cli](https://github.com/angular/angular-cli) | `22.2.2` | `22.2.4` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `22.2.2` | `22.2.3` |



Updates `@angular/cdk` from 22.2.2 to 22.2.4
- [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/v22.2.2...v22.2.4)

Updates `@angular/common` from 22.2.2 to 22.2.3
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.3/packages/common)

Updates `@angular/compiler` from 22.2.2 to 22.2.3
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.3/packages/compiler)

Updates `@angular/core` from 22.2.2 to 22.2.3
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.3/packages/core)

Updates `@angular/forms` from 22.2.2 to 22.2.3
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.3/packages/forms)

Updates `@angular/localize` from 22.2.2 to 22.2.3
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/compare/v22.2.2...v22.2.3)

Updates `@angular/platform-browser` from 22.2.2 to 22.2.3
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.3/packages/platform-browser)

Updates `@angular/router` from 22.2.2 to 22.2.3
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.3/packages/router)

Updates `@angular-devkit/core` from 22.2.2 to 22.2.4
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/v22.2.4/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.2.2...v22.2.4)

Updates `@angular-devkit/schematics` from 22.2.2 to 22.2.4
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/v22.2.4/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.2.2...v22.2.4)

Updates `@angular/build` from 22.2.2 to 22.2.4
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/v22.2.4/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.2.2...v22.2.4)

Updates `@angular/cli` from 22.2.2 to 22.2.4
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/v22.2.4/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.2.2...v22.2.4)

Updates `@angular/compiler-cli` from 22.2.2 to 22.2.3
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.3/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular-devkit/core"
  dependency-version: 22.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/schematics"
  dependency-version: 22.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/build"
  dependency-version: 22.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/cdk"
  dependency-version: 22.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 22.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/common"
  dependency-version: 22.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-version: 22.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 22.2.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-version: 22.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-version: 22.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-version: 22.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-version: 22.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-version: 22.2.3
  dependency-type: direct:production
  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>
2026-11-02 00:14:45 +00:00
dependabot[bot]andshamoon 90bddfcb88 Chore(deps-dev): Bump @playwright/test from 1.62.1 to 1.64.1 in /src-ui (#14641)
* Chore(deps-dev): Bump @playwright/test from 1.62.1 to 1.64.1 in /src-ui

Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.62.1 to 1.64.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.62.1...v1.64.1)

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

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

* Bump Playwright CI image to v1.64.1 also

---------

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>
2026-11-01 23:59:06 +00:00
dependabot[bot] 0de2cb8372 Chore(deps): Bump pdfjs-dist from 6.3.289 to 6.4.299 in /src-ui (#14644)
Bumps [pdfjs-dist](https://github.com/mozilla/pdf.js) from 6.3.289 to 6.4.299.
- [Release notes](https://github.com/mozilla/pdf.js/releases)
- [Commits](https://github.com/mozilla/pdf.js/compare/v6.3.289...v6.4.299)

---
updated-dependencies:
- dependency-name: pdfjs-dist
  dependency-version: 6.4.299
  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>
2026-11-01 15:44:37 -08:00
dependabot[bot] 37168b927e Chore(deps): Bump uuid from 14.0.2 to 14.0.3 in /src-ui (#14640)
Bumps [uuid](https://github.com/uuidjs/uuid) from 14.0.2 to 14.0.3.
- [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/v14.0.2...v14.0.3)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.3
  dependency-type: direct:production
  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>
2026-11-01 22:10:22 +00:00
dependabot[bot] ef3c9f69fe Chore(deps-dev): Bump the frontend-eslint-dependencies group (#14638)
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.70.1 to 8.72.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.72.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.70.1 to 8.72.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.72.0/packages/parser)

Updates `@typescript-eslint/utils` from 8.70.1 to 8.72.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.72.0/packages/utils)

Updates `eslint` from 10.11.0 to 10.13.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.11.0...v10.13.0)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.72.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-eslint-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.72.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-eslint-dependencies
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.72.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-eslint-dependencies
- dependency-name: eslint
  dependency-version: 10.13.0
  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>
2026-11-01 21:58:49 +00:00
GitHub Actions 3586c5747b Auto translate strings 2026-11-01 21:43:29 +00:00
dependabot[bot] f8dc15c43d Chore(deps): Bump marked from 18.0.14 to 18.1.1 in /src-ui (#14643)
Bumps [marked](https://github.com/markedjs/marked) from 18.0.14 to 18.1.1.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v18.0.14...v18.1.1)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 18.1.1
  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>
2026-11-01 21:42:04 +00:00
dependabot[bot] bfb8b9076b Chore(deps-dev): Bump prettier from 3.9.9 to 3.9.10 in /src-ui (#14645)
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.9 to 3.9.10.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.9...3.9.10)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.10
  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>
2026-11-01 21:23:09 +00:00
dependabot[bot] cef246a761 Chore(deps-dev): Bump @types/node from 26.6.2 to 26.6.5 in /src-ui (#14639)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.6.2 to 26.6.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.6.5
  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>
2026-11-01 13:07:02 -08:00
shamoon 5b63c9a0ff Fixhancement: add tag names to document history list (#14625) 2026-11-01 05:39:52 -08:00
shamoon 217c5e681d Fix: add button type to zoom buttons to prevent form submission (#14624) 2026-11-01 05:35:38 -08:00
GitHub Actions 45e99cde5a Auto translate strings 2026-10-31 20:44:16 +00:00
shamoon 7ea58eaf46 Fix: move document list preview button to own column (#14610) 2026-10-31 13:42:56 -07:00
GitHub Actions a19804a0b7 Auto translate strings 2026-10-31 16:34:17 +00:00
Trenton Handshamoon 3289be7383 Fix: Various small bugs found during typing updates (#14593)
* Fix: match Apple Mail tag colors case-insensitively

* Fix: reject OAuth callbacks with an unrecognized scope

* Fix: refuse to refresh OAuth tokens for non-OAuth accounts or missing refresh tokens

* Fix: parse datetime strings in the format_datetime filter and reject invalid ones

* Fix: reject a PAPERLESS_URL without a hostname

* Fix: report a database error in the system status instead of failing with a 500

* Fix: surface the database error when refreshing a document before moving its files

* Fix: report no unapplied migrations when the applied migrations cannot be read

* Fix: return the parsed value directly in format_datetime for typing

* Chore: annotate the paperless_dirs fixture in the refresh failure test

* Allow null for the latest_migration system status type

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-10-31 09:32:58 -07:00
GitHub Actions d98918dd97 Auto translate strings 2026-10-31 06:13:00 +00:00
shamoon 34c88613f6 Fix: fix custom fields dropdown text size (#14606) 2026-10-30 23:11:45 -07:00
github-actions[bot] 13acdabb47 Changelog v3.4.1 - GHA (#14605)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-30 22:44:05 -07:00
shamoon b67cfdf3fd Bump version to 3.4.1 2026-10-30 22:20:31 -07:00
github-actions[bot]andCrowdin Bot 98d2ddd1c6 New Crowdin translations by GitHub Action (#14584)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-10-31 05:12:07 +00:00
GitHub Actions 91b35eada6 Auto translate strings 2026-10-31 04:48:33 +00:00
shamoon 11248f776d Fix: fix the id__none schema in the right place (#14604) 2026-10-30 21:47:12 -07:00
shamoon 331fa13088 Fix: drop redundant change perms checks on cards open button (#14601) 2026-10-30 16:16:48 -07:00
shamoon ebddd5687c Chore: fix a couple e2e warnings / flakiness (#14596) 2026-10-30 15:45:46 -07:00
Trenton H 2925b8de81 Chore: Skip repeating Django system checks in the Docker consumer, scheduler and worker (#14598)
The init-system-checks step already runs them once and all three services
depend on init-complete. Measured idle anon PSS saved on
Python 3.14: consumer 14 MiB, beat 26 MiB, worker 27 MiB.
2026-10-30 22:37:37 +00:00
GitHub Actions 5e397e1222 Auto translate strings 2026-10-30 22:26:54 +00:00
Trenton H 6f99ad9469 Chore: Improve idle memory via more lazy imports (#14599)
* Chore: Import dateparser lazily so processes that never parse dates skip it

* Chore: Import turbohtml lazily in the mail parser

* Chore: Import lxml lazily in the SVG validator

* Chore: Import paperless_ai.indexing lazily in document tasks

* Chore: Import paperless_ai helpers lazily in views

* Chore: Read the OCRmyPDF sRGB profile on first use instead of at import

* Chore: Import pikepdf and img2pdf lazily so startup does not load them

* Chore: Reuse the initialized Django ASGI application for HTTP routing
2026-10-30 22:25:42 +00:00
shamoon a5d3247ca7 Tweakhancement: make new pdf controls mostly hidden (#14595) 2026-10-30 10:22:50 -07:00
GitHub Actions 8a67481a9d Auto translate strings 2026-10-30 16:59:59 +00:00
shamoon 652f964f5d Tweak: move preview button in doc list (#14594) 2026-10-30 09:58:29 -07:00
shamoon 80a1827fa0 Fix: checksum light mode display and cf dropdown mobile position (#14590) 2026-10-30 07:03:08 -07:00
456 changed files with 16611 additions and 13723 deletions

No files matched your search

+34 -34
View File
@@ -1,53 +1,53 @@
name: Install apt packages name: Install apt packages
description: > description: >
Installs apt packages with a per-attempt timeout and retries. The hosted runner's Azure apt mirror intermittently serves archives at a crawl, which apt's own timeout does not catch. A stuck attempt is killed and retried, which usually lands on a healthy mirror node. Installs apt packages with bounded download retries and Ubuntu mirror fallback. Caches downloaded packages as an optimization; apt resolves dependencies on every run.
inputs: inputs:
packages: packages:
description: Space separated list of packages to install description: Space separated list of package names to install
required: true required: true
attempts: attempts:
description: Maximum number of attempts description: Maximum number of download attempts
default: "3" default: "3"
attempt-timeout: attempt-timeout:
description: Seconds before a single attempt is killed description: Seconds before a single download attempt is killed
default: "180" default: "180"
runs: runs:
using: composite using: composite
steps: steps:
- name: Identify apt cache
id: cache-key
shell: bash
env:
PACKAGES: ${{ inputs.packages }}
run: |
. /etc/os-release
packages_hash=$(printf '%s' "$PACKAGES" | sha256sum | cut -d ' ' -f 1)
# Cached downloads are optional: apt fetches any missing/newer packages.
# Refresh weekly, including inside job containers without ImageVersion.
echo "key=apt-v2-${ID}-${VERSION_ID}-$(dpkg --print-architecture)-$(date -u +%G-%V)-${packages_hash}" >> "$GITHUB_OUTPUT"
- name: Restore apt packages
id: cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
${{ runner.temp }}/apt-packages/*.deb
${{ runner.temp }}/apt-packages/.keep
key: ${{ steps.cache-key.outputs.key }}
- name: Install ${{ inputs.packages }} - name: Install ${{ inputs.packages }}
shell: bash shell: bash
env: env:
PACKAGES: ${{ inputs.packages }} PACKAGES: ${{ inputs.packages }}
ATTEMPTS: ${{ inputs.attempts }} ATTEMPTS: ${{ inputs.attempts }}
ATTEMPT_TIMEOUT: ${{ inputs.attempt-timeout }} ATTEMPT_TIMEOUT: ${{ inputs.attempt-timeout }}
run: | CACHE_DIR: ${{ runner.temp }}/apt-packages
SUDO="" run: bash "$GITHUB_ACTION_PATH/install.sh"
if [ "$(id -u)" -ne 0 ]; then # Save immediately: a later test failure must not discard downloaded packages.
SUDO="sudo" - name: Save apt packages
fi if: steps.cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
attempt_install() { with:
$SUDO apt-get update -q path: |
# Intentionally unquoted, this is a list of package names ${{ runner.temp }}/apt-packages/*.deb
$SUDO apt-get install -q --yes --no-install-recommends $PACKAGES ${{ runner.temp }}/apt-packages/.keep
} key: ${{ steps.cache-key.outputs.key }}
export -f attempt_install
export SUDO PACKAGES
for attempt in $(seq 1 "${ATTEMPTS}"); do
echo "::group::apt install attempt ${attempt}/${ATTEMPTS}"
start=$(date +%s)
if timeout --kill-after=10 "${ATTEMPT_TIMEOUT}" bash -c attempt_install; then
echo "::endgroup::"
echo "Installed in $(( $(date +%s) - start ))s"
exit 0
fi
echo "::endgroup::"
echo "::warning::apt install attempt ${attempt}/${ATTEMPTS} failed or exceeded ${ATTEMPT_TIMEOUT}s"
# A killed attempt can leave dpkg half configured
$SUDO dpkg --configure -a || true
done
echo "::error::apt install failed after ${ATTEMPTS} attempts"
exit 1
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
# Run as root in the timed child process. Only downloads may be interrupted.
if [ "${1:-}" = download ]; then
download_archive_dir=$2
shift 2
apt-get update -q
# Keep apt itself under timeout supervision, including the kill grace period.
exec apt-get install -q --yes --no-install-recommends --download-only \
-o "Dir::Cache::archives=$download_archive_dir" "$@"
fi
SUDO=()
if [ "$(id -u)" -ne 0 ]; then
SUDO=(sudo)
fi
read -r -a requested_packages <<< "$PACKAGES"
mkdir -p "$CACHE_DIR"
touch "$CACHE_DIR/.keep"
use_ubuntu_mirrors() {
# Hosted runners use a mirror list; job containers can point directly at Azure.
# Handle both layouts, leaving other repositories and signature checks intact.
if [ -f /etc/apt/apt-mirrors.txt ]; then
"${SUDO[@]}" sed -i '\|azure\.archive\.ubuntu\.com|d' /etc/apt/apt-mirrors.txt
fi
for source in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
if [ -f "$source" ]; then
"${SUDO[@]}" sed -i 's|azure\.archive\.ubuntu\.com|archive.ubuntu.com|g' "$source"
fi
done
}
for attempt in $(seq 1 "$ATTEMPTS"); do
echo "::group::apt download attempt ${attempt}/${ATTEMPTS}"
# Run timeout as root so it can signal the root-owned apt processes.
if "${SUDO[@]}" timeout --kill-after=10 "$ATTEMPT_TIMEOUT" bash "$0" download "$CACHE_DIR" "${requested_packages[@]}"; then
echo "::endgroup::"
# Apt handles aliases (e.g. libmagic1 -> libmagic1t64) and dependencies.
"${SUDO[@]}" apt-get install -q --yes --no-install-recommends --no-download \
-o "Dir::Cache::archives=$CACHE_DIR" "${requested_packages[@]}"
# Allow the unprivileged cache action to read archives downloaded as root.
find "$CACHE_DIR" -maxdepth 1 -name '*.deb' -exec "${SUDO[@]}" chmod a+r {} +
exit 0
fi
echo "::endgroup::"
echo "::warning::apt download attempt ${attempt}/${ATTEMPTS} failed or exceeded ${ATTEMPT_TIMEOUT}s"
if [ "$attempt" -eq 1 ] && [ "$ATTEMPTS" -gt 1 ]; then
echo "Switching from Azure to Ubuntu archive mirrors for remaining attempts"
use_ubuntu_mirrors
fi
done
echo "::error::apt download failed after ${ATTEMPTS} attempts"
exit 1
+7 -5
View File
@@ -8,7 +8,8 @@ on:
- 'translations**' - 'translations**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: backend-${{ github.event.pull_request.number || github.ref }} # Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: backend-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
cancel-in-progress: true cancel-in-progress: true
env: env:
DEFAULT_UV_VERSION: "0.13.x" DEFAULT_UV_VERSION: "0.13.x"
@@ -76,6 +77,7 @@ jobs:
- 'uv.lock' - 'uv.lock'
- 'docker/compose/docker-compose.ci-test.yml' - 'docker/compose/docker-compose.ci-test.yml'
- '.github/workflows/ci-backend.yml' - '.github/workflows/ci-backend.yml'
- '.github/actions/apt-install/**'
test: test:
needs: changes needs: changes
if: needs.changes.outputs.backend_changed == 'true' if: needs.changes.outputs.backend_changed == 'true'
@@ -98,11 +100,11 @@ jobs:
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
@@ -168,11 +170,11 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: "${{ env.DEFAULT_PYTHON }}" python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+8 -7
View File
@@ -13,7 +13,8 @@ on:
- main - main
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: docker-${{ github.event.pull_request.number || github.ref }} # Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: docker-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
cancel-in-progress: true cancel-in-progress: true
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
@@ -108,7 +109,7 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -154,7 +155,7 @@ jobs:
echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt" echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt"
- name: Upload digest - name: Upload digest
if: steps.check-push.outputs.should-push == 'true' if: steps.check-push.outputs.should-push == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with: with:
name: digests-${{ matrix.arch }} name: digests-${{ matrix.arch }}
path: /tmp/digests/digest-${{ matrix.arch }}.txt path: /tmp/digests/digest-${{ matrix.arch }}.txt
@@ -172,7 +173,7 @@ jobs:
packages: write packages: write
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digest-*.txt pattern: digest-*.txt
@@ -184,20 +185,20 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub - name: Login to Docker Hub
if: needs.build-arch.outputs.push-external == 'true' if: needs.build-arch.outputs.push-external == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Quay.io - name: Login to Quay.io
if: needs.build-arch.outputs.push-external == 'true' if: needs.build-arch.outputs.push-external == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.QUAY_USERNAME }} username: ${{ secrets.QUAY_USERNAME }}
+4 -3
View File
@@ -6,7 +6,8 @@ on:
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: docs-${{ github.event.pull_request.number || github.ref }} # Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: docs-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: permissions:
contents: read contents: read
@@ -74,11 +75,11 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+11 -9
View File
@@ -8,7 +8,8 @@ on:
- 'translations**' - 'translations**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: frontend-${{ github.event.pull_request.number || github.ref }} # Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: frontend-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: {} permissions: {}
jobs: jobs:
@@ -68,6 +69,7 @@ jobs:
frontend: frontend:
- 'src-ui/**' - 'src-ui/**'
- '.github/workflows/ci-frontend.yml' - '.github/workflows/ci-frontend.yml'
- '.github/actions/apt-install/**'
install-dependencies: install-dependencies:
needs: changes needs: changes
if: needs.changes.outputs.frontend_changed == 'true' if: needs.changes.outputs.frontend_changed == 'true'
@@ -85,7 +87,7 @@ jobs:
with: with:
package_json_file: src-ui/package.json package_json_file: src-ui/package.json
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
@@ -117,7 +119,7 @@ jobs:
with: with:
package_json_file: src-ui/package.json package_json_file: src-ui/package.json
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
@@ -156,7 +158,7 @@ jobs:
with: with:
package_json_file: src-ui/package.json package_json_file: src-ui/package.json
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
@@ -191,7 +193,7 @@ jobs:
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
container: mcr.microsoft.com/playwright:v1.62.1-resolute container: mcr.microsoft.com/playwright:v1.64.1-resolute
env: env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
@@ -205,18 +207,18 @@ jobs:
with: with:
package_json_file: src-ui/package.json package_json_file: src-ui/package.json
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml' cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: '3.12' python-version: '3.12'
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with: with:
version: '0.13.x' version: '0.13.x'
enable-cache: false enable-cache: false
@@ -260,7 +262,7 @@ jobs:
with: with:
package_json_file: src-ui/package.json package_json_file: src-ui/package.json
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
+4 -3
View File
@@ -7,7 +7,8 @@ on:
branches-ignore: branches-ignore:
- 'translations**' - 'translations**'
concurrency: concurrency:
group: lint-${{ github.event.pull_request.number || github.ref }} # Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: lint-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: permissions:
contents: read contents: read
@@ -21,8 +22,8 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Install Python - name: Install Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: "3.14" python-version: "3.14"
- name: Run prek - name: Run prek
uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 uses: j178/prek-action@44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d # v3.1.0
+9 -9
View File
@@ -44,7 +44,7 @@ jobs:
with: with:
package_json_file: src-ui/package.json package_json_file: src-ui/package.json
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: with:
node-version: 24.x node-version: 24.x
package-manager-cache: false package-manager-cache: false
@@ -55,11 +55,11 @@ jobs:
# ---- Backend Setup ---- # ---- Backend Setup ----
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
@@ -137,7 +137,7 @@ jobs:
sudo chown -R 1000:1000 paperless-ngx/ sudo chown -R 1000:1000 paperless-ngx/
tar -cJf paperless-ngx.tar.xz paperless-ngx/ tar -cJf paperless-ngx.tar.xz paperless-ngx/
- name: Upload release artifact - name: Upload release artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with: with:
name: release name: release
path: dist/paperless-ngx.tar.xz path: dist/paperless-ngx.tar.xz
@@ -155,7 +155,7 @@ jobs:
version: ${{ steps.get-version.outputs.version }} version: ${{ steps.get-version.outputs.version }}
steps: steps:
- name: Download release artifact - name: Download release artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with: with:
name: release name: release
path: ./ path: ./
@@ -172,7 +172,7 @@ jobs:
fi fi
- name: Create release and changelog - name: Create release and changelog
id: create-release id: create-release
uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0 uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0
with: with:
name: Paperless-ngx ${{ steps.get-version.outputs.version }} name: Paperless-ngx ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.version }} tag: ${{ steps.get-version.outputs.version }}
@@ -183,7 +183,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release archive - name: Upload release archive
uses: shogo82148/actions-upload-release-asset@aaba0f56bdbc1071f4af234d5cb16055e8a400de # v1.10.4 uses: shogo82148/actions-upload-release-asset@73f83b114970a3485e1423ecc1fb93d887c9b096 # v1.10.7
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
upload_url: ${{ steps.create-release.outputs.upload_url }} upload_url: ${{ steps.create-release.outputs.upload_url }}
@@ -209,11 +209,11 @@ jobs:
persist-credentials: true # for pushing changelog branch persist-credentials: true # for pushing changelog branch
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
+4 -3
View File
@@ -8,7 +8,8 @@ on:
- 'translations**' - 'translations**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
group: static-analysis-${{ github.event.pull_request.number || github.ref }} # Let dev/main commits finish, PR and feature-branch runs supersede older ones
group: static-analysis-${{ github.event.pull_request.number || ((github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') && github.run_id) || github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions: permissions:
contents: read contents: read
@@ -26,7 +27,7 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
semgrep: semgrep:
name: Semgrep CE name: Semgrep CE
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04
@@ -44,7 +45,7 @@ jobs:
- name: Run Semgrep - name: Run Semgrep
run: semgrep scan --config auto --sarif-output results.sarif run: semgrep scan --config auto --sarif-output results.sarif
- name: Upload results to GitHub code scanning - name: Upload results to GitHub code scanning
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
if: always() if: always()
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Clean temporary images - name: Clean temporary images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0 uses: stumpylog/image-cleaner-action/ephemeral@9796867b2a7994f0a3b6b1f2e38a6579f502c977 # v0.14.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
@@ -56,7 +56,7 @@ jobs:
steps: steps:
- name: Clean untagged images - name: Clean untagged images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0 uses: stumpylog/image-cleaner-action/untagged@9796867b2a7994f0a3b6b1f2e38a6579f502c977 # v0.14.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
persist-credentials: false persist-credentials: false
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -47,4 +47,4 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
token: ${{ secrets.PNGX_BOT_PAT }} token: ${{ secrets.PNGX_BOT_PAT }}
persist-credentials: false persist-credentials: false
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0 uses: crowdin/github-action@959dc74ac104447731cba99a016f85d0ae719788 # v3.5.0
with: with:
upload_translations: false upload_translations: false
download_translations: true download_translations: true
+1 -1
View File
@@ -19,6 +19,6 @@ jobs:
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot' if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
steps: steps:
- name: Label PR with release-drafter - name: Label PR with release-drafter
uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0 uses: release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0 # v7.10.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+4 -4
View File
@@ -23,14 +23,14 @@ jobs:
persist-credentials: true # for pushing translation branch persist-credentials: true # for pushing translation branch
- name: Set up Python - name: Set up Python
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
- name: Install system dependencies - name: Install system dependencies
timeout-minutes: 12 timeout-minutes: 12
uses: $/.github/actions/apt-install uses: $/.github/actions/apt-install
with: with:
packages: gettext packages: gettext
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
@@ -48,7 +48,7 @@ jobs:
with: with:
package_json_file: src-ui/package.json package_json_file: src-ui/package.json
- name: Use Node.js 24 - name: Use Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with: with:
node-version: 24.x node-version: 24.x
cache: 'pnpm' cache: 'pnpm'
@@ -68,7 +68,7 @@ jobs:
cd src-ui cd src-ui
pnpm run ng extract-i18n pnpm run ng extract-i18n
- name: Commit changes - name: Commit changes
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0 uses: stefanzweifel/git-auto-commit-action@d0b01107b5db882071f96de5dd4b54961ea93f4c # v7.2.1
with: with:
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po' file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
commit_message: "Auto translate strings" commit_message: "Auto translate strings"
+1 -1
View File
@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs # Purpose: Installs s6-overlay and rootfs
# Comments: # Comments:
# - Don't leave anything extra in here either # - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.13.5-python3.14-trixie-slim@sha256:d8e732839dfc4225a1ff698803b478b2223c05636c055d3add41ab9dbec0f315 AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.13.7-python3.14-trixie-slim@sha256:e80c05a310400cfc90af00b5b7271ef9ad24816e3265e1162af8a678e1aad099 AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6
+1 -1
View File
@@ -66,7 +66,7 @@ The following are not generally considered vulnerabilities unless accompanied by
- optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability - optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability
- missing limits or hardening settings presented without concrete impact - missing limits or hardening settings presented without concrete impact
- generic AI or static-analysis output that is not confirmed against the current codebase and a real deployment scenario - generic AI or static-analysis output that is not confirmed against the current codebase and a real deployment scenario
- metadata names visible in a document's custom storage path, even when the user cannot access the underlying metadata object; this is expected behavior - metadata names or normalized words visible in a document's custom storage path or through search/autocomplete for documents the user can access, even when the user cannot access the underlying metadata object; this is expected behavior
- the ability to attach objects that a user cannot access to a document by ID is an intentional design choice, and not considered a vulnerability - the ability to attach objects that a user cannot access to a document by ID is an intentional design choice, and not considered a vulnerability
## Transparency ## Transparency
+3 -3
View File
@@ -4,7 +4,7 @@
# correct networking for the tests # correct networking for the tests
services: services:
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.37 image: docker.io/gotenberg/gotenberg:8.38
hostname: gotenberg hostname: gotenberg
container_name: gotenberg container_name: gotenberg
network_mode: host network_mode: host
@@ -24,7 +24,7 @@ services:
network_mode: host network_mode: host
restart: unless-stopped restart: unless-stopped
greenmail: greenmail:
image: docker.io/greenmail/standalone:2.1.14 image: docker.io/greenmail/standalone:2.1.15
hostname: greenmail hostname: greenmail
container_name: greenmail container_name: greenmail
environment: environment:
@@ -35,7 +35,7 @@ services:
- "3143:3143" # IMAP - "3143:3143" # IMAP
restart: unless-stopped restart: unless-stopped
nginx: nginx:
image: docker.io/nginx:1.31.6-alpine image: docker.io/nginx:1.31.7-alpine
hostname: nginx hostname: nginx
container_name: nginx container_name: nginx
ports: ports:
@@ -71,7 +71,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.37 image: docker.io/gotenberg/gotenberg:8.38
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript. # want to allow external content like tracking pixels or even javascript.
@@ -67,7 +67,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.37 image: docker.io/gotenberg/gotenberg:8.38
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript. # want to allow external content like tracking pixels or even javascript.
@@ -56,7 +56,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.37 image: docker.io/gotenberg/gotenberg:8.38
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript. # want to allow external content like tracking pixels or even javascript.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

+57
View File
@@ -0,0 +1,57 @@
# Container init flow
Startup order of the s6-rc oneshot services under `docker/rootfs/etc/s6-overlay/s6-rc.d/`. An arrow `A --> B` means B lists A in its `dependencies.d/`, so B waits for A. Redundant (transitive) edges are omitted.
Each labelled box is a group of services that can run in parallel: every service in it starts as soon as its own dependencies finish, so services in the same box run in parallel. A service does not wait for the rest of its group, only for the arrows pointing at it. Long-running services such as `init-tesseract-langs` (package installs) overlap with several later steps.
```mermaid
flowchart TD
subgraph w3 ["Run in parallel"]
init-tesseract-langs
init-modify-user
init-wait-for-db
init-wait-for-redis
end
subgraph w4 ["Run in parallel"]
init-folders
init-compile-bytecode
end
subgraph w6 ["Run in parallel"]
init-search-index
init-llmindex-migrate
init-superuser
end
base --> init-start --> init-env-file
init-env-file --> init-tesseract-langs
init-env-file --> init-modify-user
init-env-file --> init-wait-for-db
init-env-file --> init-wait-for-redis
init-start --> init-compile-bytecode
init-modify-user --> init-folders
init-modify-user --> init-compile-bytecode
init-folders --> init-migrations
init-compile-bytecode --> init-migrations
init-wait-for-db --> init-migrations
init-migrations --> init-search-index
init-migrations --> init-llmindex-migrate
init-migrations --> init-superuser
init-tesseract-langs --> init-search-index
init-tesseract-langs --> init-llmindex-migrate
init-tesseract-langs --> init-system-checks
init-superuser --> init-system-checks
init-search-index --> init-custom-init
init-system-checks --> init-custom-init
init-wait-for-redis --> init-custom-init
init-custom-init --> init-complete
init-llmindex-migrate --> init-complete
```
`init-search-index` and `init-llmindex-migrate` run `manage.py`, which runs the Django system checks, including the check that the configured OCR languages are installed, so they wait for `init-tesseract-langs`.
@@ -39,7 +39,6 @@ declare -a scope=(
"${site_packages}/allauth" "${site_packages}/allauth"
"${site_packages}/drf_spectacular" "${site_packages}/drf_spectacular"
"${site_packages}/drf_spectacular_sidecar" "${site_packages}/drf_spectacular_sidecar"
"${site_packages}/treenode"
"${site_packages}/compression_middleware" "${site_packages}/compression_middleware"
) )
@@ -10,9 +10,11 @@ if [[ -n "${PAPERLESS_CONSUMER_DISABLE}" ]]; then
else else
cd ${PAPERLESS_SRC_DIR} cd ${PAPERLESS_SRC_DIR}
# Django system checks already ran once in init-system-checks, skip repeating them
if [[ -n "${USER_IS_NON_ROOT}" ]]; then if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec python3 manage.py document_consumer exec python3 manage.py document_consumer --skip-checks
else else
exec s6-setuidgid paperless python3 manage.py document_consumer exec s6-setuidgid paperless python3 manage.py document_consumer --skip-checks
fi fi
fi fi
@@ -3,8 +3,9 @@
cd ${PAPERLESS_SRC_DIR} cd ${PAPERLESS_SRC_DIR}
# Django system checks already ran once in init-system-checks, skip repeating them
if [[ -n "${USER_IS_NON_ROOT}" ]]; then if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec /usr/local/bin/celery --app paperless beat --loglevel INFO exec /usr/local/bin/celery --skip-checks --app paperless beat --loglevel INFO
else else
exec s6-setuidgid paperless /usr/local/bin/celery --app paperless beat --loglevel INFO exec s6-setuidgid paperless /usr/local/bin/celery --skip-checks --app paperless beat --loglevel INFO
fi fi
@@ -3,8 +3,9 @@
cd ${PAPERLESS_SRC_DIR} cd ${PAPERLESS_SRC_DIR}
# Django system checks already ran once in init-system-checks, skip repeating them
if [[ -n "${USER_IS_NON_ROOT}" ]]; then if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip exec /usr/local/bin/celery --skip-checks --app paperless worker --loglevel INFO --without-mingle --without-gossip
else else
exec s6-setuidgid paperless /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip exec s6-setuidgid paperless /usr/local/bin/celery --skip-checks --app paperless worker --loglevel INFO --without-mingle --without-gossip
fi fi
+91
View File
@@ -1,5 +1,96 @@
# Changelog # Changelog
## paperless-ngx 3.4.2
### Bug Fixes
- Fix: mobile save button display obsessing [@shamoon](https://github.com/shamoon) ([#14676](https://github.com/paperless-ngx/paperless-ngx/pull/14676))
- Fix: make index and llmindex init wait for tesseract language install [@stumpylog](https://github.com/stumpylog) ([#14670](https://github.com/paperless-ngx/paperless-ngx/pull/14670))
- Fix: prevent loss of save view name on save + close [@shamoon](https://github.com/shamoon) ([#14667](https://github.com/paperless-ngx/paperless-ngx/pull/14667))
- Fixhancement: doc detail mobile ux tweaks [@shamoon](https://github.com/shamoon) ([#14664](https://github.com/paperless-ngx/paperless-ngx/pull/14664))
- Fix: Bumps filelock to 4.2.6 to pick up ACL related fixes [@stumpylog](https://github.com/stumpylog) ([#14656](https://github.com/paperless-ngx/paperless-ngx/pull/14656))
- Fix: ensure import bulk\_edit tasks in Celery workers [@shamoon](https://github.com/shamoon) ([#14653](https://github.com/paperless-ngx/paperless-ngx/pull/14653))
- Fixhancement: add tag names to document history list [@shamoon](https://github.com/shamoon) ([#14625](https://github.com/paperless-ngx/paperless-ngx/pull/14625))
- Fix: add button type to zoom buttons to prevent form submission [@shamoon](https://github.com/shamoon) ([#14624](https://github.com/paperless-ngx/paperless-ngx/pull/14624))
- Fix: move document list preview button to own column [@shamoon](https://github.com/shamoon) ([#14610](https://github.com/paperless-ngx/paperless-ngx/pull/14610))
- Fix: Various small bugs found during typing updates [@stumpylog](https://github.com/stumpylog) ([#14593](https://github.com/paperless-ngx/paperless-ngx/pull/14593))
- Fix: fix custom fields dropdown text size [@shamoon](https://github.com/shamoon) ([#14606](https://github.com/paperless-ngx/paperless-ngx/pull/14606))
### Dependencies
<details>
<summary>14 changes</summary>
- Chore: ditch the abandoned ngx-bootstrap-icons, roll our own [@shamoon](https://github.com/shamoon) ([#14665](https://github.com/paperless-ngx/paperless-ngx/pull/14665))
- chore(deps): bump turbohtml to 1.15.3 [@stumpylog](https://github.com/stumpylog) ([#14662](https://github.com/paperless-ngx/paperless-ngx/pull/14662))
- docker-compose(deps): bump gotenberg/gotenberg from 8.37 to 8.38 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14634](https://github.com/paperless-ngx/paperless-ngx/pull/14634))
- chore(deps): bump Django, django-guardian, django-filter, drf-spectacular-sidecar [@stumpylog](https://github.com/stumpylog) ([#14661](https://github.com/paperless-ngx/paperless-ngx/pull/14661))
- docker-compose(deps): bump greenmail/standalone from 2.1.14 to 2.1.15 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14631](https://github.com/paperless-ngx/paperless-ngx/pull/14631))
- docker-compose(deps): Bump nginx from 1.31.6-alpine to 1.31.7-alpine in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14633](https://github.com/paperless-ngx/paperless-ngx/pull/14633))
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 13 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14637](https://github.com/paperless-ngx/paperless-ngx/pull/14637))
- Chore(deps-dev): Bump @playwright/test from 1.62.1 to 1.64.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14641](https://github.com/paperless-ngx/paperless-ngx/pull/14641))
- Chore(deps): Bump pdfjs-dist from 6.3.289 to 6.4.299 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14644](https://github.com/paperless-ngx/paperless-ngx/pull/14644))
- Chore(deps): Bump uuid from 14.0.2 to 14.0.3 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14640](https://github.com/paperless-ngx/paperless-ngx/pull/14640))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14638](https://github.com/paperless-ngx/paperless-ngx/pull/14638))
- Chore(deps): Bump marked from 18.0.14 to 18.1.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14643](https://github.com/paperless-ngx/paperless-ngx/pull/14643))
- Chore(deps-dev): Bump prettier from 3.9.9 to 3.9.10 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14645](https://github.com/paperless-ngx/paperless-ngx/pull/14645))
- Chore(deps-dev): Bump @types/node from 26.6.2 to 26.6.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14639](https://github.com/paperless-ngx/paperless-ngx/pull/14639))
</details>
### All App Changes
<details>
<summary>21 changes</summary>
- Fix: mobile save button display obsessing [@shamoon](https://github.com/shamoon) ([#14676](https://github.com/paperless-ngx/paperless-ngx/pull/14676))
- Fix: prevent loss of save view name on save + close [@shamoon](https://github.com/shamoon) ([#14667](https://github.com/paperless-ngx/paperless-ngx/pull/14667))
- Chore: ditch the abandoned ngx-bootstrap-icons, roll our own [@shamoon](https://github.com/shamoon) ([#14665](https://github.com/paperless-ngx/paperless-ngx/pull/14665))
- Fixhancement: doc detail mobile ux tweaks [@shamoon](https://github.com/shamoon) ([#14664](https://github.com/paperless-ngx/paperless-ngx/pull/14664))
- chore(deps): bump turbohtml to 1.15.3 [@stumpylog](https://github.com/stumpylog) ([#14662](https://github.com/paperless-ngx/paperless-ngx/pull/14662))
- chore(deps): bump Django, django-guardian, django-filter, drf-spectacular-sidecar [@stumpylog](https://github.com/stumpylog) ([#14661](https://github.com/paperless-ngx/paperless-ngx/pull/14661))
- Fix: Bumps filelock to 4.2.6 to pick up ACL related fixes [@stumpylog](https://github.com/stumpylog) ([#14656](https://github.com/paperless-ngx/paperless-ngx/pull/14656))
- Fix: ensure import bulk\_edit tasks in Celery workers [@shamoon](https://github.com/shamoon) ([#14653](https://github.com/paperless-ngx/paperless-ngx/pull/14653))
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 13 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14637](https://github.com/paperless-ngx/paperless-ngx/pull/14637))
- Chore(deps-dev): Bump @playwright/test from 1.62.1 to 1.64.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14641](https://github.com/paperless-ngx/paperless-ngx/pull/14641))
- Chore(deps): Bump pdfjs-dist from 6.3.289 to 6.4.299 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14644](https://github.com/paperless-ngx/paperless-ngx/pull/14644))
- Chore(deps): Bump uuid from 14.0.2 to 14.0.3 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14640](https://github.com/paperless-ngx/paperless-ngx/pull/14640))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14638](https://github.com/paperless-ngx/paperless-ngx/pull/14638))
- Chore(deps): Bump marked from 18.0.14 to 18.1.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14643](https://github.com/paperless-ngx/paperless-ngx/pull/14643))
- Chore(deps-dev): Bump prettier from 3.9.9 to 3.9.10 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14645](https://github.com/paperless-ngx/paperless-ngx/pull/14645))
- Chore(deps-dev): Bump @types/node from 26.6.2 to 26.6.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14639](https://github.com/paperless-ngx/paperless-ngx/pull/14639))
- Fixhancement: add tag names to document history list [@shamoon](https://github.com/shamoon) ([#14625](https://github.com/paperless-ngx/paperless-ngx/pull/14625))
- Fix: add button type to zoom buttons to prevent form submission [@shamoon](https://github.com/shamoon) ([#14624](https://github.com/paperless-ngx/paperless-ngx/pull/14624))
- Fix: move document list preview button to own column [@shamoon](https://github.com/shamoon) ([#14610](https://github.com/paperless-ngx/paperless-ngx/pull/14610))
- Fix: Various small bugs found during typing updates [@stumpylog](https://github.com/stumpylog) ([#14593](https://github.com/paperless-ngx/paperless-ngx/pull/14593))
- Fix: fix custom fields dropdown text size [@shamoon](https://github.com/shamoon) ([#14606](https://github.com/paperless-ngx/paperless-ngx/pull/14606))
</details>
## paperless-ngx 3.4.1
### Bug Fixes
- Fix the id\_\_ schemae in the right place [@shamoon](https://github.com/shamoon) ([#14604](https://github.com/paperless-ngx/paperless-ngx/pull/14604))
- Fix: drop redundant change perms checks on cards open button [@shamoon](https://github.com/shamoon) ([#14601](https://github.com/paperless-ngx/paperless-ngx/pull/14601))
- Tweak/fix: make new pdf controls mostly hidden [@shamoon](https://github.com/shamoon) ([#14595](https://github.com/paperless-ngx/paperless-ngx/pull/14595))
- Tweak/fix: move preview button in doc list [@shamoon](https://github.com/shamoon) ([#14594](https://github.com/paperless-ngx/paperless-ngx/pull/14594))
- Fix: checksum light mode display and cf dropdown mobile position [@shamoon](https://github.com/shamoon) ([#14590](https://github.com/paperless-ngx/paperless-ngx/pull/14590))
### All App Changes
<details>
<summary>6 changes</summary>
- Fix the id\_\_ schemae in the right place [@shamoon](https://github.com/shamoon) ([#14604](https://github.com/paperless-ngx/paperless-ngx/pull/14604))
- Fix: drop redundant change perms checks on cards open button [@shamoon](https://github.com/shamoon) ([#14601](https://github.com/paperless-ngx/paperless-ngx/pull/14601))
- Chore: Improve idle memory via more lazy imports [@stumpylog](https://github.com/stumpylog) ([#14599](https://github.com/paperless-ngx/paperless-ngx/pull/14599))
- Tweak/fix: make new pdf controls mostly hidden [@shamoon](https://github.com/shamoon) ([#14595](https://github.com/paperless-ngx/paperless-ngx/pull/14595))
- Tweak/fix: move preview button in doc list [@shamoon](https://github.com/shamoon) ([#14594](https://github.com/paperless-ngx/paperless-ngx/pull/14594))
- Fix: checksum light mode display and cf dropdown mobile position [@shamoon](https://github.com/shamoon) ([#14590](https://github.com/paperless-ngx/paperless-ngx/pull/14590))
</details>
## paperless-ngx 3.4.0 ## paperless-ngx 3.4.0
### Features / Enhancements ### Features / Enhancements
+30 -5
View File
@@ -493,7 +493,7 @@ fields and permissions, which will be merged.
#### Types {#workflow-trigger-types} #### Types {#workflow-trigger-types}
Currently, there are four events that correspond to workflow trigger 'types': Currently, there are five events that correspond to workflow trigger 'types':
1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption 1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption
folder or API), file path, file name, mail rule folder or API), file path, file name, mail rule
@@ -505,8 +505,10 @@ Currently, there are four events that correspond to workflow trigger 'types':
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document 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 (positive added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive
offsets will trigger after the date, negative offsets will trigger before). offsets will trigger after the date, negative offsets will trigger before).
5. **Manual**: a trigger that only runs when you explicitly choose to run the workflow against one or more existing documents. See
[running a workflow manually](#running-a-workflow-manually) below.
The following flow diagram illustrates the four document trigger types: The following flow diagram illustrates the document trigger types:
```mermaid ```mermaid
flowchart TD flowchart TD
@@ -526,6 +528,10 @@ flowchart TD
matching matching
trigger(s)"} trigger(s)"}
manual{"Matching
'Manual'
trigger(s)"}
A[New Document] --> consumption A[New Document] --> consumption
consumption --> |Yes| C[Workflow Actions Run] consumption --> |Yes| C[Workflow Actions Run]
consumption --> |No| D consumption --> |No| D
@@ -543,6 +549,10 @@ flowchart TD
scheduled --> |Yes| N[Workflow Actions Run] scheduled --> |Yes| N[Workflow Actions Run]
scheduled --> |No| O[Document Saved] scheduled --> |No| O[Document Saved]
N --> O N --> O
P[User Runs Workflow] --> manual
manual --> |Yes| Q[Workflow Actions Run]
manual --> |No| R[Document Saved]
Q --> R
``` ```
#### Filters {#workflow-trigger-filters} #### Filters {#workflow-trigger-filters}
@@ -554,9 +564,9 @@ Workflows allow you to filter by:
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for - 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. 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. - Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
- Content matching (`Added`, `Updated` and `Scheduled` triggers only). Filter document content using the matching settings. - Content matching (`Added`, `Updated`, `Scheduled` and `Manual` triggers only). Filter document content using the matching settings.
There are also 'advanced' filters available for `Added`, `Updated` and `Scheduled` triggers: There are also 'advanced' filters available for `Added`, `Updated`, `Scheduled` and `Manual` triggers:
- Any Tags: Filter for documents with any of the specified tags. - Any Tags: Filter for documents with any of the specified tags.
- All Tags: Filter for documents with all of the specified tags. - All Tags: Filter for documents with all of the specified tags.
@@ -569,6 +579,21 @@ There are also 'advanced' filters available for `Added`, `Updated` and `Schedule
- Not Storage paths: Filter documents without any of these storage paths. - Not Storage paths: Filter documents without any of these storage paths.
- Custom field query: Filter documents with a custom field query (the same as used for the document list filters). - Custom field query: Filter documents with a custom field query (the same as used for the document list filters).
#### Running a workflow manually {#running-a-workflow-manually}
A workflow with a **Manual** trigger does not run automatically. Instead, it can be run on demand against existing
documents from the web UI:
- From the document list, select one or more documents and choose **Run workflow** from the "Actions" menu.
- From an individual document, choose **Run workflow** from the "Actions" menu.
You will be asked which workflow to run. By default the workflow's trigger filters still apply, so documents that do
not match are skipped. You can instead choose to **run on all selected documents**, which ignores the trigger filters
and runs the workflow's actions on every selected document.
Manual runs happen in the background. As with scheduled workflows, the document list is not refreshed automatically when
they finish, so you may need to reload the list to see the results.
### Workflow Actions ### Workflow Actions
#### Types {#workflow-action-types} #### Types {#workflow-action-types}
@@ -649,7 +674,7 @@ new version of the same document, this time with normal OCR/text extraction appl
a password-protected file added with this trigger will briefly exist as an un-OCR'd version before a password-protected file added with this trigger will briefly exist as an un-OCR'd version before
the properly processed version is created. the properly processed version is created.
**Document Added**, **Document Updated**, **Scheduled**: these triggers run against a document that **Document Added**, **Document Updated**, **Scheduled**, **Manual**: these triggers run against a document that
already exists, so password removal happens immediately: the decrypted content is queued for already exists, so password removal happens immediately: the decrypted content is queued for
consumption as a new version right away. Note that if the document's initial consumption also consumption as a new version right away. Note that if the document's initial consumption also
happened while it was still encrypted, that original version will likewise be missing OCR text. happened while it was still encrypted, that original version will likewise be missing OCR text.
+10 -11
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "paperless-ngx" name = "paperless-ngx"
version = "3.4.0" version = "3.4.2"
description = """\ description = """\
A community-supported supercharged document management system: scan, index and archive all your physical documents\ A community-supported supercharged document management system: scan, index and archive all your physical documents\
""" """
@@ -26,22 +26,21 @@ dependencies = [
"dateparser~=1.4", "dateparser~=1.4",
# WARNING: django does not use semver. # WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes. # Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.13", "django~=5.2.18",
"django-allauth[mfa,socialaccount]~=65.20.0", "django-allauth[mfa,socialaccount]~=65.21.1",
"django-auditlog~=3.4.1", "django-auditlog~=3.4.1",
"django-cachalot~=2.9.0", "django-cachalot~=2.9.0",
"django-compression-middleware~=0.5.0", "django-compression-middleware~=0.5.0",
"django-cors-headers~=4.9.0", "django-cors-headers~=4.9.0",
"django-extensions~=4.1", "django-extensions~=4.1",
"django-filter>=25.1,<27", "django-filter>=26.2,<27",
"django-guardian>=3.3.3,<3.6", "django-guardian>=3.5.2,<3.6",
"django-multiselectfield~=1.0.1", "django-multiselectfield~=1.0.1",
"django-rich~=2.2.0", "django-rich~=2.2.0",
"django-soft-delete~=1.0.18", "django-soft-delete~=1.0.18",
"django-treenode>=0.24",
"djangorestframework~=3.16", "djangorestframework~=3.16",
"drf-spectacular~=0.30", "drf-spectacular~=0.30",
"drf-spectacular-sidecar>=2026.7.1,<2026.10", "drf-spectacular-sidecar>=2026.10.1,<2026.11",
"drf-writable-nested~=0.7.1", "drf-writable-nested~=0.7.1",
"filelock>=3.32,<4.3", "filelock>=3.32,<4.3",
"flower>=2.0.1,<2.3", "flower>=2.0.1,<2.3",
@@ -76,7 +75,7 @@ dependencies = [
"tantivy~=0.26.0", "tantivy~=0.26.0",
"tika-client[httpx]~=1.0", "tika-client[httpx]~=1.0",
"torch>=2.13,<2.15", "torch>=2.13,<2.15",
"turbohtml~=1.10.0", "turbohtml~=1.15.3",
"watchfiles>=1.2", "watchfiles>=1.2",
"whitenoise~=6.11", "whitenoise~=6.11",
"whoosh-compat[tantivy]==0.3", "whoosh-compat[tantivy]==0.3",
@@ -112,14 +111,14 @@ lint = [
testing = [ testing = [
"daphne", "daphne",
"factory-boy~=3.3.1", "factory-boy~=3.3.1",
"faker>=40.36,<40.40", "faker>=40.36,<40.44",
"imagehash", "imagehash",
"pytest~=9.1.1", "pytest~=9.1.1",
"pytest-cov~=7.1.0", "pytest-cov~=7.1.0",
"pytest-django>=4.12,<4.15", "pytest-django>=4.12,<4.15",
"pytest-env~=1.7.0", "pytest-env~=1.8.1",
"pytest-httpx", "pytest-httpx",
"pytest-mock~=3.15.1", "pytest-mock~=3.17.0",
"pytest-randomly~=5.0.0", "pytest-randomly~=5.0.0",
"pytest-rerunfailures~=16.4", "pytest-rerunfailures~=16.4",
"pytest-sugar", "pytest-sugar",
+3
View File
@@ -69,6 +69,9 @@
"src/polyfills.ts" "src/polyfills.ts"
], ],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"loader": {
".svg": "text"
},
"localize": true, "localize": true,
"assets": [ "assets": [
"src/apple-touch-icon.png", "src/apple-touch-icon.png",
+9 -1
View File
@@ -43,6 +43,7 @@ def configure_environment(instance_root: Path) -> None:
def seed_database() -> None: def seed_database() -> None:
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management import call_command from django.core.management import call_command
from django.utils import timezone from django.utils import timezone
@@ -63,11 +64,18 @@ def seed_database() -> None:
username="playwright", username="playwright",
password="playwright", # NOSONAR password="playwright", # NOSONAR
) )
User.objects.create_user( viewer = User.objects.create_user(
username="viewer", username="viewer",
password="viewer", # NOSONAR password="viewer", # NOSONAR
) )
viewer.user_permissions.add(
Permission.objects.get(
content_type__app_label="documents",
codename="view_uisettings",
),
)
inbox = Tag.objects.create(name="Inbox", is_inbox_tag=True, owner=admin) inbox = Tag.objects.create(name="Inbox", is_inbox_tag=True, owner=admin)
quick_filter = Tag.objects.create(name="Another Sample Tag", owner=admin) quick_filter = Tag.objects.create(name="Another Sample Tag", owner=admin)
Tag.objects.create(name="TagWithPartial", owner=admin) Tag.objects.create(name="TagWithPartial", owner=admin)
@@ -80,12 +80,21 @@ test('should show a list of notes', async ({ page }) => {
test('should support quick filters', async ({ page }) => { test('should support quick filters', async ({ page }) => {
await page.goto('/documents/1/details') await page.goto('/documents/1/details')
// filter button is available before tags have loaded
const selectedTags = page.locator('pngx-input-tags .ng-value pngx-tag')
await expect(selectedTags).toHaveCount(2)
await expect(
selectedTags.getByText('Another Sample Tag', { exact: true })
).toBeVisible()
await expect(selectedTags.getByText('Inbox', { exact: true })).toBeVisible()
await page await page
.getByRole('button', { name: 'Filter documents with these Tags' }) .getByRole('button', { name: 'Filter documents with these Tags' })
.click() .click()
await expect(page).toHaveURL( await expect(page).toHaveURL((url) => {
/tags__id__all=2,1&sort=created&reverse=1&page=1/ const tags = url.searchParams.get('tags__id__all')?.split(',').sort()
) return url.pathname === '/documents' && tags?.join(',') === '1,2'
})
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
}) })
test('should finish reloading the preview after a remote document update', async ({ test('should finish reloading the preview after a remote document update', async ({
@@ -123,7 +123,7 @@ test('sorting', async ({ page }) => {
await page.getByRole('button', { name: 'Notes' }).click() await page.getByRole('button', { name: 'Notes' }).click()
await expect(page).toHaveURL(/sort=num_notes/) await expect(page).toHaveURL(/sort=num_notes/)
await page.getByRole('button', { name: 'Sort' }).click() await page.getByRole('button', { name: 'Sort' }).click()
await page.locator('.w-100 > label > i-bs').first().click() await page.locator('.w-100 > label > pngx-bs-icon').first().click()
await expect(page).not.toHaveURL(/reverse=1/) await expect(page).not.toHaveURL(/reverse=1/)
}) })
@@ -6,8 +6,15 @@ test.use({
}, },
}) })
test('should not allow user to edit settings', async ({ page }) => { test.beforeEach(async ({ page }) => {
const settingsLoaded = page.waitForResponse((response) =>
response.url().endsWith('/api/ui_settings/')
)
await page.goto('/dashboard') await page.goto('/dashboard')
expect((await settingsLoaded).status()).toBe(200)
})
test('should not allow user to edit settings', async ({ page }) => {
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached() await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached()
await page.goto('/settings') await page.goto('/settings')
await expect(page.locator('body')).toHaveText( await expect(page.locator('body')).toHaveText(
@@ -16,7 +23,6 @@ test('should not allow user to edit settings', async ({ page }) => {
}) })
test('should not allow user to view documents', async ({ page }) => { test('should not allow user to view documents', async ({ page }) => {
await page.goto('/dashboard')
await expect( await expect(
page.locator('nav').getByRole('link', { name: 'Documents' }) page.locator('nav').getByRole('link', { name: 'Documents' })
).not.toBeAttached() ).not.toBeAttached()
@@ -31,7 +37,6 @@ test('should not allow user to view documents', async ({ page }) => {
}) })
test('should not allow user to view correspondents', async ({ page }) => { test('should not allow user to view correspondents', async ({ page }) => {
await page.goto('/dashboard')
await expect( await expect(
page.getByRole('link', { name: 'Attributes' }) page.getByRole('link', { name: 'Attributes' })
).not.toBeAttached() ).not.toBeAttached()
@@ -42,7 +47,6 @@ test('should not allow user to view correspondents', async ({ page }) => {
}) })
test('should not allow user to view tags', async ({ page }) => { test('should not allow user to view tags', async ({ page }) => {
await page.goto('/dashboard')
await expect( await expect(
page.getByRole('link', { name: 'Attributes' }) page.getByRole('link', { name: 'Attributes' })
).not.toBeAttached() ).not.toBeAttached()
@@ -53,7 +57,6 @@ test('should not allow user to view tags', async ({ page }) => {
}) })
test('should not allow user to view document types', async ({ page }) => { test('should not allow user to view document types', async ({ page }) => {
await page.goto('/dashboard')
await expect( await expect(
page.getByRole('link', { name: 'Attributes' }) page.getByRole('link', { name: 'Attributes' })
).not.toBeAttached() ).not.toBeAttached()
@@ -64,7 +67,6 @@ test('should not allow user to view document types', async ({ page }) => {
}) })
test('should not allow user to view storage paths', async ({ page }) => { test('should not allow user to view storage paths', async ({ page }) => {
await page.goto('/dashboard')
await expect( await expect(
page.getByRole('link', { name: 'Attributes' }) page.getByRole('link', { name: 'Attributes' })
).not.toBeAttached() ).not.toBeAttached()
@@ -75,7 +77,6 @@ test('should not allow user to view storage paths', async ({ page }) => {
}) })
test('should not allow user to view logs', async ({ page }) => { test('should not allow user to view logs', async ({ page }) => {
await page.goto('/dashboard')
await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached() await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached()
await page.goto('/logs') await page.goto('/logs')
await expect(page.locator('body')).toHaveText( await expect(page.locator('body')).toHaveText(
@@ -84,7 +85,6 @@ test('should not allow user to view logs', async ({ page }) => {
}) })
test('should not allow user to view tasks', async ({ page }) => { test('should not allow user to view tasks', async ({ page }) => {
await page.goto('/dashboard')
await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached() await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached()
await page.goto('/tasks') await page.goto('/tasks')
await expect(page.locator('body')).toHaveText( await expect(page.locator('body')).toHaveText(
+1 -1
View File
@@ -28,7 +28,7 @@ module.exports = {
'abstract-paperless-service', 'abstract-paperless-service',
], ],
transformIgnorePatterns: [ transformIgnorePatterns: [
'node_modules/(?!.*(\\.mjs$|tslib|lodash-es|normalize-diacritics|marked|@angular/common/locales/.*\\.js$))', 'node_modules/(?!.*(\\.mjs$|tslib|lodash-es|normalize-diacritics|marked|@angular/common/locales/.*\\.js$|bootstrap-icons/icons/.*\\.svg$))',
], ],
moduleNameMapper: { moduleNameMapper: {
...esmPreset.moduleNameMapper, ...esmPreset.moduleNameMapper,
+155 -135
View File
@@ -5,14 +5,14 @@
<trans-unit id="ngb.alert.close" datatype="html"> <trans-unit id="ngb.alert.close" datatype="html">
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/alert/alert.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/alert/alert.ts</context>
<context context-type="linenumber">38,39</context> <context context-type="linenumber">38,39</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.carousel.slide-number" datatype="html"> <trans-unit id="ngb.carousel.slide-number" datatype="html">
<source> Slide <x id="INTERPOLATION" equiv-text="xt(source?: Ngb"/> of <x id="INTERPOLATION_1" equiv-text="this._cycleToSe"/> </source> <source> Slide <x id="INTERPOLATION" equiv-text="source?: NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="s._cycleToSelec"/> </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">386,393</context> <context context-type="linenumber">386,393</context>
</context-group> </context-group>
<note priority="1" from="description">Currently selected slide number read by screen reader</note> <note priority="1" from="description">Currently selected slide number read by screen reader</note>
@@ -20,219 +20,219 @@
<trans-unit id="ngb.carousel.previous" datatype="html"> <trans-unit id="ngb.carousel.previous" datatype="html">
<source>Previous</source> <source>Previous</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">425,426</context> <context context-type="linenumber">425,426</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.carousel.next" datatype="html"> <trans-unit id="ngb.carousel.next" datatype="html">
<source>Next</source> <source>Next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">452,453</context> <context context-type="linenumber">452,453</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.select-month" datatype="html"> <trans-unit id="ngb.datepicker.select-month" datatype="html">
<source>Select month</source> <source>Select month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.select-year" datatype="html"> <trans-unit id="ngb.datepicker.select-year" datatype="html">
<source>Select year</source> <source>Select year</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation-select.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.previous-month" datatype="html"> <trans-unit id="ngb.datepicker.previous-month" datatype="html">
<source>Previous month</source> <source>Previous month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">73,76</context> <context context-type="linenumber">73,76</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">73,76</context> <context context-type="linenumber">73,76</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.datepicker.next-month" datatype="html"> <trans-unit id="ngb.datepicker.next-month" datatype="html">
<source>Next month</source> <source>Next month</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">73,74</context> <context context-type="linenumber">73,74</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/datepicker/datepicker-navigation.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">73,74</context> <context context-type="linenumber">73,74</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.first" datatype="html"> <trans-unit id="ngb.pagination.first" datatype="html">
<source>««</source> <source>««</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.previous" datatype="html"> <trans-unit id="ngb.pagination.previous" datatype="html">
<source>«</source> <source>«</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next" datatype="html"> <trans-unit id="ngb.pagination.next" datatype="html">
<source>»</source> <source>»</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277</context> <context context-type="linenumber">277</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.last" datatype="html"> <trans-unit id="ngb.pagination.last" datatype="html">
<source>»»</source> <source>»»</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277</context> <context context-type="linenumber">277</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.first-aria" datatype="html"> <trans-unit id="ngb.pagination.first-aria" datatype="html">
<source>First</source> <source>First</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.previous-aria" datatype="html"> <trans-unit id="ngb.pagination.previous-aria" datatype="html">
<source>Previous</source> <source>Previous</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.next-aria" datatype="html"> <trans-unit id="ngb.pagination.next-aria" datatype="html">
<source>Next</source> <source>Next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.pagination.last-aria" datatype="html"> <trans-unit id="ngb.pagination.last-aria" datatype="html">
<source>Last</source> <source>Last</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">277,278</context> <context context-type="linenumber">277,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.progressbar.value" datatype="html"> <trans-unit id="ngb.progressbar.value" datatype="html">
<source><x id="INTERPOLATION"/></source> <source><x id="INTERPOLATION"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/progressbar/progressbar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/progressbar/progressbar.ts</context>
<context context-type="linenumber">39,40</context> <context context-type="linenumber">39,40</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.HH" datatype="html"> <trans-unit id="ngb.timepicker.HH" datatype="html">
<source>HH</source> <source>HH</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.hours" datatype="html"> <trans-unit id="ngb.timepicker.hours" datatype="html">
<source>Hours</source> <source>Hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.MM" datatype="html"> <trans-unit id="ngb.timepicker.MM" datatype="html">
<source>MM</source> <source>MM</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.minutes" datatype="html"> <trans-unit id="ngb.timepicker.minutes" datatype="html">
<source>Minutes</source> <source>Minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-hours" datatype="html"> <trans-unit id="ngb.timepicker.increment-hours" datatype="html">
<source>Increment hours</source> <source>Increment hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html"> <trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
<source>Decrement hours</source> <source>Decrement hours</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html"> <trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
<source>Increment minutes</source> <source>Increment minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html"> <trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
<source>Decrement minutes</source> <source>Decrement minutes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.SS" datatype="html"> <trans-unit id="ngb.timepicker.SS" datatype="html">
<source>SS</source> <source>SS</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.seconds" datatype="html"> <trans-unit id="ngb.timepicker.seconds" datatype="html">
<source>Seconds</source> <source>Seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html"> <trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
<source>Increment seconds</source> <source>Increment seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html"> <trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
<source>Decrement seconds</source> <source>Decrement seconds</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.timepicker.PM" datatype="html"> <trans-unit id="ngb.timepicker.PM" datatype="html">
<source><x id="INTERPOLATION"/></source> <source><x id="INTERPOLATION"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">442</context> <context context-type="linenumber">442</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="ngb.toast.close-aria" datatype="html"> <trans-unit id="ngb.toast.close-aria" datatype="html">
<source>Close</source> <source>Close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.2_@angular+core@22.2.2_@angular+_56804c6e9d763769999573a32671b982/node_modules/src/toast/toast.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.2.3_@angular+core@22.2.3_@angular+_2a970977f09c8afdd1f9933e219c8db5/node_modules/src/toast/toast.ts</context>
<context context-type="linenumber">80</context> <context context-type="linenumber">80</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
@@ -687,7 +687,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">441,442</context> <context context-type="linenumber">442</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">443,444</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html</context>
@@ -835,7 +839,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">30</context> <context context-type="linenumber">32</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
@@ -859,7 +863,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">460</context> <context context-type="linenumber">474</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@@ -1137,7 +1141,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">18</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -1362,7 +1366,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1913</context> <context context-type="linenumber">1918</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1577733187050997705" datatype="html"> <trans-unit id="1577733187050997705" datatype="html">
@@ -1402,7 +1406,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">239,240</context> <context context-type="linenumber">240,241</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -1474,6 +1478,10 @@
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">166</context> <context context-type="linenumber">166</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
<context context-type="linenumber">46</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="309314153079578337" datatype="html"> <trans-unit id="309314153079578337" datatype="html">
<source>Default Permissions</source> <source>Default Permissions</source>
@@ -1733,7 +1741,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">248,249</context> <context context-type="linenumber">249,250</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -1768,7 +1776,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">257,258</context> <context context-type="linenumber">258,259</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -1811,7 +1819,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">221,222</context> <context context-type="linenumber">222,223</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
@@ -2289,7 +2297,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">266,267</context> <context context-type="linenumber">267,268</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
@@ -3393,11 +3401,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1527</context> <context context-type="linenumber">1532</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1914</context> <context context-type="linenumber">1919</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -4115,7 +4123,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">388</context> <context context-type="linenumber">391</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7515883357904500238" datatype="html"> <trans-unit id="7515883357904500238" datatype="html">
@@ -4144,7 +4152,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1476</context> <context context-type="linenumber">1481</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -4288,7 +4296,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1967</context> <context context-type="linenumber">1972</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6661109599266152398" datatype="html"> <trans-unit id="6661109599266152398" datatype="html">
@@ -4299,7 +4307,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1968</context> <context context-type="linenumber">1973</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5162686434580248853" datatype="html"> <trans-unit id="5162686434580248853" datatype="html">
@@ -4310,7 +4318,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1969</context> <context context-type="linenumber">1974</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6665634854532231106" datatype="html"> <trans-unit id="6665634854532231106" datatype="html">
@@ -4389,7 +4397,7 @@
<source>Create new field</source> <source>Create new field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context>
<context context-type="linenumber">20</context> <context context-type="linenumber">19</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6973528734330066202" datatype="html"> <trans-unit id="6973528734330066202" datatype="html">
@@ -4786,7 +4794,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">275,276</context> <context context-type="linenumber">276,277</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -6567,7 +6575,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1480</context> <context context-type="linenumber">1485</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -6604,7 +6612,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">218,219</context> <context context-type="linenumber">219,220</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
@@ -6788,21 +6796,21 @@
<source>Email sent</source> <source>Email sent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context>
<context context-type="linenumber">53</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4632604705213766411" datatype="html"> <trans-unit id="4632604705213766411" datatype="html">
<source>Error emailing documents</source> <source>Error emailing documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context>
<context context-type="linenumber">59</context> <context context-type="linenumber">58</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3742745894977668908" datatype="html"> <trans-unit id="3742745894977668908" datatype="html">
<source>Error emailing document</source> <source>Error emailing document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context>
<context context-type="linenumber">60</context> <context context-type="linenumber">59</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6381578200008167206" datatype="html"> <trans-unit id="6381578200008167206" datatype="html">
@@ -7148,14 +7156,14 @@
<source>Copy ID</source> <source>Copy ID</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">7</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5392341774767336507" datatype="html"> <trans-unit id="5392341774767336507" datatype="html">
<source>Copied!</source> <source>Copied!</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">8</context> <context context-type="linenumber">9</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@@ -7190,7 +7198,7 @@
<source>Read more</source> <source>Read more</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context> <context context-type="sourcefile">src/app/components/common/page-header/page-header.component.html</context>
<context context-type="linenumber">24,25</context> <context context-type="linenumber">26,27</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
@@ -8410,7 +8418,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">320</context> <context context-type="linenumber">321</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="78870852467682010" datatype="html"> <trans-unit id="78870852467682010" datatype="html">
@@ -8425,7 +8433,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">360</context> <context context-type="linenumber">363</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="157572966557284263" datatype="html"> <trans-unit id="157572966557284263" datatype="html">
@@ -8440,7 +8448,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">367</context> <context context-type="linenumber">370</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="883965278435032344" datatype="html"> <trans-unit id="883965278435032344" datatype="html">
@@ -8458,14 +8466,14 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">388</context> <context context-type="linenumber">391</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="872092479747931526" datatype="html"> <trans-unit id="872092479747931526" datatype="html">
<source>No documents</source> <source>No documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html</context> <context context-type="sourcefile">src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html</context>
<context context-type="linenumber">189,192</context> <context context-type="linenumber">191,194</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1069523139277190436" datatype="html"> <trans-unit id="1069523139277190436" datatype="html">
@@ -8588,7 +8596,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">483</context> <context context-type="linenumber">497</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@@ -8701,7 +8709,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">483</context> <context context-type="linenumber">168</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5028777105388019087" datatype="html"> <trans-unit id="5028777105388019087" datatype="html">
@@ -8863,63 +8871,82 @@
<source>Save &amp; next</source> <source>Save &amp; next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">443</context> <context context-type="linenumber">446</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">457</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4910102545766233758" datatype="html"> <trans-unit id="4910102545766233758" datatype="html">
<source>Save &amp; close</source> <source>Save &amp; close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">445</context> <context context-type="linenumber">448</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">459</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3823219296477075982" datatype="html"> <trans-unit id="3823219296477075982" datatype="html">
<source>Discard</source> <source>Discard</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">447,448</context> <context context-type="linenumber">450</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">451,452</context>
</context-group>
</trans-unit>
<trans-unit id="5587846594415285055" datatype="html">
<source>More save options</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">454</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1309556917227148591" datatype="html"> <trans-unit id="1309556917227148591" datatype="html">
<source>Document loading...</source> <source>Document loading...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">455</context> <context context-type="linenumber">469</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2266163016683537825" datatype="html"> <trans-unit id="2266163016683537825" datatype="html">
<source>of <x id="INTERPOLATION" equiv-text="{{previewNumPages()}}"/></source> <source>of <x id="INTERPOLATION" equiv-text="{{previewNumPages()}}"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">484,485</context> <context context-type="linenumber">498,499</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="944767796496567082" datatype="html"> <trans-unit id="944767796496567082" datatype="html">
<source>Zoom out</source> <source>Zoom out</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">488</context> <context context-type="linenumber">502</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6421460271971606186" datatype="html"> <trans-unit id="6421460271971606186" datatype="html">
<source>Zoom</source> <source>Zoom</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">491</context> <context context-type="linenumber">505</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6500061599200357251" datatype="html"> <trans-unit id="6500061599200357251" datatype="html">
<source>Zoom in</source> <source>Zoom in</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">498</context> <context context-type="linenumber">512</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8191371354890763172" datatype="html"> <trans-unit id="8191371354890763172" datatype="html">
<source>Enter Password</source> <source>Enter Password</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">532</context> <context context-type="linenumber">546</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5758784066858623886" datatype="html"> <trans-unit id="5758784066858623886" datatype="html">
@@ -9115,14 +9142,14 @@
<source>Do you really want to move the document &quot;<x id="PH" equiv-text="this.document().title"/>&quot; to the trash?</source> <source>Do you really want to move the document &quot;<x id="PH" equiv-text="this.document().title"/>&quot; to the trash?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1477</context> <context context-type="linenumber">1482</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="282586936710748252" datatype="html"> <trans-unit id="282586936710748252" datatype="html">
<source>Documents can be restored prior to permanent deletion.</source> <source>Documents can be restored prior to permanent deletion.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1478</context> <context context-type="linenumber">1483</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -9133,21 +9160,21 @@
<source>Moved &quot;<x id="PH" equiv-text="this.document().title"/>&quot; to the trash</source> <source>Moved &quot;<x id="PH" equiv-text="this.document().title"/>&quot; to the trash</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1496</context> <context context-type="linenumber">1501</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7295637485862454066" datatype="html"> <trans-unit id="7295637485862454066" datatype="html">
<source>Error deleting document</source> <source>Error deleting document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1503</context> <context context-type="linenumber">1508</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="619486176823357521" datatype="html"> <trans-unit id="619486176823357521" datatype="html">
<source>Reprocess confirm</source> <source>Reprocess confirm</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1523</context> <context context-type="linenumber">1528</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
@@ -9158,102 +9185,102 @@
<source>This operation will permanently recreate the archive file for this document.</source> <source>This operation will permanently recreate the archive file for this document.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1524</context> <context context-type="linenumber">1529</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="302054111564709516" datatype="html"> <trans-unit id="302054111564709516" datatype="html">
<source>The archive file will be re-generated with the current settings.</source> <source>The archive file will be re-generated with the current settings.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1525</context> <context context-type="linenumber">1530</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4700389117298802932" datatype="html"> <trans-unit id="4700389117298802932" datatype="html">
<source>Reprocess operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source> <source>Reprocess operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1538</context> <context context-type="linenumber">1543</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4409560272830824468" datatype="html"> <trans-unit id="4409560272830824468" datatype="html">
<source>Error executing operation</source> <source>Error executing operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1549</context> <context context-type="linenumber">1554</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6030453331794586802" datatype="html"> <trans-unit id="6030453331794586802" datatype="html">
<source>Error downloading document</source> <source>Error downloading document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1613</context> <context context-type="linenumber">1618</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4458954481601077369" datatype="html"> <trans-unit id="4458954481601077369" datatype="html">
<source>Page Fit</source> <source>Page Fit</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1691</context> <context context-type="linenumber">1696</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4663705961777238777" datatype="html"> <trans-unit id="4663705961777238777" datatype="html">
<source>PDF edit operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source> <source>PDF edit operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1934</context> <context context-type="linenumber">1939</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9043972994040261999" datatype="html"> <trans-unit id="9043972994040261999" datatype="html">
<source>Error executing PDF edit operation</source> <source>Error executing PDF edit operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1946</context> <context context-type="linenumber">1951</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6172690334763056188" datatype="html"> <trans-unit id="6172690334763056188" datatype="html">
<source>Please enter the current password before attempting to remove it.</source> <source>Please enter the current password before attempting to remove it.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1957</context> <context context-type="linenumber">1962</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="968660764814228922" datatype="html"> <trans-unit id="968660764814228922" datatype="html">
<source>Password removal operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source> <source>Password removal operation for &quot;<x id="PH" equiv-text="this.document().title"/>&quot; will begin in the background.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">1991</context> <context context-type="linenumber">1996</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2282118435712883014" datatype="html"> <trans-unit id="2282118435712883014" datatype="html">
<source>Error executing password removal operation</source> <source>Error executing password removal operation</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2005</context> <context context-type="linenumber">2010</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3740891324955700797" datatype="html"> <trans-unit id="3740891324955700797" datatype="html">
<source>Print failed.</source> <source>Print failed.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2055</context> <context context-type="linenumber">2060</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6457245677384603573" datatype="html"> <trans-unit id="6457245677384603573" datatype="html">
<source>Error loading document for printing.</source> <source>Error loading document for printing.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2064</context> <context context-type="linenumber">2069</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6085793215710522488" datatype="html"> <trans-unit id="6085793215710522488" datatype="html">
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source> <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2147</context> <context context-type="linenumber">2152</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">2153</context> <context context-type="linenumber">2158</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4958946940233632319" datatype="html"> <trans-unit id="4958946940233632319" datatype="html">
@@ -9919,7 +9946,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">336</context> <context context-type="linenumber">339</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="106713086593101376" datatype="html"> <trans-unit id="106713086593101376" datatype="html">
@@ -10234,21 +10261,21 @@
<source>Sort by title</source> <source>Sort by title</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">213</context> <context context-type="linenumber">214</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6232673011753681091" datatype="html"> <trans-unit id="6232673011753681091" datatype="html">
<source>Sort by owner</source> <source>Sort by owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">226</context> <context context-type="linenumber">227</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3715596725146409911" datatype="html"> <trans-unit id="3715596725146409911" datatype="html">
<source>Owner</source> <source>Owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">230,231</context> <context context-type="linenumber">231,232</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -10263,49 +10290,49 @@
<source>Sort by notes</source> <source>Sort by notes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">235</context> <context context-type="linenumber">236</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5499001829734502606" datatype="html"> <trans-unit id="5499001829734502606" datatype="html">
<source>Sort by document type</source> <source>Sort by document type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">244</context> <context context-type="linenumber">245</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6213829731736042759" datatype="html"> <trans-unit id="6213829731736042759" datatype="html">
<source>Sort by storage path</source> <source>Sort by storage path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">253</context> <context context-type="linenumber">254</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3406167410329973166" datatype="html"> <trans-unit id="3406167410329973166" datatype="html">
<source>Sort by created date</source> <source>Sort by created date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">262</context> <context context-type="linenumber">263</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3769035778779263084" datatype="html"> <trans-unit id="3769035778779263084" datatype="html">
<source>Sort by added date</source> <source>Sort by added date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">271</context> <context context-type="linenumber">272</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4874754501044009042" datatype="html"> <trans-unit id="4874754501044009042" datatype="html">
<source>Sort by number of pages</source> <source>Sort by number of pages</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">280</context> <context context-type="linenumber">281</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3817498941817715969" datatype="html"> <trans-unit id="3817498941817715969" datatype="html">
<source>Pages</source> <source>Pages</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">284,285</context> <context context-type="linenumber">285,286</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@@ -10324,28 +10351,28 @@
<source> Shared </source> <source> Shared </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">288</context> <context context-type="linenumber">289</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5083658411133224968" datatype="html"> <trans-unit id="5083658411133224968" datatype="html">
<source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source> <source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">294,295</context> <context context-type="linenumber">295,296</context>
</context-group>
</trans-unit>
<trans-unit id="2179847500064178686" datatype="html">
<source>Edit document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">328</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3420321797707163677" datatype="html"> <trans-unit id="3420321797707163677" datatype="html">
<source>Preview document</source> <source>Preview document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">329</context> <context context-type="linenumber">327</context>
</context-group>
</trans-unit>
<trans-unit id="2179847500064178686" datatype="html">
<source>Edit document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">335</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4512084577073831437" datatype="html"> <trans-unit id="4512084577073831437" datatype="html">
@@ -10377,31 +10404,31 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2155249406916744630" datatype="html"> <trans-unit id="2155249406916744630" datatype="html">
<source>View &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot; saved successfully.</source> <source>View &quot;<x id="PH" equiv-text="view.name ?? title"/>&quot; saved successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">422</context> <context context-type="linenumber">423</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4646273665293421938" datatype="html"> <trans-unit id="4646273665293421938" datatype="html">
<source>Failed to save view &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot;.</source> <source>Failed to save view &quot;<x id="PH" equiv-text="title"/>&quot;.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">428</context> <context context-type="linenumber">429</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6837554170707123455" datatype="html"> <trans-unit id="6837554170707123455" datatype="html">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source> <source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">497</context> <context context-type="linenumber">498</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6028096992841030074" datatype="html"> <trans-unit id="6028096992841030074" datatype="html">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully, but could not update visibility settings.</source> <source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully, but could not update visibility settings.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">503</context> <context context-type="linenumber">504</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="739880801667335279" datatype="html"> <trans-unit id="739880801667335279" datatype="html">
@@ -11451,13 +11478,6 @@
<context context-type="linenumber">4,5</context> <context context-type="linenumber">4,5</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8420539325839715135" datatype="html">
<source><x id="START_TAG_I_BS" ctype="x-i_bs" equiv-text="&lt;i-bs class=&quot;me-1&quot; name=&quot;person-fill-lock&quot;&gt;"/><x id="CLOSE_TAG_I_BS" ctype="x-i_bs" equiv-text="&lt;/i-bs&gt;"/>Permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
<context context-type="linenumber">46,47</context>
</context-group>
</trans-unit>
<trans-unit id="6338800642797811873" datatype="html"> <trans-unit id="6338800642797811873" datatype="html">
<source>Documents page size</source> <source>Documents page size</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -13458,14 +13478,14 @@
<source>Prev</source> <source>Prev</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">482</context> <context context-type="linenumber">167</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1241348629231510663" datatype="html"> <trans-unit id="1241348629231510663" datatype="html">
<source>End</source> <source>End</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/main.ts</context> <context context-type="sourcefile">src/main.ts</context>
<context context-type="linenumber">484</context> <context context-type="linenumber">169</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
</body> </body>
+25 -25
View File
@@ -1,6 +1,6 @@
{ {
"name": "paperless-ngx-ui", "name": "paperless-ngx-ui",
"version": "3.4.0", "version": "3.4.2",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"ng": "ng", "ng": "ng",
@@ -15,60 +15,60 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^22.2.2", "@angular/cdk": "^22.2.4",
"@angular/common": "~22.2.2", "@angular/common": "~22.2.3",
"@angular/compiler": "~22.2.2", "@angular/compiler": "~22.2.3",
"@angular/core": "~22.2.2", "@angular/core": "~22.2.3",
"@angular/forms": "~22.2.2", "@angular/forms": "~22.2.3",
"@angular/localize": "~22.2.2", "@angular/localize": "~22.2.3",
"@angular/platform-browser": "~22.2.2", "@angular/platform-browser": "~22.2.3",
"@angular/router": "~22.2.2", "@angular/router": "~22.2.3",
"@ng-bootstrap/ng-bootstrap": "^21.0.0", "@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "~24.2.1", "@ng-select/ng-select": "~24.2.1",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"lodash-es": "^4.18.1", "lodash-es": "^4.18.1",
"marked": "~18.0.14", "marked": "~18.1.1",
"mime-names": "^1.0.0", "mime-names": "^1.0.0",
"ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^10.1.0", "ngx-color": "^10.1.0",
"ngx-cookie-service": "^22.0.0", "ngx-cookie-service": "^22.0.0",
"ngx-device-detector": "^12.0.0", "ngx-device-detector": "^12.0.0",
"ngx-ui-tour-ng-bootstrap": "^19.0.0", "ngx-ui-tour-ng-bootstrap": "^19.0.0",
"normalize-diacritics": "^5.0.0", "normalize-diacritics": "^5.0.0",
"pdfjs-dist": "^6.3.289", "pdfjs-dist": "^6.4.299",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"utif": "^3.1.0", "utif": "^3.1.0",
"uuid": "^14.0.2" "uuid": "^14.0.3"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "^22.0.1", "@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.2.2", "@angular-devkit/core": "^22.2.4",
"@angular-devkit/schematics": "^22.2.2", "@angular-devkit/schematics": "^22.2.4",
"@angular-eslint/builder": "22.6.0", "@angular-eslint/builder": "22.6.0",
"@angular-eslint/eslint-plugin": "22.6.0", "@angular-eslint/eslint-plugin": "22.6.0",
"@angular-eslint/eslint-plugin-template": "22.6.0", "@angular-eslint/eslint-plugin-template": "22.6.0",
"@angular-eslint/schematics": "22.6.0", "@angular-eslint/schematics": "22.6.0",
"@angular-eslint/template-parser": "22.6.0", "@angular-eslint/template-parser": "22.6.0",
"@angular/build": "22.2.2", "@angular/build": "22.2.4",
"@angular/cli": "22.2.2", "@angular/cli": "22.2.4",
"@angular/compiler-cli": "~22.2.2", "@angular/compiler-cli": "~22.2.3",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.64.1",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^26.6.2", "@types/node": "^26.6.5",
"@typescript-eslint/eslint-plugin": "^8.70.1", "@typescript-eslint/eslint-plugin": "^8.72.0",
"@typescript-eslint/parser": "^8.70.1", "@typescript-eslint/parser": "^8.72.0",
"@typescript-eslint/utils": "^8.70.1", "@typescript-eslint/utils": "^8.72.0",
"eslint": "^10.11.0", "eslint": "^10.13.0",
"jest": "30.5.2", "jest": "30.5.2",
"jest-environment-jsdom": "^30.5.2", "jest-environment-jsdom": "^30.5.2",
"jest-junit": "^17.0.0", "jest-junit": "^17.0.0",
"jest-preset-angular": "^17.0.3", "jest-preset-angular": "^17.0.3",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"prettier": "^3.9.9", "prettier": "^3.9.10",
"prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.2", "ts-node": "~10.9.2",
"typescript": "^6.0.3" "typescript": "^6.0.3"
+702 -664
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -37,7 +37,7 @@ export const routes: Routes = [
path: 'dashboard', path: 'dashboard',
component: DashboardComponent, component: DashboardComponent,
data: { data: {
componentName: 'AppFrameComponent', componentName: 'DashboardComponent',
}, },
}, },
{ {
-2
View File
@@ -3,7 +3,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Router, RouterModule } from '@angular/router' import { Router, RouterModule } from '@angular/router'
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { import {
provideUiTour, provideUiTour,
TourNgBootstrap, TourNgBootstrap,
@@ -47,7 +46,6 @@ describe('AppComponent', () => {
ToastsComponent, ToastsComponent,
FileDropComponent, FileDropComponent,
TourNgBootstrap, TourNgBootstrap,
NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [ providers: [
PermissionsGuard, PermissionsGuard,
@@ -28,7 +28,7 @@
{{option.title}} {{option.title}}
</h6> </h6>
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer"> <a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
<i-bs name="info-circle"></i-bs> <pngx-bs-icon name="info-circle"></pngx-bs-icon>
</a> </a>
@if (isExternallyConfigured(option.config_key)) { @if (isExternallyConfigured(option.config_key)) {
@if (isSet(option.key)) { @if (isSet(option.key)) {
@@ -40,11 +40,11 @@
@if (isSet(option.key)) { @if (isSet(option.key)) {
@if (isExternallyConfigured(option.config_key)) { @if (isExternallyConfigured(option.config_key)) {
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Use the externally configured value" i18n-title (click)="resetOption(option.key)"> <button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Use the externally configured value" i18n-title (click)="resetOption(option.key)">
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset to external</ng-container> <pngx-bs-icon class="me-1" name="x"></pngx-bs-icon><ng-container i18n>Reset to external</ng-container>
</button> </button>
} @else { } @else {
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)"> <button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container> <pngx-bs-icon class="me-1" name="x"></pngx-bs-icon><ng-container i18n>Reset</ng-container>
</button> </button>
} }
} }
@@ -6,7 +6,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { import {
ConfigCategory, ConfigCategory,
@@ -39,7 +38,6 @@ describe('ConfigComponent', () => {
NgSelectModule, NgSelectModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
ConfigComponent, ConfigComponent,
TextComponent, TextComponent,
SelectComponent, SelectComponent,
@@ -9,7 +9,6 @@ import {
} from '@angular/forms' } from '@angular/forms'
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap' import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms' import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { import {
BehaviorSubject, BehaviorSubject,
Observable, Observable,
@@ -17,6 +16,7 @@ import {
first, first,
takeUntil, takeUntil,
} from 'rxjs' } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { import {
ConfigCategory, ConfigCategory,
ConfigOption, ConfigOption,
@@ -52,7 +52,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
NgbNavModule, NgbNavModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
], ],
}) })
export class ConfigComponent export class ConfigComponent
@@ -8,7 +8,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { BrowserModule, By } from '@angular/platform-browser' import { BrowserModule, By } from '@angular/platform-browser'
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap' import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { LogService } from 'src/app/services/rest/log.service' import { LogService } from 'src/app/services/rest/log.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
@@ -40,7 +39,6 @@ describe('LogsComponent', () => {
imports: [ imports: [
BrowserModule, BrowserModule,
NgbModule, NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
LogsComponent, LogsComponent,
PageHeaderComponent, PageHeaderComponent,
CommonModule, CommonModule,
@@ -5,7 +5,7 @@
i18n-info i18n-info
> >
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"> <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
<i-bs class="me-2" name="airplane"></i-bs><ng-container i18n>Start tour</ng-container> <pngx-bs-icon class="me-2" name="airplane"></pngx-bs-icon><ng-container i18n>Start tour</ng-container>
</button> </button>
@if (canViewSystemStatus) { @if (canViewSystemStatus) {
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()" <button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
@@ -13,14 +13,14 @@
@if (!systemStatus()) { @if (!systemStatus()) {
<div class="spinner-border spinner-border-sm me-2 h-75" role="status"></div> <div class="spinner-border spinner-border-sm me-2 h-75" role="status"></div>
} @else { } @else {
<i-bs class="me-2" name="card-checklist"></i-bs> <pngx-bs-icon class="me-2" name="card-checklist"></pngx-bs-icon>
@if (systemStatusHasErrors) { @if (systemStatusHasErrors) {
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0"> <span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
<i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs> <pngx-bs-icon name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></pngx-bs-icon>
</span> </span>
} @else { } @else {
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0"> <span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
<i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs> <pngx-bs-icon name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></pngx-bs-icon>
</span> </span>
} }
} }
@@ -30,7 +30,7 @@
@if (permissionsService.isAdmin()) { @if (permissionsService.isAdmin()) {
<a class="btn btn-sm btn-primary" href="admin/" target="_blank"> <a class="btn btn-sm btn-primary" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container> <ng-container i18n>Open Django Admin</ng-container>
<i-bs class="ms-2" name="arrow-up-right"></i-bs> <pngx-bs-icon class="ms-2" name="arrow-up-right"></pngx-bs-icon>
</a> </a>
} }
</pngx-page-header> </pngx-page-header>
@@ -151,7 +151,7 @@
</div> </div>
<div class="col-2"> <div class="col-2">
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()"> <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> <pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon><ng-container i18n>Reset</ng-container>
</button> </button>
</div> </div>
</div> </div>
@@ -181,7 +181,7 @@
<div class="col d-flex flex-row align-items-start"> <div class="col d-flex flex-row align-items-start">
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check> <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"> <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> <pngx-bs-icon name="question-circle"></pngx-bs-icon>
</button> </button>
<ng-template #updatesPopover> <ng-template #updatesPopover>
<p i18n> <p i18n>
@@ -263,9 +263,9 @@
<input type="radio" class="btn-check" id="suggestionSourceBoth" [value]="SuggestionSource.Both" formControlName="documentEditingSuggestionSource"> <input type="radio" class="btn-check" id="suggestionSourceBoth" [value]="SuggestionSource.Both" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceBoth"><ng-container i18n>Both</ng-container></label> <label class="btn btn-outline-primary" for="suggestionSourceBoth"><ng-container i18n>Both</ng-container></label>
<input type="radio" class="btn-check" id="suggestionSourceML" [value]="SuggestionSource.ML" formControlName="documentEditingSuggestionSource"> <input type="radio" class="btn-check" id="suggestionSourceML" [value]="SuggestionSource.ML" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceML"><i-bs class="me-1" name="cpu"></i-bs><ng-container i18n>ML only</ng-container></label> <label class="btn btn-outline-primary" for="suggestionSourceML"><pngx-bs-icon class="me-1" name="cpu"></pngx-bs-icon><ng-container i18n>ML only</ng-container></label>
<input type="radio" class="btn-check" id="suggestionSourceAI" [value]="SuggestionSource.AI" formControlName="documentEditingSuggestionSource"> <input type="radio" class="btn-check" id="suggestionSourceAI" [value]="SuggestionSource.AI" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceAI"><i-bs class="me-1" name="stars"></i-bs><ng-container i18n>AI only</ng-container></label> <label class="btn btn-outline-primary" for="suggestionSourceAI"><pngx-bs-icon class="me-1" name="stars"></pngx-bs-icon><ng-container i18n>AI only</ng-container></label>
</fieldset> </fieldset>
</div> </div>
</div> </div>
@@ -15,7 +15,6 @@ import {
NgbNavLink, NgbNavLink,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap' import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
@@ -133,7 +132,6 @@ describe('SettingsComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbAlertModule, NgbAlertModule,
NgSelectModule, NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons),
NgbModalModule, NgbModalModule,
DragDropModule, DragDropModule,
SettingsComponent, SettingsComponent,
@@ -23,7 +23,6 @@ import {
NgbPopoverModule, NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms' import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import { import {
BehaviorSubject, BehaviorSubject,
@@ -34,6 +33,7 @@ import {
takeUntil, takeUntil,
tap, tap,
} from 'rxjs' } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Group } from 'src/app/data/group' import { Group } from 'src/app/data/group'
import { import {
SystemStatus, SystemStatus,
@@ -135,7 +135,7 @@ const sidebarItemLabels: Record<HideableSidebarItemID, string> = {
ReactiveFormsModule, ReactiveFormsModule,
NgbNavModule, NgbNavModule,
NgbPopoverModule, NgbPopoverModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
], ],
}) })
export class SettingsComponent export class SettingsComponent
@@ -7,13 +7,13 @@
> >
<div class="btn-toolbar col col-md-auto align-items-center gap-2"> <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"> <button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container> <pngx-bs-icon name="x" class="me-1"></pngx-bs-icon><ng-container i18n>Clear selection</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="visibleTasks.length === 0"> <button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="visibleTasks.length === 0">
<i-bs name="check2-all" class="me-1"></i-bs>{{dismissButtonText}} <pngx-bs-icon name="check2-all" class="me-1"></pngx-bs-icon>{{dismissButtonText}}
</button> </button>
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissAllTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="totalTasks() === 0"> <button class="btn btn-sm btn-outline-primary me-2" (click)="dismissAllTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="totalTasks() === 0">
<i-bs name="check2-all" class="me-1"></i-bs><ng-container i18n>Dismiss all</ng-container> <pngx-bs-icon name="check2-all" class="me-1"></pngx-bs-icon><ng-container i18n>Dismiss all</ng-container>
</button> </button>
<div class="form-check form-switch mb-0 ms-2"> <div class="form-check form-switch mb-0 ms-2">
<input id="autoRefreshSwitch" class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled"> <input id="autoRefreshSwitch" class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
@@ -86,7 +86,7 @@
</div> </div>
@if (filterText?.length) { @if (filterText?.length) {
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()" aria-label="Clear search" i18n-aria-label> <button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()" aria-label="Clear search" i18n-aria-label>
<i-bs width="1em" height="1em" name="x"></i-bs> <pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon>
</button> </button>
} }
<label class="visually-hidden" for="taskSearch" i18n>Search tasks</label> <label class="visually-hidden" for="taskSearch" i18n>Search tasks</label>
@@ -98,7 +98,7 @@
@if (isFiltered) { @if (isFiltered) {
<button class="btn btn-link py-0 ms-md-auto" (click)="resetFilters()"> <button class="btn btn-link py-0 ms-md-auto" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small> <pngx-bs-icon width="1em" height="1em" name="x"></pngx-bs-icon><small i18n>Reset filters</small>
</button> </button>
} }
@@ -125,7 +125,7 @@
@if (hasMore) { @if (hasMore) {
<ng-container i18n>{{tasks.length}} of {{count}} tasks</ng-container> <ng-container i18n>{{tasks.length}} of {{count}} tasks</ng-container>
<button class="btn btn-link show-all p-0 ms-2 align-baseline" type="button" (click)="setSection(section)"> <button class="btn btn-link show-all p-0 ms-2 align-baseline" type="button" (click)="setSection(section)">
<ng-container i18n>Show all</ng-container><i-bs class="ms-1" width="1em" height="1em" name="arrow-right"></i-bs> <ng-container i18n>Show all</ng-container><pngx-bs-icon class="ms-1" width="1em" height="1em" name="arrow-right"></pngx-bs-icon>
</button> </button>
} @else { } @else {
<ng-container i18n>{count, plural, =1 {1 task} other {{{count}} tasks}}</ng-container> <ng-container i18n>{count, plural, =1 {1 task} other {{{count}} tasks}}</ng-container>
@@ -180,7 +180,7 @@
@if (taskShowsSeparateTypeLabel(task)) { @if (taskShowsSeparateTypeLabel(task)) {
<span class="me-2">{{ task.task_type_display }}</span> <span class="me-2">{{ task.task_type_display }}</span>
} }
<span class="text-nowrap"><i-bs class="me-1" width="1em" height="1em" name="box-arrow-in-right"></i-bs>{{ task.trigger_source_display }}</span> <span class="text-nowrap"><pngx-bs-icon class="me-1" width="1em" height="1em" name="box-arrow-in-right"></pngx-bs-icon>{{ task.trigger_source_display }}</span>
</div> </div>
</td> </td>
<td class="d-none d-lg-table-cell created-col">{{ task.date_created | customDate:'short' }}</td> <td class="d-none d-lg-table-cell created-col">{{ task.date_created | customDate:'short' }}</td>
@@ -197,7 +197,7 @@
} }
@if (duplicateDocumentId(task)) { @if (duplicateDocumentId(task)) {
<div class="small text-warning-emphasis d-flex align-items-center gap-1 mt-1"> <div class="small text-warning-emphasis d-flex align-items-center gap-1 mt-1">
<i-bs class="lh-1" width="1em" height="1em" name="exclamation-triangle"></i-bs> <pngx-bs-icon class="lh-1" width="1em" height="1em" name="exclamation-triangle"></pngx-bs-icon>
<span>{{ duplicateTaskLabel(task) }}</span> <span>{{ duplicateTaskLabel(task) }}</span>
</div> </div>
} }
@@ -213,18 +213,18 @@
} }
<td class="d-lg-none"> <td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();" aria-label="View task details" i18n-aria-label> <button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();" aria-label="View task details" i18n-aria-label>
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs> <pngx-bs-icon width="1.2em" height="1.2em" name="info-circle"></pngx-bs-icon>
</button> </button>
</td> </td>
<td class="actions-col"> <td class="actions-col">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }"> <button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container> <pngx-bs-icon name="check" class="me-1"></pngx-bs-icon><ng-container i18n>Dismiss</ng-container>
</button> </button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document_ids?.[0]) { @if (task.related_document_ids?.[0]) {
<a class="btn btn-sm btn-outline-primary" [routerLink]="['/documents', task.related_document_ids[0]]" (click)="dismissTask(task)"> <a class="btn btn-sm btn-outline-primary" [routerLink]="['/documents', task.related_document_ids[0]]" (click)="dismissTask(task)">
<i-bs name="file-text" class="me-1"></i-bs><ng-container i18n>Open Document</ng-container> <pngx-bs-icon name="file-text" class="me-1"></pngx-bs-icon><ng-container i18n>Open Document</ng-container>
</a> </a>
} }
</ng-container> </ng-container>
@@ -10,7 +10,6 @@ import { By } from '@angular/platform-browser'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
import { import {
@@ -174,7 +173,6 @@ describe('TasksComponent', () => {
imports: [ imports: [
NgbModule, NgbModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
NgxBootstrapIconsModule.pick(allIcons),
FormsModule, FormsModule,
TasksComponent, TasksComponent,
PageHeaderComponent, PageHeaderComponent,
@@ -9,7 +9,6 @@ import {
NgbPaginationModule, NgbPaginationModule,
NgbPopoverModule, NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { import {
debounceTime, debounceTime,
distinctUntilChanged, distinctUntilChanged,
@@ -22,6 +21,7 @@ import {
takeUntil, takeUntil,
timer, timer,
} from 'rxjs' } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { import {
PaperlessTask, PaperlessTask,
PaperlessTaskStatus, PaperlessTaskStatus,
@@ -151,7 +151,7 @@ const TRIGGER_SOURCE_OPTIONS: Array<{
NgbDropdownModule, NgbDropdownModule,
NgbPaginationModule, NgbPaginationModule,
NgbPopoverModule, NgbPopoverModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
], ],
}) })
export class TasksComponent export class TasksComponent
@@ -5,16 +5,16 @@
i18n-info i18n-info
infoLink="usage/#document-trash"> infoLink="usage/#document-trash">
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments().size === 0"> <button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments().size === 0">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container> <pngx-bs-icon name="x" class="me-1"></pngx-bs-icon><ng-container i18n>Clear selection</ng-container>
</button> </button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments())" [disabled]="selectedDocuments().size === 0"> <button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments())" [disabled]="selectedDocuments().size === 0">
<i-bs name="arrow-counterclockwise" class="me-1"></i-bs><ng-container i18n>Restore selected</ng-container> <pngx-bs-icon name="arrow-counterclockwise" class="me-1"></pngx-bs-icon><ng-container i18n>Restore selected</ng-container>
</button> </button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments())" [disabled]="selectedDocuments().size === 0"> <button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments())" [disabled]="selectedDocuments().size === 0">
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Delete selected</ng-container> <pngx-bs-icon name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete selected</ng-container>
</button> </button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash().length === 0"> <button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash().length === 0">
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Empty trash</ng-container> <pngx-bs-icon name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Empty trash</ng-container>
</button> </button>
</pngx-page-header> </pngx-page-header>
@@ -59,7 +59,7 @@
<td scope="row"> <td scope="row">
{{ document.title }} {{ document.title }}
<pngx-preview-popup [document]="document" linkClasses="btn btn-sm btn-link" #popupPreview> <pngx-preview-popup [document]="document" linkClasses="btn btn-sm btn-link" #popupPreview>
<i-bs name="eye"></i-bs> <pngx-bs-icon name="eye"></pngx-bs-icon>
</pngx-preview-popup> </pngx-preview-popup>
</td> </td>
<td scope="row" class="d-none d-sm-table-cell" i18n>{{ getDaysRemaining(document) }} days</td> <td scope="row" class="d-none d-sm-table-cell" i18n>{{ getDaysRemaining(document) }} days</td>
@@ -67,7 +67,7 @@
<div class="btn-group d-block d-sm-none"> <div class="btn-group d-block d-sm-none">
<div ngbDropdown container="body" class="d-inline-block"> <div ngbDropdown container="body" class="d-inline-block">
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle aria-label="Actions" i18n-aria-label> <button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle aria-label="Actions" i18n-aria-label>
<i-bs name="three-dots-vertical"></i-bs> <pngx-bs-icon name="three-dots-vertical"></pngx-bs-icon>
</button> </button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile"> <div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="restore(document)" ngbDropdownItem i18n>Restore</button> <button (click)="restore(document)" ngbDropdownItem i18n>Restore</button>
@@ -77,10 +77,10 @@
</div> </div>
<div class="btn-group d-none d-sm-block"> <div class="btn-group d-none d-sm-block">
<button class="btn btn-sm btn-outline-secondary" (click)="restore(document); $event.stopPropagation();"> <button class="btn btn-sm btn-outline-secondary" (click)="restore(document); $event.stopPropagation();">
<i-bs width="1em" height="1em" name="arrow-counterclockwise" class="me-1"></i-bs><ng-container i18n>Restore</ng-container> <pngx-bs-icon width="1em" height="1em" name="arrow-counterclockwise" class="me-1"></pngx-bs-icon><ng-container i18n>Restore</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-danger" (click)="delete(document); $event.stopPropagation();"> <button class="btn btn-sm btn-outline-danger" (click)="delete(document); $event.stopPropagation();">
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container> <pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</td> </td>
@@ -9,7 +9,6 @@ import {
NgbPaginationModule, NgbPaginationModule,
NgbPopoverModule, NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { TrashService } from 'src/app/services/trash.service' import { TrashService } from 'src/app/services/trash.service'
@@ -48,7 +47,6 @@ describe('TrashComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbPopoverModule, NgbPopoverModule,
NgbPaginationModule, NgbPaginationModule,
NgxBootstrapIconsModule.pick(allIcons),
TrashComponent, TrashComponent,
PageHeaderComponent, PageHeaderComponent,
ConfirmDialogComponent, ConfirmDialogComponent,
@@ -6,8 +6,8 @@ import {
NgbModal, NgbModal,
NgbPaginationModule, NgbPaginationModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil, tap } from 'rxjs' import { takeUntil, tap } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
@@ -29,7 +29,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
ReactiveFormsModule, ReactiveFormsModule,
NgbDropdownModule, NgbDropdownModule,
NgbPaginationModule, NgbPaginationModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
], ],
}) })
export class TrashComponent export class TrashComponent
@@ -11,7 +11,7 @@
<h4 class="d-flex"> <h4 class="d-flex">
<ng-container i18n>Users</ng-container> <ng-container i18n>Users</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add User</ng-container> <pngx-bs-icon name="plus-circle" class="me-1"></pngx-bs-icon><ng-container i18n>Add User</ng-container>
</button> </button>
</h4> </h4>
<ul class="list-group"> <ul class="list-group">
@@ -32,10 +32,10 @@
<div class="col"> <div class="col">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }"> <button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container> <pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><ng-container i18n>Edit</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }"> <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container> <pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</div> </div>
@@ -49,7 +49,7 @@
<h4 class="mt-4 d-flex"> <h4 class="mt-4 d-flex">
<ng-container i18n>Groups</ng-container> <ng-container i18n>Groups</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }"> <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add Group</ng-container> <pngx-bs-icon name="plus-circle" class="me-1"></pngx-bs-icon><ng-container i18n>Add Group</ng-container>
</button> </button>
</h4> </h4>
<ul class="list-group"> <ul class="list-group">
@@ -70,10 +70,10 @@
<div class="col"> <div class="col">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }"> <button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container> <pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><ng-container i18n>Edit</ng-container>
</button> </button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }"> <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container> <pngx-bs-icon width="1em" height="1em" name="trash" class="me-1"></pngx-bs-icon><ng-container i18n>Delete</ng-container>
</button> </button>
</div> </div>
</div> </div>
@@ -3,7 +3,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { Group } from 'src/app/data/group' import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
@@ -41,7 +40,6 @@ describe('UsersAndGroupsComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [NgxBootstrapIconsModule.pick(allIcons)],
providers: [ providers: [
CustomDatePipe, CustomDatePipe,
DatePipe, DatePipe,
@@ -1,7 +1,7 @@
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core' import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, first, takeUntil } from 'rxjs' import { Subject, first, takeUntil } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Group } from 'src/app/data/group' import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@@ -29,7 +29,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
imports: [ imports: [
PageHeaderComponent, PageHeaderComponent,
IfPermissionsDirective, IfPermissionsDirective,
NgxBootstrapIconsModule, BootstrapIconComponent,
], ],
}) })
export class UsersAndGroupsComponent export class UsersAndGroupsComponent
@@ -2,7 +2,7 @@
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse" <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" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
(click)="closeMobileSearch(); toggleMenuCollapsed()"> (click)="closeMobileSearch(); toggleMenuCollapsed()">
<i-bs width="1.5em" height="1.5em" name="list"></i-bs> <pngx-bs-icon width="1.5em" height="1.5em" name="list"></pngx-bs-icon>
</button> </button>
<div class="navbar-brand-container order-sm-0"> <div class="navbar-brand-container order-sm-0">
<a class="navbar-brand d-flex align-items-center me-0 ps-md-3 py-0" <a class="navbar-brand d-flex align-items-center me-0 ps-md-3 py-0"
@@ -34,7 +34,7 @@
<div *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }" class="btn-group header-upload-group flex-shrink-0"> <div *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Document }" class="btn-group header-upload-group flex-shrink-0">
<button class="btn btn-sm header-upload border-0 d-flex align-items-center justify-content-center py-0 px-2" type="button" <button class="btn btn-sm header-upload border-0 d-flex align-items-center justify-content-center py-0 px-2" type="button"
(click)="fileUpload.click()" aria-label="Upload documents" i18n-aria-label> (click)="fileUpload.click()" aria-label="Upload documents" i18n-aria-label>
<i-bs class="d-flex" width="1.1em" height="1.1em" name="file-earmark-arrow-up"></i-bs><span class="ms-2 d-sm-none d-md-inline" i18n>Upload</span> <pngx-bs-icon class="d-flex" width="1.1em" height="1.1em" name="file-earmark-arrow-up"></pngx-bs-icon><span class="ms-2 d-sm-none d-md-inline" i18n>Upload</span>
</button> </button>
@if (uploadsInProgress > 0) { @if (uploadsInProgress > 0) {
<button class="btn btn-sm header-upload border-0 d-flex align-items-center py-0 px-2" type="button" <button class="btn btn-sm header-upload border-0 d-flex align-items-center py-0 px-2" type="button"
@@ -52,7 +52,7 @@
<pngx-toasts-dropdown></pngx-toasts-dropdown> <pngx-toasts-dropdown></pngx-toasts-dropdown>
<li ngbDropdown class="nav-item dropdown"> <li ngbDropdown class="nav-item dropdown">
<button class="btn navbar-action border-0 me-1 me-md-2" id="userDropdown" ngbDropdownToggle aria-label="User menu" i18n-aria-label> <button class="btn navbar-action border-0 me-1 me-md-2" id="userDropdown" ngbDropdownToggle aria-label="User menu" i18n-aria-label>
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs> <pngx-bs-icon width="1.3em" height="1.3em" name="person-circle"></pngx-bs-icon>
<span class="small ms-2 d-none d-sm-inline"> <span class="small ms-2 d-none d-sm-inline">
{{this.settingsService.displayName}} {{this.settingsService.displayName}}
</span> </span>
@@ -63,19 +63,19 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
</div> </div>
<button ngbDropdownItem class="nav-link" (click)="editProfile()"> <button ngbDropdownItem class="nav-link" (click)="editProfile()">
<i-bs class="me-2" name="person"></i-bs><ng-container i18n>My Profile</ng-container> <pngx-bs-icon class="me-2" name="person"></pngx-bs-icon><ng-container i18n>My Profile</ng-container>
</button> </button>
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()" <a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()"
*pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }"> *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }">
<i-bs class="me-2" name="gear"></i-bs><ng-container i18n>Settings</ng-container> <pngx-bs-icon class="me-2" name="gear"></pngx-bs-icon><ng-container i18n>Settings</ng-container>
</a> </a>
<a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()"> <a ngbDropdownItem class="nav-link d-flex" href="accounts/logout/" (click)="onLogout()">
<i-bs class="me-2" name="door-open"></i-bs><ng-container i18n>Logout</ng-container> <pngx-bs-icon class="me-2" name="door-open"></pngx-bs-icon><ng-container i18n>Logout</ng-container>
</a> </a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer" <a ngbDropdownItem class="nav-link" target="_blank" rel="noopener noreferrer"
href="https://docs.paperless-ngx.com"> href="https://docs.paperless-ngx.com">
<i-bs class="me-2" name="question-circle"></i-bs><ng-container i18n>Documentation</ng-container> <pngx-bs-icon class="me-2" name="question-circle"></pngx-bs-icon><ng-container i18n>Documentation</ng-container>
</a> </a>
</div> </div>
</li> </li>
@@ -93,9 +93,9 @@
@if (canSaveSettings) { @if (canSaveSettings) {
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()" [aria-label]="slimSidebarEnabled ? 'Expand sidebar' : 'Collapse sidebar'" i18n-aria-label> <button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()" [aria-label]="slimSidebarEnabled ? 'Expand sidebar' : 'Collapse sidebar'" i18n-aria-label>
@if (slimSidebarEnabled) { @if (slimSidebarEnabled) {
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs> <pngx-bs-icon width="0.9em" height="0.9em" name="chevron-double-right"></pngx-bs-icon>
} @else { } @else {
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs> <pngx-bs-icon width="0.9em" height="0.9em" name="chevron-double-left"></pngx-bs-icon>
} }
</button> </button>
} }
@@ -105,7 +105,7 @@
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Dashboard)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Dashboard)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="house"></i-bs><span class="nav-link-label"><ng-container i18n>Dashboard</ng-container></span> <pngx-bs-icon class="me-2" name="house"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Dashboard</ng-container></span>
</a> </a>
@if (settingsService.organizingSidebarItems()) { @if (settingsService.organizingSidebarItems()) {
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Dashboard" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Dashboard)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Dashboard, $event)"></pngx-input-switch> <pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Dashboard" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Dashboard)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Dashboard, $event)"></pngx-input-switch>
@@ -117,7 +117,7 @@
(click)="closeMenu()" (click)="closeMenu()"
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="files"></i-bs><span class="nav-link-label"><ng-container i18n>Documents</ng-container></span> <pngx-bs-icon class="me-2" name="files"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Documents</ng-container></span>
</a> </a>
</li> </li>
</ul> </ul>
@@ -136,7 +136,7 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim"> popoverClass="popover-slim">
<i-bs class="me-2" [name]="view.icon || 'funnel'"></i-bs><span><div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div> <pngx-bs-icon class="me-2" [name]="view.icon || 'funnel'"></pngx-bs-icon><span><div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div>
@if (showSidebarCounts && !slimSidebarEnabled) { @if (showSidebarCounts && !slimSidebarEnabled) {
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span> <span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
} }
@@ -147,7 +147,7 @@
</a> </a>
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) { @if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
<div class="position-absolute end-0 top-0 px-1 py-2" [class.me-n2]="slimSidebarEnabled" cdkDragHandle> <div class="position-absolute end-0 top-0 px-1 py-2" [class.me-n2]="slimSidebarEnabled" cdkDragHandle>
<i-bs name="grip-vertical"></i-bs> <pngx-bs-icon name="grip-vertical"></pngx-bs-icon>
</div> </div>
} }
</li> </li>
@@ -174,10 +174,10 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim"> popoverClass="popover-slim">
<i-bs class="me-2" name="file-text"></i-bs><span>{{d.title | documentTitle}}</span> <pngx-bs-icon class="me-2" name="file-text"></pngx-bs-icon><span>{{d.title | documentTitle}}</span>
<span class="close flex-column justify-content-center" <span class="close flex-column justify-content-center"
(click)="$event.preventDefault(); $event.stopPropagation(); closeDocument(d)"> (click)="$event.preventDefault(); $event.stopPropagation(); closeDocument(d)">
<i-bs name="x"></i-bs> <pngx-bs-icon name="x"></pngx-bs-icon>
</span> </span>
</a> </a>
</li> </li>
@@ -188,7 +188,7 @@
[class.text-truncate]="!slimSidebarEnabled" (click)="closeAll()" [class.text-truncate]="!slimSidebarEnabled" (click)="closeAll()"
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="x"></i-bs><span class="nav-link-label"><ng-container i18n>Close all</ng-container></span> <pngx-bs-icon class="me-2" name="x"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Close all</ng-container></span>
</button> </button>
</li> </li>
} }
@@ -207,7 +207,7 @@
[routerLinkActiveOptions]="{ exact: !(slimSidebarEnabled || attributesSectionsCollapsed) }" (click)="closeMenu()" [routerLinkActiveOptions]="{ exact: !(slimSidebarEnabled || attributesSectionsCollapsed) }" (click)="closeMenu()"
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs name="stack"></i-bs><span class="nav-link-label ms-2"><ng-container i18n>Attributes</ng-container></span> <pngx-bs-icon name="stack"></pngx-bs-icon><span class="nav-link-label ms-2"><ng-container i18n>Attributes</ng-container></span>
</a> </a>
@if (!slimSidebarEnabled && canSaveSettings) { @if (!slimSidebarEnabled && canSaveSettings) {
<button <button
@@ -217,7 +217,7 @@
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'" [attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
i18n-aria-label i18n-aria-label
> >
<i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs> <pngx-bs-icon [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></pngx-bs-icon>
</button> </button>
} }
</div> </div>
@@ -228,27 +228,27 @@
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
<a class="nav-link" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()"> <a class="nav-link" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="tags"></i-bs><span class="nav-link-label"><ng-container i18n>Tags</ng-container></span> <pngx-bs-icon class="me-2" name="tags"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Tags</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a class="nav-link" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()"> <a class="nav-link" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="person"></i-bs><span class="nav-link-label"><ng-container i18n>Correspondents</ng-container></span> <pngx-bs-icon class="me-2" name="person"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Correspondents</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a class="nav-link" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()"> <a class="nav-link" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="hash"></i-bs><span class="nav-link-label"><ng-container i18n>Document types</ng-container></span> <pngx-bs-icon class="me-2" name="hash"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Document types</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()"> <a class="nav-link" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="folder"></i-bs><span class="nav-link-label"><ng-container i18n>Storage paths</ng-container></span> <pngx-bs-icon class="me-2" name="folder"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Storage paths</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()"> <a class="nav-link" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="ui-radios"></i-bs><span class="nav-link-label"><ng-container i18n>Custom fields</ng-container></span> <pngx-bs-icon class="me-2" name="ui-radios"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Custom fields</ng-container></span>
</a> </a>
</li> </li>
</ul> </ul>
@@ -259,7 +259,7 @@
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.SavedViews)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.SavedViews)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="window-stack"></i-bs><span class="nav-link-label"><ng-container i18n>Saved Views</ng-container></span> <pngx-bs-icon class="me-2" name="window-stack"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Saved Views</ng-container></span>
</a> </a>
@if (settingsService.organizingSidebarItems()) { @if (settingsService.organizingSidebarItems()) {
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Saved Views" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.SavedViews)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.SavedViews, $event)"></pngx-input-switch> <pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Saved Views" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.SavedViews)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.SavedViews, $event)"></pngx-input-switch>
@@ -270,7 +270,7 @@
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.ShareLinks)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="share-links" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.ShareLinks)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="share-links" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Share links" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Share links" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="link"></i-bs><span class="nav-link-label"><ng-container i18n>Share links</ng-container></span> <pngx-bs-icon class="me-2" name="link"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Share links</ng-container></span>
</a> </a>
@if (settingsService.organizingSidebarItems()) { @if (settingsService.organizingSidebarItems()) {
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Share Links" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.ShareLinks)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.ShareLinks, $event)"></pngx-input-switch> <pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Share Links" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.ShareLinks)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.ShareLinks, $event)"></pngx-input-switch>
@@ -283,7 +283,7 @@
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Workflows)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Workflows)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="boxes"></i-bs><span class="nav-link-label"><ng-container i18n>Workflows</ng-container></span> <pngx-bs-icon class="me-2" name="boxes"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Workflows</ng-container></span>
</a> </a>
@if (settingsService.organizingSidebarItems()) { @if (settingsService.organizingSidebarItems()) {
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Workflows" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Workflows)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Workflows, $event)"></pngx-input-switch> <pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Workflows" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Workflows)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Workflows, $event)"></pngx-input-switch>
@@ -294,7 +294,7 @@
<a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Mail)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail" <a class="nav-link" [class.opacity-50]="settingsService.sidebarItemIsHidden(HideableSidebarItemID.Mail)" [class.pe-5]="settingsService.organizingSidebarItems() && !slimSidebarEnabled && !slimSidebarAnimating()" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim"> triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="envelope"></i-bs><span class="nav-link-label"><ng-container i18n>Mail</ng-container></span> <pngx-bs-icon class="me-2" name="envelope"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Mail</ng-container></span>
</a> </a>
@if (settingsService.organizingSidebarItems()) { @if (settingsService.organizingSidebarItems()) {
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Mail" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Mail)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Mail, $event)"></pngx-input-switch> <pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Mail" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Mail)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Mail, $event)"></pngx-input-switch>
@@ -304,7 +304,7 @@
<a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash" <a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash"
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim"> triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="trash"></i-bs><span class="nav-link-label"><ng-container i18n>Trash</ng-container></span> <pngx-bs-icon class="me-2" name="trash"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Trash</ng-container></span>
</a> </a>
</li> </li>
</ul> </ul>
@@ -320,21 +320,21 @@
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="gear"></i-bs><span class="nav-link-label"><ng-container i18n>Settings</ng-container></span> <pngx-bs-icon class="me-2" name="gear"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Settings</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="sliders2-vertical"></i-bs><span class="nav-link-label"><ng-container i18n>Configuration</ng-container></span> <pngx-bs-icon class="me-2" name="sliders2-vertical"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Configuration</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }"> <li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="people"></i-bs><span class="nav-link-label"><ng-container i18n>Users & Groups</ng-container></span> <pngx-bs-icon class="me-2" name="people"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Users & Groups</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item app-link" <li class="nav-item app-link"
@@ -343,7 +343,7 @@
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" <a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" ngbPopover="Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="list-task"></i-bs><span class="nav-link-label"><ng-container i18n>Tasks</ng-container></span> <pngx-bs-icon class="me-2" name="list-task"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Tasks</ng-container></span>
@if (tasksService.needsAttentionTasksCount > 0 && !slimSidebarEnabled) { @if (tasksService.needsAttentionTasksCount > 0 && !slimSidebarEnabled) {
<span class="badge bg-danger ms-2 d-inline flex-shrink-0">{{tasksService.needsAttentionTasksCount}}</span> <span class="badge bg-danger ms-2 d-inline flex-shrink-0">{{tasksService.needsAttentionTasksCount}}</span>
} }
@@ -357,7 +357,7 @@
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" <a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim"> triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="text-left"></i-bs><span class="nav-link-label"><ng-container i18n>Logs</ng-container></span> <pngx-bs-icon class="me-2" name="text-left"></pngx-bs-icon><span class="nav-link-label"><ng-container i18n>Logs</ng-container></span>
</a> </a>
</li> </li>
} }
@@ -366,7 +366,7 @@
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation" target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body" i18n-ngbPopover [disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim"> triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="d-flex me-2" name="question-circle"></i-bs><span><ng-container i18n>Documentation</ng-container></span> <pngx-bs-icon class="d-flex me-2" name="question-circle"></pngx-bs-icon><span><ng-container i18n>Documentation</ng-container></span>
</a> </a>
@if (settingsService.organizingSidebarItems()) { @if (settingsService.organizingSidebarItems()) {
<pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Documentation" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Documentation)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Documentation, $event)"></pngx-input-switch> <pngx-input-switch class="position-absolute top-50 end-0 translate-middle-y me-1" [class.d-none]="slimSidebarEnabled || slimSidebarAnimating()" [compact]="true" title="Documentation" i18n-title [ngModel]="!settingsService.sidebarItemIsHidden(HideableSidebarItemID.Documentation)" (ngModelChange)="toggleSidebarItem(HideableSidebarItemID.Documentation, $event)"></pngx-input-switch>
@@ -408,7 +408,7 @@
href="https://github.com/paperless-ngx/paperless-ngx/releases" href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
container="body"> container="body">
<i-bs width="1.2em" height="1.2em" name="info-circle" class="me-1"></i-bs> <pngx-bs-icon width="1.2em" height="1.2em" name="info-circle" class="me-1"></pngx-bs-icon>
@if (appRemoteVersion()?.update_available) { @if (appRemoteVersion()?.update_available) {
<ng-container i18n>Update available</ng-container> <ng-container i18n>Update available</ng-container>
} }
@@ -418,7 +418,7 @@
<a *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" class="small text-decoration-none" routerLink="/settings" fragment="update-checking" aria-label="Configure update checking" i18n-aria-label <a *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.UISettings }" class="small text-decoration-none" routerLink="/settings" fragment="update-checking" aria-label="Configure update checking" i18n-aria-label
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter" [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter"
container="body"> container="body">
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs> <pngx-bs-icon width="1.2em" height="1.2em" name="info-circle"></pngx-bs-icon>
</a> </a>
} }
</div> </div>
@@ -142,7 +142,7 @@ main {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
i-bs { pngx-bs-icon {
margin-right: 0 !important; margin-right: 0 !important;
} }
} }
@@ -268,7 +268,7 @@ main {
} }
} }
i-bs { pngx-bs-icon {
position: relative; position: relative;
top: -1px; top: -1px;
} }
@@ -279,7 +279,7 @@ main {
align-items: center; align-items: center;
min-width: 0; min-width: 0;
> i-bs { > pngx-bs-icon {
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -373,11 +373,11 @@ main {
width: 1.8rem; width: 1.8rem;
height: 100%; height: 100%;
i-bs { pngx-bs-icon {
opacity: 0.5; opacity: 0.5;
} }
&:hover i-bs { &:hover pngx-bs-icon {
opacity: 1; opacity: 1;
} }
} }
@@ -392,7 +392,7 @@ main {
text-decoration: underline; text-decoration: underline;
} }
i-bs { pngx-bs-icon {
margin-bottom: 2px; margin-bottom: 2px;
} }
} }
@@ -665,7 +665,7 @@ main {
.navbar .dropdown-menu { .navbar .dropdown-menu {
font-size: 0.875rem; // body size font-size: 0.875rem; // body size
a i-bs, button i-bs { a pngx-bs-icon, button pngx-bs-icon {
opacity: 0.6; opacity: 0.6;
} }
} }
@@ -693,7 +693,7 @@ main {
cursor: move; cursor: move;
} }
::ng-deep .navItemDrag .position-absolute i-bs { ::ng-deep .navItemDrag .position-absolute pngx-bs-icon {
display: none; display: none;
} }
@@ -10,7 +10,6 @@ import { BrowserModule, By } from '@angular/platform-browser'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, NgbModalModule, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap' import { provideUiTour } from 'ngx-ui-tour-ng-bootstrap'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
@@ -108,7 +107,6 @@ describe('AppFrameComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
DragDropModule, DragDropModule,
NgbModalModule, NgbModalModule,
NgxBootstrapIconsModule.pick(allIcons),
AppFrameComponent, AppFrameComponent,
IfPermissionsDirective, IfPermissionsDirective,
GlobalSearchComponent, GlobalSearchComponent,
@@ -23,10 +23,10 @@ import {
NgbNavModule, NgbNavModule,
NgbPopoverModule, NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrap } from 'ngx-ui-tour-ng-bootstrap'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { first } from 'rxjs/operators' import { first } from 'rxjs/operators'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { import {
@@ -89,7 +89,7 @@ const SCROLL_THRESHOLD = 16
NgbPopoverModule, NgbPopoverModule,
NgbCollapseModule, NgbCollapseModule,
NgbNavModule, NgbNavModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
DragDropModule, DragDropModule,
TourNgBootstrap, TourNgBootstrap,
FormsModule, FormsModule,
@@ -1,7 +1,7 @@
<div ngbDropdown #resultsDropdown="ngbDropdown" (openChange)="onDropdownOpenChange"> <div ngbDropdown #resultsDropdown="ngbDropdown" (openChange)="onDropdownOpenChange">
<form class="form-inline position-relative"> <form class="form-inline position-relative">
<i-bs width="1em" height="1em" name="search"></i-bs> <pngx-bs-icon width="1em" height="1em" name="search"></pngx-bs-icon>
<div class="input-group"> <div class="input-group">
<div class="form-control form-control-sm"> <div class="form-control form-control-sm">
<input class="bg-transparent border-0 w-100 h-100" #searchInput type="text" name="query" <input class="bg-transparent border-0 w-100 h-100" #searchInput type="text" name="query"
@@ -25,7 +25,7 @@
} @else { } @else {
<ng-container i18n>Search</ng-container> <ng-container i18n>Search</ng-container>
} }
<i-bs width="1em" height="1em" name="arrow-right-short"></i-bs> <pngx-bs-icon width="1em" height="1em" name="arrow-right-short"></pngx-bs-icon>
</button> </button>
} }
</div> </div>
@@ -35,7 +35,7 @@
<div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1" <div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1"
(click)="primaryAction(type, item, $event)" (click)="primaryAction(type, item, $event)"
(mouseenter)="onItemHover($event)"> (mouseenter)="onItemHover($event)">
<i-bs width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></i-bs> <pngx-bs-icon width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></pngx-bs-icon>
<div class="text-truncate"> <div class="text-truncate">
{{item[nameProp]}} {{item[nameProp]}}
@if (date) { @if (date) {
@@ -49,13 +49,13 @@
[disabled]="disablePrimaryButton(type, item)" [disabled]="disablePrimaryButton(type, item)"
(mouseenter)="onButtonHover($event)"> (mouseenter)="onButtonHover($event)">
@if (type === DataType.Document) { @if (type === DataType.Document) {
<i-bs width="1em" height="1em" name="file-earmark-richtext" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span> <pngx-bs-icon width="1em" height="1em" name="file-earmark-richtext" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span>
} @else if (type === DataType.SavedView) { } @else if (type === DataType.SavedView) {
<i-bs width="1em" height="1em" name="eye" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span> <pngx-bs-icon width="1em" height="1em" name="eye" class="me-1"></pngx-bs-icon><span><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) { } @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" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span> <pngx-bs-icon width="1em" height="1em" name="pencil" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span>
} @else { } @else {
<i-bs width="1em" height="1em" name="filter" class="me-1"></i-bs><span><ng-container i18n>Filter documents</ng-container></span> <pngx-bs-icon width="1em" height="1em" name="filter" class="me-1"></pngx-bs-icon><span><ng-container i18n>Filter documents</ng-container></span>
} }
</button> </button>
@if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) { @if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) {
@@ -65,9 +65,9 @@
[disabled]="disableSecondaryButton(type, item)" [disabled]="disableSecondaryButton(type, item)"
(mouseenter)="onButtonHover($event)"> (mouseenter)="onButtonHover($event)">
@if (type === DataType.Document) { @if (type === DataType.Document) {
<i-bs width="1em" height="1em" name="download" class="me-1"></i-bs><span><ng-container i18n>Download</ng-container></span> <pngx-bs-icon width="1em" height="1em" name="download" class="me-1"></pngx-bs-icon><span><ng-container i18n>Download</ng-container></span>
} @else { } @else {
<i-bs width="1em" height="1em" name="file-earmark-richtext" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span> <pngx-bs-icon width="1em" height="1em" name="file-earmark-richtext" class="me-1"></pngx-bs-icon><span><ng-container i18n>Open</ng-container></span>
} }
</button> </button>
} }
@@ -1,7 +1,7 @@
form { form {
position: relative; position: relative;
> i-bs[name="search"] { > pngx-bs-icon[name="search"] {
position: absolute; position: absolute;
left: 0.6rem; left: 0.6rem;
top: .25rem; top: .25rem;
@@ -14,7 +14,7 @@ form {
} }
&:focus-within { &:focus-within {
i-bs[name="search"], pngx-bs-icon[name="search"],
.badge { .badge {
display: none !important; display: none !important;
} }
@@ -72,7 +72,7 @@ form {
.dropdown.show { .dropdown.show {
// Hide the search icon when the dropdown is shown // Hide the search icon when the dropdown is shown
i-bs[name="search"] { pngx-bs-icon[name="search"] {
display: none !important; display: none !important;
} }
@@ -10,7 +10,6 @@ import {
NgbModalModule, NgbModalModule,
NgbModalRef, NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs' import { of } from 'rxjs'
import { DataType } from 'src/app/data/datatype' import { DataType } from 'src/app/data/datatype'
import { import {
@@ -138,7 +137,6 @@ describe('GlobalSearchComponent', () => {
NgbDropdownModule, NgbDropdownModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons),
GlobalSearchComponent, GlobalSearchComponent,
], ],
providers: [ providers: [
@@ -17,8 +17,8 @@ import {
NgbModal, NgbModal,
NgbModalRef, NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, debounceTime, distinctUntilChanged, filter } from 'rxjs' import { Subject, debounceTime, distinctUntilChanged, filter } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { DataType } from 'src/app/data/datatype' import { DataType } from 'src/app/data/datatype'
import { import {
FILTER_FULLTEXT_QUERY, FILTER_FULLTEXT_QUERY,
@@ -65,7 +65,7 @@ import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-e
CustomDatePipe, CustomDatePipe,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
NgbDropdownModule, NgbDropdownModule,
NgTemplateOutlet, NgTemplateOutlet,
], ],
@@ -4,7 +4,7 @@
<span class="notification-count badge rounded-pill z-3 pe-none position-absolute" [class.bg-danger]="hasUnreadError()" [class.bg-secondary]="!hasUnreadError()">{{ unread().length }}</span> <span class="notification-count badge rounded-pill z-3 pe-none position-absolute" [class.bg-danger]="hasUnreadError()" [class.bg-secondary]="!hasUnreadError()">{{ unread().length }}</span>
} }
<button class="btn navbar-action border-0" id="notificationsDropdown" ngbDropdownToggle aria-label="Notifications" i18n-aria-label> <button class="btn navbar-action border-0" id="notificationsDropdown" ngbDropdownToggle aria-label="Notifications" i18n-aria-label>
<i-bs width="1.3em" height="1.3em" name="bell"></i-bs> <pngx-bs-icon width="1.3em" height="1.3em" name="bell"></pngx-bs-icon>
</button> </button>
<div ngbDropdownMenu class="dropdown-menu-end shadow p-2" aria-labelledby="notificationsDropdown"> <div ngbDropdownMenu class="dropdown-menu-end shadow p-2" aria-labelledby="notificationsDropdown">
<div class="d-flex align-items-center px-1 mb-2"> <div class="d-flex align-items-center px-1 mb-2">
@@ -12,13 +12,13 @@
@if (toasts().length) { @if (toasts().length) {
<button class="btn btn-link btn-sm p-0 ms-auto text-body-secondary text-decoration-none clear-all" <button class="btn btn-link btn-sm p-0 ms-auto text-body-secondary text-decoration-none clear-all"
(click)="toastService.clearToasts()"> (click)="toastService.clearToasts()">
<i-bs width="1em" height="1em" name="check2-all" class="me-1"></i-bs><ng-container i18n>Clear All</ng-container> <pngx-bs-icon width="1em" height="1em" name="check2-all" class="me-1"></pngx-bs-icon><ng-container i18n>Clear All</ng-container>
</button> </button>
} }
</div> </div>
@if (toasts().length === 0) { @if (toasts().length === 0) {
<div class="text-center text-body-secondary small py-4"> <div class="text-center text-body-secondary small py-4">
<i-bs width="1.5em" height="1.5em" name="bell" class="d-block mx-auto mb-2 opacity-50"></i-bs> <pngx-bs-icon width="1.5em" height="1.5em" name="bell" class="d-block mx-auto mb-2 opacity-50"></pngx-bs-icon>
<ng-container i18n>No notifications</ng-container> <ng-container i18n>No notifications</ng-container>
</div> </div>
} }
@@ -1,7 +1,6 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service' import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service'
import { ToastsDropdownComponent } from './toasts-dropdown.component' import { ToastsDropdownComponent } from './toasts-dropdown.component'
@@ -41,10 +40,7 @@ describe('ToastsDropdownComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [ToastsDropdownComponent],
ToastsDropdownComponent,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
@@ -12,8 +12,8 @@ import {
NgbDropdownModule, NgbDropdownModule,
NgbProgressbarModule, NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subscription, interval, map } from 'rxjs' import { Subscription, interval, map } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service' import { Toast, ToastLevel, ToastService } from 'src/app/services/toast.service'
import { ToastComponent } from '../../common/toast/toast.component' import { ToastComponent } from '../../common/toast/toast.component'
@@ -25,7 +25,7 @@ import { ToastComponent } from '../../common/toast/toast.component'
ToastComponent, ToastComponent,
NgbDropdownModule, NgbDropdownModule,
NgbProgressbarModule, NgbProgressbarModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
], ],
}) })
export class ToastsDropdownComponent { export class ToastsDropdownComponent {
@@ -1,11 +1,11 @@
<li ngbDropdown class="nav-item" display="dynamic" placement="bottom-end" [popperOptions]="popperOptions" (openChange)="onOpenChange($event)"> <li ngbDropdown class="nav-item" display="dynamic" placement="bottom-end" [popperOptions]="popperOptions" (openChange)="onOpenChange($event)">
<button class="btn navbar-action border-0" id="chatDropdown" ngbDropdownToggle aria-label="Chat" i18n-aria-label> <button class="btn navbar-action border-0" id="chatDropdown" ngbDropdownToggle aria-label="Chat" i18n-aria-label>
<i-bs width="1.3em" height="1.3em" name="chatSquareDots"></i-bs> <pngx-bs-icon width="1.3em" height="1.3em" name="chatSquareDots"></pngx-bs-icon>
</button> </button>
<div ngbDropdownMenu class="dropdown-menu-end shadow p-0 overflow-hidden" aria-labelledby="chatDropdown"> <div ngbDropdownMenu class="dropdown-menu-end shadow p-0 overflow-hidden" aria-labelledby="chatDropdown">
<div class="d-flex align-items-center border-bottom px-3 py-2"> <div class="d-flex align-items-center border-bottom px-3 py-2">
<i-bs width="1em" height="1em" name="stars" class="text-primary me-2"></i-bs> <pngx-bs-icon width="1em" height="1em" name="stars" class="text-primary me-2"></pngx-bs-icon>
<span class="fw-semibold"> <span class="fw-semibold">
@if (documentId()) { @if (documentId()) {
<ng-container i18n>Ask about this document</ng-container> <ng-container i18n>Ask about this document</ng-container>
@@ -15,7 +15,7 @@
</span> </span>
@if (messages().length) { @if (messages().length) {
<button class="btn btn-sm btn-link text-muted ms-auto p-0" type="button" (click)="clearChat()" title="Clear chat" i18n-title> <button class="btn btn-sm btn-link text-muted ms-auto p-0" type="button" (click)="clearChat()" title="Clear chat" i18n-title>
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs> <pngx-bs-icon width="1em" height="1em" name="arrow-counterclockwise"></pngx-bs-icon>
</button> </button>
} }
</div> </div>
@@ -46,7 +46,7 @@
<div class="list-group-item text-muted small py-1" i18n>Sources</div> <div class="list-group-item text-muted small py-1" i18n>Sources</div>
@for (reference of message.references; track reference.id) { @for (reference of message.references; track reference.id) {
<a class="list-group-item list-group-item-action d-flex align-items-center text-primary" [routerLink]="['/documents', reference.id]" [title]="reference.title"> <a class="list-group-item list-group-item-action d-flex align-items-center text-primary" [routerLink]="['/documents', reference.id]" [title]="reference.title">
<i-bs width="0.9em" height="0.9em" name="file-text" class="me-2 flex-shrink-0"></i-bs><span class="text-truncate">{{ reference.title }}</span> <pngx-bs-icon width="0.9em" height="0.9em" name="file-text" class="me-2 flex-shrink-0"></pngx-bs-icon><span class="text-truncate">{{ reference.title }}</span>
</a> </a>
} }
</div> </div>
@@ -72,11 +72,11 @@
/> />
@if (loading()) { @if (loading()) {
<button class="btn btn-outline-secondary" type="button" (click)="stopStreaming()" title="Stop" i18n-title aria-label="Stop" i18n-aria-label> <button class="btn btn-outline-secondary" type="button" (click)="stopStreaming()" title="Stop" i18n-title aria-label="Stop" i18n-aria-label>
<i-bs width="1em" height="1em" name="x-lg"></i-bs> <pngx-bs-icon width="1em" height="1em" name="x-lg"></pngx-bs-icon>
</button> </button>
} @else { } @else {
<button class="btn btn-primary" type="button" (click)="sendMessage()" title="Send" i18n-title aria-label="Send" i18n-aria-label> <button class="btn btn-primary" type="button" (click)="sendMessage()" title="Send" i18n-title aria-label="Send" i18n-aria-label>
<i-bs width="1em" height="1em" name="arrow-return-left"></i-bs> <pngx-bs-icon width="1em" height="1em" name="arrow-return-left"></pngx-bs-icon>
</button> </button>
} }
</div> </div>
@@ -4,7 +4,6 @@ import { ElementRef } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NavigationEnd, Router } from '@angular/router' import { NavigationEnd, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { import {
CHAT_METADATA_DELIMITER, CHAT_METADATA_DELIMITER,
@@ -22,11 +21,7 @@ describe('ChatComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [RouterTestingModule, ChatComponent],
NgxBootstrapIconsModule.pick(allIcons),
RouterTestingModule,
ChatComponent,
],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
@@ -9,8 +9,8 @@ import {
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NavigationEnd, Router, RouterModule } from '@angular/router' import { NavigationEnd, Router, RouterModule } from '@angular/router'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { filter, map, Subscription } from 'rxjs' import { filter, map, Subscription } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { MarkdownPipe } from 'src/app/pipes/markdown.pipe' import { MarkdownPipe } from 'src/app/pipes/markdown.pipe'
import { import {
ChatMessage, ChatMessage,
@@ -27,7 +27,7 @@ const SLOW_RESPONSE_DELAY_MS = 8000
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule, RouterModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
NgbDropdownModule, NgbDropdownModule,
MarkdownPipe, MarkdownPipe,
], ],
@@ -0,0 +1,61 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { BootstrapIconComponent } from './bootstrap-icon.component'
describe('BootstrapIconComponent', () => {
let fixture: ComponentFixture<BootstrapIconComponent>
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BootstrapIconComponent],
})
fixture = TestBed.createComponent(BootstrapIconComponent)
})
it('should render the named icon as inline svg', () => {
fixture.componentRef.setInput('name', 'arrow-down-up')
fixture.detectChanges()
const svg: SVGElement = fixture.nativeElement.querySelector('svg')
expect(svg.classList).toContain('bi-arrow-down-up')
expect(svg.getAttribute('width')).toEqual('16')
expect(svg.getAttribute('height')).toEqual('16')
})
it('should apply width and height', () => {
fixture.componentRef.setInput('name', 'x-lg')
fixture.componentRef.setInput('width', '1.2em')
fixture.componentRef.setInput('height', 2)
fixture.detectChanges()
const svg: SVGElement = fixture.nativeElement.querySelector('svg')
expect(svg.getAttribute('width')).toEqual('1.2em')
expect(svg.getAttribute('height')).toEqual('2')
})
it('should not allow width or height to inject attributes', () => {
fixture.componentRef.setInput('name', 'x-lg')
fixture.componentRef.setInput('width', '1" onload="alert(1)')
fixture.componentRef.setInput('height', '1" onclick="alert(1)')
fixture.detectChanges()
const svg: SVGElement = fixture.nativeElement.querySelector('svg')
expect(svg.hasAttribute('onload')).toBe(false)
expect(svg.hasAttribute('onclick')).toBe(false)
expect(svg.getAttribute('width')).toEqual('1" onload="alert(1)')
})
it('should update when the name changes', () => {
fixture.componentRef.setInput('name', 'x-lg')
fixture.detectChanges()
fixture.componentRef.setInput('name', 'sliders2-vertical')
fixture.detectChanges()
expect(fixture.nativeElement.querySelector('svg').classList).toContain(
'bi-sliders2-vertical'
)
})
it('should render nothing and warn for an unregistered icon', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
fixture.componentRef.setInput('name', 'not-an-icon')
fixture.detectChanges()
expect(fixture.nativeElement.innerHTML).toEqual('')
expect(warnSpy).toHaveBeenCalledWith('Icon not found: not-an-icon')
})
})
@@ -0,0 +1,54 @@
import {
ChangeDetectionStrategy,
Component,
effect,
ElementRef,
inject,
input,
} from '@angular/core'
import { icons } from './icons'
/** Converts icon name e.g. `arrow-down-up` to its registry key. */
const toKey = (name: string) =>
name.trim().replace(/-(.)/g, (_, char: string) => char.toUpperCase())
/**
* Renders an icon from the official bootstrap-icons package as inline SVG.
* Icons must be registered in `./icons.ts`.
*/
@Component({
selector: 'pngx-bs-icon',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BootstrapIconComponent {
private readonly elementRef = inject(ElementRef<HTMLElement>)
readonly name = input<string>()
readonly width = input<string | number>()
readonly height = input<string | number>()
constructor() {
effect(() => {
const element = this.elementRef.nativeElement
element.innerHTML = this.svg()
const svg = element.firstElementChild
if (!svg) return
const width = this.width()
const height = this.height()
if (width) svg.setAttribute('width', String(width))
if (height) svg.setAttribute('height', String(height))
})
}
private svg(): string {
const name = this.name()
if (!name) return ''
const svg = icons[toKey(name)]
if (!svg) {
console.warn(`Icon not found: ${name}`)
return ''
}
return svg
}
}
@@ -0,0 +1,319 @@
import airplane from 'bootstrap-icons/icons/airplane.svg'
import archive from 'bootstrap-icons/icons/archive.svg'
import arrowClockwise from 'bootstrap-icons/icons/arrow-clockwise.svg'
import arrowCounterclockwise from 'bootstrap-icons/icons/arrow-counterclockwise.svg'
import arrowDownUp from 'bootstrap-icons/icons/arrow-down-up.svg'
import arrowDown from 'bootstrap-icons/icons/arrow-down.svg'
import arrowLeft from 'bootstrap-icons/icons/arrow-left.svg'
import arrowRepeat from 'bootstrap-icons/icons/arrow-repeat.svg'
import arrowReturnLeft from 'bootstrap-icons/icons/arrow-return-left.svg'
import arrowRightShort from 'bootstrap-icons/icons/arrow-right-short.svg'
import arrowRight from 'bootstrap-icons/icons/arrow-right.svg'
import arrowUpRight from 'bootstrap-icons/icons/arrow-up-right.svg'
import asterisk from 'bootstrap-icons/icons/asterisk.svg'
import bank from 'bootstrap-icons/icons/bank.svg'
import basket from 'bootstrap-icons/icons/basket.svg'
import bell from 'bootstrap-icons/icons/bell.svg'
import bodyText from 'bootstrap-icons/icons/body-text.svg'
import bookmark from 'bootstrap-icons/icons/bookmark.svg'
import boxArrowInRight from 'bootstrap-icons/icons/box-arrow-in-right.svg'
import boxArrowUpRight from 'bootstrap-icons/icons/box-arrow-up-right.svg'
import boxArrowUp from 'bootstrap-icons/icons/box-arrow-up.svg'
import boxes from 'bootstrap-icons/icons/boxes.svg'
import braces from 'bootstrap-icons/icons/braces.svg'
import briefcase from 'bootstrap-icons/icons/briefcase.svg'
import building from 'bootstrap-icons/icons/building.svg'
import calculator from 'bootstrap-icons/icons/calculator.svg'
import calendarEventFill from 'bootstrap-icons/icons/calendar-event-fill.svg'
import calendarEvent from 'bootstrap-icons/icons/calendar-event.svg'
import calendar from 'bootstrap-icons/icons/calendar.svg'
import camera from 'bootstrap-icons/icons/camera.svg'
import cardChecklist from 'bootstrap-icons/icons/card-checklist.svg'
import cardHeading from 'bootstrap-icons/icons/card-heading.svg'
import caretDown from 'bootstrap-icons/icons/caret-down.svg'
import caretUp from 'bootstrap-icons/icons/caret-up.svg'
import cash from 'bootstrap-icons/icons/cash.svg'
import chatLeftText from 'bootstrap-icons/icons/chat-left-text.svg'
import chatSquareDots from 'bootstrap-icons/icons/chat-square-dots.svg'
import checkAll from 'bootstrap-icons/icons/check-all.svg'
import checkCircleFill from 'bootstrap-icons/icons/check-circle-fill.svg'
import checkCircle from 'bootstrap-icons/icons/check-circle.svg'
import checkLg from 'bootstrap-icons/icons/check-lg.svg'
import check from 'bootstrap-icons/icons/check.svg'
import check2All from 'bootstrap-icons/icons/check2-all.svg'
import chevronDoubleLeft from 'bootstrap-icons/icons/chevron-double-left.svg'
import chevronDoubleRight from 'bootstrap-icons/icons/chevron-double-right.svg'
import chevronDown from 'bootstrap-icons/icons/chevron-down.svg'
import chevronRight from 'bootstrap-icons/icons/chevron-right.svg'
import chevronUp from 'bootstrap-icons/icons/chevron-up.svg'
import circle from 'bootstrap-icons/icons/circle.svg'
import clipboardCheckFill from 'bootstrap-icons/icons/clipboard-check-fill.svg'
import clipboardCheck from 'bootstrap-icons/icons/clipboard-check.svg'
import clipboardFill from 'bootstrap-icons/icons/clipboard-fill.svg'
import clipboard from 'bootstrap-icons/icons/clipboard.svg'
import clockHistory from 'bootstrap-icons/icons/clock-history.svg'
import cpu from 'bootstrap-icons/icons/cpu.svg'
import creditCard from 'bootstrap-icons/icons/credit-card.svg'
import dashCircle from 'bootstrap-icons/icons/dash-circle.svg'
import dash from 'bootstrap-icons/icons/dash.svg'
import diagram3 from 'bootstrap-icons/icons/diagram-3.svg'
import dice5 from 'bootstrap-icons/icons/dice-5.svg'
import doorOpen from 'bootstrap-icons/icons/door-open.svg'
import download from 'bootstrap-icons/icons/download.svg'
import envelopeAtFill from 'bootstrap-icons/icons/envelope-at-fill.svg'
import envelopeAt from 'bootstrap-icons/icons/envelope-at.svg'
import envelope from 'bootstrap-icons/icons/envelope.svg'
import exclamationCircleFill from 'bootstrap-icons/icons/exclamation-circle-fill.svg'
import exclamationTriangleFill from 'bootstrap-icons/icons/exclamation-triangle-fill.svg'
import exclamationTriangle from 'bootstrap-icons/icons/exclamation-triangle.svg'
import eye from 'bootstrap-icons/icons/eye.svg'
import fileEarmarkArrowUp from 'bootstrap-icons/icons/file-earmark-arrow-up.svg'
import fileEarmarkCheck from 'bootstrap-icons/icons/file-earmark-check.svg'
import fileEarmarkDiff from 'bootstrap-icons/icons/file-earmark-diff.svg'
import fileEarmarkFill from 'bootstrap-icons/icons/file-earmark-fill.svg'
import fileEarmarkLock from 'bootstrap-icons/icons/file-earmark-lock.svg'
import fileEarmarkMedical from 'bootstrap-icons/icons/file-earmark-medical.svg'
import fileEarmarkMinus from 'bootstrap-icons/icons/file-earmark-minus.svg'
import fileEarmarkPerson from 'bootstrap-icons/icons/file-earmark-person.svg'
import fileEarmarkPlus from 'bootstrap-icons/icons/file-earmark-plus.svg'
import fileEarmarkRichtext from 'bootstrap-icons/icons/file-earmark-richtext.svg'
import fileEarmarkSpreadsheet from 'bootstrap-icons/icons/file-earmark-spreadsheet.svg'
import fileEarmark from 'bootstrap-icons/icons/file-earmark.svg'
import fileText from 'bootstrap-icons/icons/file-text.svg'
import files from 'bootstrap-icons/icons/files.svg'
import filter from 'bootstrap-icons/icons/filter.svg'
import floppy from 'bootstrap-icons/icons/floppy.svg'
import folderFill from 'bootstrap-icons/icons/folder-fill.svg'
import folder from 'bootstrap-icons/icons/folder.svg'
import funnel from 'bootstrap-icons/icons/funnel.svg'
import gear from 'bootstrap-icons/icons/gear.svg'
import globe2 from 'bootstrap-icons/icons/globe2.svg'
import google from 'bootstrap-icons/icons/google.svg'
import grid from 'bootstrap-icons/icons/grid.svg'
import gripVertical from 'bootstrap-icons/icons/grip-vertical.svg'
import hash from 'bootstrap-icons/icons/hash.svg'
import hddStack from 'bootstrap-icons/icons/hdd-stack.svg'
import heart from 'bootstrap-icons/icons/heart.svg'
import house from 'bootstrap-icons/icons/house.svg'
import inbox from 'bootstrap-icons/icons/inbox.svg'
import infoCircle from 'bootstrap-icons/icons/info-circle.svg'
import journalBookmarkFill from 'bootstrap-icons/icons/journal-bookmark-fill.svg'
import journals from 'bootstrap-icons/icons/journals.svg'
import lightbulb from 'bootstrap-icons/icons/lightbulb.svg'
import link from 'bootstrap-icons/icons/link.svg'
import listNested from 'bootstrap-icons/icons/list-nested.svg'
import listTask from 'bootstrap-icons/icons/list-task.svg'
import listUl from 'bootstrap-icons/icons/list-ul.svg'
import list from 'bootstrap-icons/icons/list.svg'
import microsoft from 'bootstrap-icons/icons/microsoft.svg'
import newspaper from 'bootstrap-icons/icons/newspaper.svg'
import nodePlus from 'bootstrap-icons/icons/node-plus.svg'
import paperclip from 'bootstrap-icons/icons/paperclip.svg'
import pencil from 'bootstrap-icons/icons/pencil.svg'
import peopleFill from 'bootstrap-icons/icons/people-fill.svg'
import people from 'bootstrap-icons/icons/people.svg'
import personCircle from 'bootstrap-icons/icons/person-circle.svg'
import personFillLock from 'bootstrap-icons/icons/person-fill-lock.svg'
import personFill from 'bootstrap-icons/icons/person-fill.svg'
import personLock from 'bootstrap-icons/icons/person-lock.svg'
import personSquare from 'bootstrap-icons/icons/person-square.svg'
import person from 'bootstrap-icons/icons/person.svg'
import playCircle from 'bootstrap-icons/icons/play-circle.svg'
import playFill from 'bootstrap-icons/icons/play-fill.svg'
import plusCircle from 'bootstrap-icons/icons/plus-circle.svg'
import plus from 'bootstrap-icons/icons/plus.svg'
import printer from 'bootstrap-icons/icons/printer.svg'
import questionCircle from 'bootstrap-icons/icons/question-circle.svg'
import receipt from 'bootstrap-icons/icons/receipt.svg'
import safe from 'bootstrap-icons/icons/safe.svg'
import scissors from 'bootstrap-icons/icons/scissors.svg'
import search from 'bootstrap-icons/icons/search.svg'
import send from 'bootstrap-icons/icons/send.svg'
import shop from 'bootstrap-icons/icons/shop.svg'
import slashCircle from 'bootstrap-icons/icons/slash-circle.svg'
import sliders2Vertical from 'bootstrap-icons/icons/sliders2-vertical.svg'
import sortAlphaDown from 'bootstrap-icons/icons/sort-alpha-down.svg'
import sortAlphaUpAlt from 'bootstrap-icons/icons/sort-alpha-up-alt.svg'
import stack from 'bootstrap-icons/icons/stack.svg'
import stars from 'bootstrap-icons/icons/stars.svg'
import tagFill from 'bootstrap-icons/icons/tag-fill.svg'
import tag from 'bootstrap-icons/icons/tag.svg'
import tags from 'bootstrap-icons/icons/tags.svg'
import telephone from 'bootstrap-icons/icons/telephone.svg'
import textIndentLeft from 'bootstrap-icons/icons/text-indent-left.svg'
import textLeft from 'bootstrap-icons/icons/text-left.svg'
import threeDotsVertical from 'bootstrap-icons/icons/three-dots-vertical.svg'
import threeDots from 'bootstrap-icons/icons/three-dots.svg'
import trash from 'bootstrap-icons/icons/trash.svg'
import truck from 'bootstrap-icons/icons/truck.svg'
import uiRadios from 'bootstrap-icons/icons/ui-radios.svg'
import unlock from 'bootstrap-icons/icons/unlock.svg'
import upcScan from 'bootstrap-icons/icons/upc-scan.svg'
import wallet2 from 'bootstrap-icons/icons/wallet2.svg'
import windowStack from 'bootstrap-icons/icons/window-stack.svg'
import xCircle from 'bootstrap-icons/icons/x-circle.svg'
import xLg from 'bootstrap-icons/icons/x-lg.svg'
import x from 'bootstrap-icons/icons/x.svg'
/**
* `<pngx-bs-icon>`. Only icons registered here are bundled but can add
* new ones (any icon from the bootstrap-icons package) as needed.
*/
export const icons: Record<string, string> = {
airplane,
archive,
arrowClockwise,
arrowCounterclockwise,
arrowDown,
arrowDownUp,
arrowLeft,
arrowRepeat,
arrowReturnLeft,
arrowRight,
arrowRightShort,
arrowUpRight,
asterisk,
bank,
basket,
bell,
braces,
bodyText,
bookmark,
boxArrowInRight,
boxArrowUp,
boxArrowUpRight,
boxes,
briefcase,
building,
calculator,
calendar,
calendarEvent,
calendarEventFill,
camera,
cardChecklist,
cardHeading,
caretDown,
caretUp,
chatLeftText,
chatSquareDots,
check,
check2All,
checkAll,
checkCircle,
checkCircleFill,
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
chevronDown,
chevronRight,
chevronUp,
circle,
clipboard,
clipboardCheck,
clipboardCheckFill,
clipboardFill,
clockHistory,
cpu,
cash,
creditCard,
dash,
dashCircle,
diagram3,
dice5,
doorOpen,
download,
envelope,
envelopeAt,
envelopeAtFill,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
eye,
fileEarmark,
fileEarmarkArrowUp,
fileEarmarkCheck,
fileEarmarkDiff,
fileEarmarkFill,
fileEarmarkLock,
fileEarmarkMedical,
fileEarmarkMinus,
fileEarmarkPerson,
fileEarmarkPlus,
fileEarmarkRichtext,
fileEarmarkSpreadsheet,
files,
fileText,
filter,
floppy,
folder,
folderFill,
funnel,
gear,
globe2,
google,
grid,
gripVertical,
hash,
hddStack,
heart,
house,
inbox,
infoCircle,
journalBookmarkFill,
journals,
lightbulb,
link,
list,
listNested,
listTask,
listUl,
microsoft,
newspaper,
nodePlus,
pencil,
paperclip,
people,
peopleFill,
person,
personCircle,
personFill,
personFillLock,
personLock,
personSquare,
playCircle,
playFill,
plus,
plusCircle,
printer,
questionCircle,
receipt,
safe,
scissors,
search,
send,
slashCircle,
shop,
sliders2Vertical,
sortAlphaDown,
sortAlphaUpAlt,
stack,
stars,
tagFill,
tag,
tags,
textIndentLeft,
textLeft,
telephone,
threeDots,
threeDotsVertical,
trash,
truck,
uiRadios,
unlock,
upcScan,
wallet2,
windowStack,
x,
xCircle,
xLg,
}
@@ -1,11 +1,11 @@
@if (active()) { @if (active()) {
<button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)"> <button class="position-absolute top-0 start-100 translate-middle badge bg-secondary border border-light rounded-pill p-1" title="Clear" i18n-title (click)="onClick($event)">
@if (!isNumbered() && selected()) { @if (!isNumbered() && selected()) {
<i-bs class="check" width="1em" height="1em" name="check-lg"></i-bs> <pngx-bs-icon class="check" width="1em" height="1em" name="check-lg"></pngx-bs-icon>
} }
@if (isNumbered()) { @if (isNumbered()) {
<div class="number">{{number()}}<span class="visually-hidden">selected</span></div> <div class="number">{{number()}}<span class="visually-hidden">selected</span></div>
} }
<i-bs class="x" width=".9em" height="1em" name="x-lg"></i-bs> <pngx-bs-icon class="x" width=".9em" height="1em" name="x-lg"></pngx-bs-icon>
</button> </button>
} }
@@ -1,5 +1,4 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ClearableBadgeComponent } from './clearable-badge.component' import { ClearableBadgeComponent } from './clearable-badge.component'
describe('ClearableBadgeComponent', () => { describe('ClearableBadgeComponent', () => {
@@ -8,10 +7,7 @@ describe('ClearableBadgeComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [ClearableBadgeComponent],
NgxBootstrapIconsModule.pick(allIcons),
ClearableBadgeComponent,
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(ClearableBadgeComponent) fixture = TestBed.createComponent(ClearableBadgeComponent)
@@ -1,11 +1,11 @@
import { Component, computed, EventEmitter, input, Output } from '@angular/core' import { Component, computed, EventEmitter, input, Output } from '@angular/core'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
@Component({ @Component({
selector: 'pngx-clearable-badge', selector: 'pngx-clearable-badge',
templateUrl: './clearable-badge.component.html', templateUrl: './clearable-badge.component.html',
styleUrls: ['./clearable-badge.component.scss'], styleUrls: ['./clearable-badge.component.scss'],
imports: [NgxBootstrapIconsModule], imports: [BootstrapIconComponent],
}) })
export class ClearableBadgeComponent { export class ClearableBadgeComponent {
readonly number = input<number>(undefined) readonly number = input<number>(undefined)
@@ -10,7 +10,7 @@
popoverClass="popover-slim" popoverClass="popover-slim"
> >
@if (iconName) { @if (iconName) {
<i-bs [class.me-1]="label" name="{{iconName}}"></i-bs> <pngx-bs-icon [class.me-1]="label" name="{{iconName}}"></pngx-bs-icon>
} }
<ng-container>{{label}}</ng-container> <ng-container>{{label}}</ng-container>
</button> </button>
@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from './confirm-button.component' import { ConfirmButtonComponent } from './confirm-button.component'
describe('ConfirmButtonComponent', () => { describe('ConfirmButtonComponent', () => {
@@ -10,11 +9,7 @@ describe('ConfirmButtonComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [NgbPopoverModule, ConfirmButtonComponent],
NgbPopoverModule,
NgxBootstrapIconsModule.pick(allIcons),
ConfirmButtonComponent,
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(ConfirmButtonComponent) fixture = TestBed.createComponent(ConfirmButtonComponent)
@@ -6,13 +6,13 @@ import {
ViewChild, ViewChild,
} from '@angular/core' } from '@angular/core'
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
@Component({ @Component({
selector: 'pngx-confirm-button', selector: 'pngx-confirm-button',
templateUrl: './confirm-button.component.html', templateUrl: './confirm-button.component.html',
styleUrl: './confirm-button.component.scss', styleUrl: './confirm-button.component.scss',
imports: [NgbPopoverModule, NgxBootstrapIconsModule], imports: [NgbPopoverModule, BootstrapIconComponent],
}) })
export class ConfirmButtonComponent { export class ConfirmButtonComponent {
@Input() @Input()
@@ -22,7 +22,7 @@
@let document = getDocument(documentID); @let document = getDocument(documentID);
@if (document) { @if (document) {
<li class="list-group-item d-flex align-items-center" cdkDrag> <li class="list-group-item d-flex align-items-center" cdkDrag>
<i-bs name="grip-vertical" class="me-2"></i-bs> <pngx-bs-icon name="grip-vertical" class="me-2"></pngx-bs-icon>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div> <div>
@if (document.correspondent) { @if (document.correspondent) {
@@ -2,7 +2,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs' import { of } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { MergeAsVersionsConfirmDialogComponent } from './merge-as-versions-confirm-dialog.component' import { MergeAsVersionsConfirmDialogComponent } from './merge-as-versions-confirm-dialog.component'
@@ -14,10 +13,7 @@ describe('MergeAsVersionsConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [MergeAsVersionsConfirmDialogComponent],
NgxBootstrapIconsModule.pick(allIcons),
MergeAsVersionsConfirmDialogComponent,
],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
@@ -6,8 +6,8 @@ import {
import { AsyncPipe } from '@angular/common' import { AsyncPipe } from '@angular/common'
import { Component, OnInit, computed, inject, signal } from '@angular/core' import { Component, OnInit, computed, inject, signal } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe' import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -24,7 +24,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
CustomDatePipe, CustomDatePipe,
DragDropModule, DragDropModule,
FormsModule, FormsModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
], ],
}) })
export class MergeAsVersionsConfirmDialogComponent export class MergeAsVersionsConfirmDialogComponent
@@ -15,7 +15,7 @@
@let document = getDocument(documentID); @let document = getDocument(documentID);
@if (document) { @if (document) {
<li class="list-group-item d-flex align-items-center" cdkDrag> <li class="list-group-item d-flex align-items-center" cdkDrag>
<i-bs name="grip-vertical" class="me-2"></i-bs> <pngx-bs-icon name="grip-vertical" class="me-2"></pngx-bs-icon>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<div> <div>
@if (document.correspondent) { @if (document.correspondent) {
@@ -3,7 +3,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs' import { of } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component' import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
@@ -15,12 +14,7 @@ describe('MergeConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [ReactiveFormsModule, FormsModule, MergeConfirmDialogComponent],
NgxBootstrapIconsModule.pick(allIcons),
ReactiveFormsModule,
FormsModule,
MergeConfirmDialogComponent,
],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
@@ -6,8 +6,8 @@ import {
import { AsyncPipe } from '@angular/common' import { AsyncPipe } from '@angular/common'
import { Component, OnInit, inject, signal } from '@angular/core' import { Component, OnInit, inject, signal } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe' import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -26,7 +26,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
DragDropModule, DragDropModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule, BootstrapIconComponent,
], ],
}) })
export class MergeConfirmDialogComponent export class MergeConfirmDialogComponent
@@ -18,7 +18,7 @@
(ngModelChange)="onUpdateDocumentChange($event)" (ngModelChange)="onUpdateDocumentChange($event)"
/> />
<label class="btn btn-outline-primary btn-sm" for="removeReplace"> <label class="btn btn-outline-primary btn-sm" for="removeReplace">
<i-bs name="pencil"></i-bs> <pngx-bs-icon name="pencil"></pngx-bs-icon>
<span class="ms-2" i18n>Replace current document</span> <span class="ms-2" i18n>Replace current document</span>
</label> </label>
<input <input
@@ -31,7 +31,7 @@
(ngModelChange)="onUpdateDocumentChange($event)" (ngModelChange)="onUpdateDocumentChange($event)"
/> />
<label class="btn btn-outline-primary btn-sm" for="removeCreate"> <label class="btn btn-outline-primary btn-sm" for="removeCreate">
<i-bs name="plus"></i-bs> <pngx-bs-icon name="plus"></pngx-bs-icon>
<span class="ms-2" i18n>Create new document</span> <span class="ms-2" i18n>Create new document</span>
</label> </label>
</div> </div>
@@ -1,7 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { PasswordRemovalConfirmDialogComponent } from './password-removal-confirm-dialog.component' import { PasswordRemovalConfirmDialogComponent } from './password-removal-confirm-dialog.component'
describe('PasswordRemovalConfirmDialogComponent', () => { describe('PasswordRemovalConfirmDialogComponent', () => {
@@ -11,10 +10,7 @@ describe('PasswordRemovalConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
providers: [NgbActiveModal], providers: [NgbActiveModal],
imports: [ imports: [PasswordRemovalConfirmDialogComponent],
NgxBootstrapIconsModule.pick(allIcons),
PasswordRemovalConfirmDialogComponent,
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent) fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent)
@@ -1,13 +1,13 @@
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { ConfirmDialogComponent } from '../confirm-dialog.component' import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({ @Component({
selector: 'pngx-password-removal-confirm-dialog', selector: 'pngx-password-removal-confirm-dialog',
templateUrl: './password-removal-confirm-dialog.component.html', templateUrl: './password-removal-confirm-dialog.component.html',
styleUrls: ['./password-removal-confirm-dialog.component.scss'], styleUrls: ['./password-removal-confirm-dialog.component.scss'],
imports: [FormsModule, NgxBootstrapIconsModule], imports: [FormsModule, BootstrapIconComponent],
}) })
export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent { export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent {
updateDocument: boolean = true updateDocument: boolean = true
@@ -7,7 +7,7 @@
<div class="row"> <div class="row">
<div class="col-2 d-flex justify-content-end"> <div class="col-2 d-flex justify-content-end">
<button class="btn btn-secondary mt-auto" (click)="rotate(false)" aria-label="Rotate counterclockwise" i18n-aria-label> <button class="btn btn-secondary mt-auto" (click)="rotate(false)" aria-label="Rotate counterclockwise" i18n-aria-label>
<i-bs name="arrow-counterclockwise"></i-bs> <pngx-bs-icon name="arrow-counterclockwise"></pngx-bs-icon>
</button> </button>
</div> </div>
<div class="col-8 d-flex align-items-center"> <div class="col-8 d-flex align-items-center">
@@ -17,7 +17,7 @@
</div> </div>
<div class="col-2 d-flex"> <div class="col-2 d-flex">
<button class="btn btn-secondary mt-auto" (click)="rotate()" aria-label="Rotate clockwise" i18n-aria-label> <button class="btn btn-secondary mt-auto" (click)="rotate()" aria-label="Rotate clockwise" i18n-aria-label>
<i-bs name="arrow-clockwise"></i-bs> <pngx-bs-icon name="arrow-clockwise"></pngx-bs-icon>
</button> </button>
</div> </div>
</div> </div>
@@ -2,7 +2,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component' import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
describe('RotateConfirmDialogComponent', () => { describe('RotateConfirmDialogComponent', () => {
@@ -11,10 +10,7 @@ describe('RotateConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [RotateConfirmDialogComponent],
NgxBootstrapIconsModule.pick(allIcons),
RotateConfirmDialogComponent,
],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
@@ -1,6 +1,6 @@
import { NgStyle } from '@angular/common' import { NgStyle } from '@angular/common'
import { Component, inject, signal } from '@angular/core' import { Component, inject, signal } from '@angular/core'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component' import { ConfirmDialogComponent } from '../confirm-dialog.component'
@@ -8,7 +8,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
selector: 'pngx-rotate-confirm-dialog', selector: 'pngx-rotate-confirm-dialog',
templateUrl: './rotate-confirm-dialog.component.html', templateUrl: './rotate-confirm-dialog.component.html',
styleUrl: './rotate-confirm-dialog.component.scss', styleUrl: './rotate-confirm-dialog.component.scss',
imports: [NgStyle, NgxBootstrapIconsModule], imports: [NgStyle, BootstrapIconComponent],
}) })
export class RotateConfirmDialogComponent extends ConfirmDialogComponent { export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
documentService = inject(DocumentService) documentService = inject(DocumentService)
@@ -0,0 +1,32 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
@if (message) {
<p class="text-break" [innerHTML]="message"></p>
}
<div class="mb-3">
<label class="form-label" for="runWorkflowSelect" i18n>Workflow</label>
<select class="form-select" id="runWorkflowSelect" [(ngModel)]="workflowId" (ngModelChange)="workflowSelected()">
<option [ngValue]="null" disabled i18n>Select a workflow</option>
@for (workflow of workflows; track workflow.id) {
<option [ngValue]="workflow.id">{{workflow.name}}</option>
}
</select>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="runWorkflowForce" [(ngModel)]="force" />
<label class="form-check-label" for="runWorkflowForce" i18n>Run on all selected documents</label>
<div class="form-text" i18n>Ignore the workflow's trigger filters and run on every selected document.</div>
</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>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}}
</button>
</div>
@@ -0,0 +1,42 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { RunWorkflowConfirmDialogComponent } from './run-workflow-confirm-dialog.component'
describe('RunWorkflowConfirmDialogComponent', () => {
let component: RunWorkflowConfirmDialogComponent
let fixture: ComponentFixture<RunWorkflowConfirmDialogComponent>
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [NgbActiveModal],
imports: [RunWorkflowConfirmDialogComponent],
}).compileComponents()
fixture = TestBed.createComponent(RunWorkflowConfirmDialogComponent)
component = fixture.componentInstance
component.workflows = [{ id: 5, name: 'Manual workflow' } as any]
fixture.detectChanges()
})
it('should disable confirm until a workflow is selected and not force by default', () => {
expect(component.confirmButtonEnabled).toBeFalsy()
expect(component.force).toBeFalsy()
component.workflowId = 5
component.workflowSelected()
expect(component.confirmButtonEnabled).toBeTruthy()
component.workflowId = null
component.workflowSelected()
expect(component.confirmButtonEnabled).toBeFalsy()
})
it('should render an option per workflow', () => {
const options = fixture.nativeElement.querySelectorAll(
'#runWorkflowSelect option'
)
// placeholder option + one per workflow
expect(options).toHaveLength(2)
expect(options[1].textContent).toContain('Manual workflow')
})
})
@@ -0,0 +1,24 @@
import { Component } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { Workflow } from 'src/app/data/workflow'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-run-workflow-confirm-dialog',
templateUrl: './run-workflow-confirm-dialog.component.html',
imports: [FormsModule],
})
export class RunWorkflowConfirmDialogComponent extends ConfirmDialogComponent {
public workflows: Workflow[] = []
public workflowId: number = null
// i.e. ignore the workflow's trigger filters
public force: boolean = false
override confirmButtonEnabled = false
public workflowSelected(): void {
this.confirmButtonEnabled = !!this.workflowId
}
}
@@ -20,7 +20,7 @@
@for (docId of value; track docId) { @for (docId of value; track docId) {
@if (getDocumentTitle(docId)) { @if (getDocumentTitle(docId)) {
<a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title> <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" class="me-1"></i-bs><span>{{ getDocumentTitle(docId) }}</span> <pngx-bs-icon width="0.9em" height="0.9em" name="file-text" class="me-1"></pngx-bs-icon><span>{{ getDocumentTitle(docId) }}</span>
</a> </a>
} }
} }
@@ -1,6 +1,6 @@
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions"> <div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions">
<button type="button" class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle aria-label="Custom Fields" i18n-aria-label> <button type="button" class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle aria-label="Custom Fields" i18n-aria-label>
<i-bs name="ui-radios"></i-bs><div class="d-none d-lg-inline ms-1"><ng-container i18n>Custom Fields</ng-container></div> <pngx-bs-icon name="ui-radios"></pngx-bs-icon><div class="d-none d-lg-inline ms-1"><ng-container i18n>Custom Fields</ng-container></div>
</button> </button>
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown"> <div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
<div class="list-group list-group-flush" (keydown)="listKeyDown($event)"> <div class="list-group list-group-flush" (keydown)="listKeyDown($event)">
@@ -11,14 +11,12 @@
</div> </div>
@for (field of filteredFields; track field.id) { @for (field of filteredFields; track field.id) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button> <button type="button" class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button>
<small class="d-flex">{{field.name}} <small class="ms-auto text-muted">{{getDataTypeLabel(field.data_type)}}</small></small> <div class="d-flex">{{field.name}} <small class="ms-auto text-muted">{{getDataTypeLabel(field.data_type)}}</small></div>
</button> </button>
} }
@if (!filterText?.length || filteredFields.length === 0) { @if (!filterText?.length || filteredFields.length === 0) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button> <button type="button" class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
<small> <pngx-bs-icon width=".9em" height=".9em" name="asterisk" class="me-1"></pngx-bs-icon><ng-container i18n>Create new field</ng-container>
<i-bs width=".9em" height=".9em" name="asterisk" class="me-1"></i-bs><ng-container i18n>Create new field</ng-container>
</small>
</button> </button>
} }
</div> </div>
@@ -1,10 +1,3 @@
.custom-fields-dropdown { .custom-fields-dropdown {
min-width: 300px; min-width: 300px;
// correct position on mobile
@media (max-width: 575.98px) {
&.show {
margin-left: -245px !important;
}
}
} }
@@ -10,7 +10,6 @@ import {
NgbModalRef, NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs' import { of } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
@@ -49,7 +48,6 @@ describe('CustomFieldsDropdownComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgbModalModule, NgbModalModule,
NgbDropdownModule, NgbDropdownModule,
NgxBootstrapIconsModule.pick(allIcons),
CustomFieldsDropdownComponent, CustomFieldsDropdownComponent,
SelectComponent, SelectComponent,
], ],
Loaded 100 of 456 files, more files were not shown because too many files have changed in this diff. Show more