Compare commits

..
Author SHA1 Message Date
shamoon b5d44f6224 Remove angular platform-browser-dynamic 2026-07-10 01:00:47 -07:00
191 changed files with 49457 additions and 86923 deletions
-1
View File
@@ -26,7 +26,6 @@ jobs:
check-name: 'Merge and Push Manifest' check-name: 'Merge and Push Manifest'
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 60 wait-interval: 60
checks-discovery-timeout: 1800
build-release: build-release:
name: Build Release name: Build Release
needs: wait-for-docker needs: wait-for-docker
+4 -4
View File
@@ -38,7 +38,7 @@ repos:
- json - json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason # See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.9.4' rev: 'v3.8.4'
hooks: hooks:
- id: prettier - id: prettier
types_or: types_or:
@@ -46,16 +46,16 @@ repos:
- ts - ts
- markdown - markdown
additional_dependencies: additional_dependencies:
- prettier@3.9.4 - prettier@3.8.3
- 'prettier-plugin-organize-imports@4.3.0' - 'prettier-plugin-organize-imports@4.3.0'
# Python hooks # Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.20 rev: v0.15.17
hooks: hooks:
- id: ruff-check - id: ruff-check
- id: ruff-format - id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.25.1" rev: "v2.24.1"
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
additional_dependencies: [tomli] additional_dependencies: [tomli]
+1 -5
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.11.28-python3.12-trixie-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.11.19-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6
@@ -238,10 +238,6 @@ RUN set -eux \
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \ && chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
&& echo "Making fontconfig cache writable for arbitrary container UIDs" \ && echo "Making fontconfig cache writable for arbitrary container UIDs" \
&& chmod 1777 /var/cache/fontconfig \ && chmod 1777 /var/cache/fontconfig \
&& echo "Making /run world-writable for rootless operation" \
&& chmod 1777 /run \
&& echo "Removing setuid from s6-overlay-suexec for rootless compat" \
&& chmod u-s /command/s6-overlay-suexec \
&& echo "Collecting static files" \ && echo "Collecting static files" \
&& PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \ && PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
&& PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py compilemessages \ && PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py compilemessages \
+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.34 image: docker.io/gotenberg/gotenberg:8.33
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.9 image: docker.io/greenmail/standalone:2.1.8
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.2-alpine image: docker.io/nginx:1.31.1-alpine
hostname: nginx hostname: nginx
container_name: nginx container_name: nginx
ports: ports:
@@ -72,7 +72,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.34 image: docker.io/gotenberg/gotenberg:8.33
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.34 image: docker.io/gotenberg/gotenberg:8.33
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.34 image: docker.io/gotenberg/gotenberg:8.33
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.
-143
View File
File diff suppressed because it is too large Load Diff
-8
View File
@@ -326,11 +326,3 @@ behind a reverse proxy may need to set
[`PAPERLESS_TRUSTED_PROXIES`](configuration.md#PAPERLESS_TRUSTED_PROXIES), [`PAPERLESS_TRUSTED_PROXIES`](configuration.md#PAPERLESS_TRUSTED_PROXIES),
[`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER), [`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER),
or both, to avoid `403 Forbidden` errors on login. or both, to avoid `403 Forbidden` errors on login.
## Database Migrations
Some integer fields have been changed to smaller types to reduce database size. If you have any `MailRule` records with a `maximum_age` greater than 32767, they will be clamped to 32767 during the migration to avoid errors during migration.
### Action Required
No user action is required. The migration will automatically clamp any `MailRule.maximum_age` values greater than 32767 to 32767 during the migration process.
+1 -1
View File
@@ -142,7 +142,7 @@ a [superuser](usage.md#superusers) account.
It is not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`. It is not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`.
If you want to run Paperless as a rootless container, set `user:` in `docker-compose.yml` to the UID and GID of your host user (use `id -u` and `id -g` to find these values). The container process starts directly as that user with no internal privilege remapping. Any UID and GID is supported: If you want to run Paperless as a rootless container, set `user:` in `docker-compose.yml` to the UID and GID of your host user (use `id -u` and `id -g` to find these values). The container process starts directly as that user with no internal privilege remapping:
```yaml ```yaml
webserver: webserver:
+4 -4
View File
@@ -16,7 +16,7 @@ classifiers = [
dependencies = [ dependencies = [
"azure-ai-documentintelligence>=1.0.2", "azure-ai-documentintelligence>=1.0.2",
"babel>=2.17", "babel>=2.17",
"bleach~=6.4.0", "bleach~=6.3.0",
"celery[redis]~=5.6.2", "celery[redis]~=5.6.2",
"channels~=4.2", "channels~=4.2",
"channels-redis~=4.2", "channels-redis~=4.2",
@@ -56,7 +56,7 @@ dependencies = [
"llama-index-embeddings-openai-like>=0.2.2", "llama-index-embeddings-openai-like>=0.2.2",
"llama-index-llms-ollama>=0.9.1", "llama-index-llms-ollama>=0.9.1",
"llama-index-llms-openai-like>=0.7.1", "llama-index-llms-openai-like>=0.7.1",
"nltk~=3.10.0", "nltk~=3.9.1",
"ocrmypdf~=17.4.2", "ocrmypdf~=17.4.2",
"openai>=2.32", "openai>=2.32",
"pathvalidate~=3.3.1", "pathvalidate~=3.3.1",
@@ -101,11 +101,11 @@ dev = [
{ include-group = "testing" }, { include-group = "testing" },
] ]
docs = [ docs = [
"zensical>=0.0.47", "zensical>=0.0.43",
] ]
lint = [ lint = [
"prek~=0.3.10", "prek~=0.3.10",
"ruff~=0.15.20", "ruff~=0.15.15",
] ]
testing = [ testing = [
"daphne", "daphne",
-10
View File
@@ -79,16 +79,6 @@
"glob": "{pdf.worker.min.mjs,pdf.min.mjs}", "glob": "{pdf.worker.min.mjs,pdf.min.mjs}",
"input": "node_modules/pdfjs-dist/legacy/build/", "input": "node_modules/pdfjs-dist/legacy/build/",
"output": "/assets/js/" "output": "/assets/js/"
},
{
"glob": "**/*",
"input": "node_modules/pdfjs-dist/wasm/",
"output": "/assets/wasm/"
},
{
"glob": "**/*",
"input": "node_modules/pdfjs-dist/iccs/",
"output": "/assets/iccs/"
} }
], ],
"styles": [ "styles": [
+122 -151
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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/alert/alert.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/alert/alert.ts</context>
<context context-type="linenumber">39</context> <context context-type="linenumber">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="ueryList&lt;NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source> <source> Slide <x id="INTERPOLATION" equiv-text="ueryList&lt;NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">129,133</context> <context context-type="linenumber">129,133</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,114 +20,114 @@
<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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">160,161</context> <context context-type="linenumber">160,161</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/carousel/carousel.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/carousel/carousel.ts</context>
<context context-type="linenumber">205,206</context> <context context-type="linenumber">205,206</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/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.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">74,76</context> <context context-type="linenumber">74,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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/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.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">103</context> <context context-type="linenumber">103</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/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.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">103</context> <context context-type="linenumber">103</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/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.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/datepicker/datepicker-navigation.ts</context>
<context context-type="linenumber">103</context> <context context-type="linenumber">103</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/datepicker/ngb-calendar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/datepicker/ngb-calendar.ts</context>
<context context-type="linenumber">63,65</context> <context context-type="linenumber">63,65</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/datepicker/ngb-calendar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/datepicker/ngb-calendar.ts</context>
<context context-type="linenumber">106,107</context> <context context-type="linenumber">106,107</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/datepicker/ngb-calendar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/datepicker/ngb-calendar.ts</context>
<context context-type="linenumber">145,146</context> <context context-type="linenumber">145,146</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/datepicker/ngb-calendar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/datepicker/ngb-calendar.ts</context>
<context context-type="linenumber">179,183</context> <context context-type="linenumber">179,183</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">302</context> <context context-type="linenumber">302</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">333,335</context> <context context-type="linenumber">333,335</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">375</context> <context context-type="linenumber">375</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">408</context> <context context-type="linenumber">408</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">437</context> <context context-type="linenumber">437</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">472,474</context> <context context-type="linenumber">472,474</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">506</context> <context context-type="linenumber">506</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/pagination/pagination.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/pagination/pagination.ts</context>
<context context-type="linenumber">506</context> <context context-type="linenumber">506</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
@@ -135,105 +135,105 @@
<source><x id="INTERPOLATION" equiv-text="barConfig); <source><x id="INTERPOLATION" equiv-text="barConfig);
pu"/></source> pu"/></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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/progressbar/progressbar.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/progressbar/progressbar.ts</context>
<context context-type="linenumber">40,41</context> <context context-type="linenumber">40,41</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">263,264</context> <context context-type="linenumber">263,264</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">301,302</context> <context context-type="linenumber">301,302</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">341,342</context> <context context-type="linenumber">341,342</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">381,384</context> <context context-type="linenumber">381,384</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/timepicker/timepicker.ts</context>
<context context-type="linenumber">429,430</context> <context context-type="linenumber">429,430</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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/timepicker/timepicker.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/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.0.5_@angular+core@22.0.5_@angular+_88eda8c8846cc5cfe00e25de83b3f075/node_modules/src/toast/toast.ts</context> <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.0.5_@angular+core@22.0.5_@angular+_d3d748c9cb3098b2696b844e4082b488/node_modules/src/toast/toast.ts</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">116</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
@@ -332,7 +332,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">86</context> <context context-type="linenumber">85</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context>
@@ -379,11 +379,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">273</context> <context context-type="linenumber">272</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">275</context> <context context-type="linenumber">274</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5890330709052835856" datatype="html"> <trans-unit id="5890330709052835856" datatype="html">
@@ -724,11 +724,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">308</context> <context context-type="linenumber">307</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">311</context> <context context-type="linenumber">310</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2272120016352772836" datatype="html"> <trans-unit id="2272120016352772836" datatype="html">
@@ -757,14 +757,14 @@
<source>lines</source> <source>lines</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context> <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
<context context-type="linenumber">18</context> <context context-type="linenumber">17</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8838884664569764142" datatype="html"> <trans-unit id="8838884664569764142" datatype="html">
<source>Auto refresh</source> <source>Auto refresh</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context> <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
<context context-type="linenumber">22</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
@@ -775,11 +775,11 @@
<source>Loading...</source> <source>Loading...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context> <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
<context context-type="linenumber">39</context> <context context-type="linenumber">38</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context> <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
<context context-type="linenumber">49</context> <context context-type="linenumber">48</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
@@ -886,7 +886,7 @@
<source>Jump to bottom</source> <source>Jump to bottom</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context> <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
<context context-type="linenumber">63</context> <context context-type="linenumber">62</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1255048712725285892" datatype="html"> <trans-unit id="1255048712725285892" datatype="html">
@@ -1135,11 +1135,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">233</context> <context context-type="linenumber">232</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">235</context> <context context-type="linenumber">234</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
@@ -1696,7 +1696,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">204</context> <context context-type="linenumber">203</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context> <context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context>
@@ -1801,11 +1801,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">296</context> <context context-type="linenumber">295</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">298</context> <context context-type="linenumber">297</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8492095365580052820" datatype="html"> <trans-unit id="8492095365580052820" datatype="html">
@@ -1905,18 +1905,11 @@
<context context-type="linenumber">227</context> <context context-type="linenumber">227</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3570013039254960445" datatype="html">
<source>Search tasks</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="6849725902312323996" datatype="html"> <trans-unit id="6849725902312323996" datatype="html">
<source>Reset filters</source> <source>Reset filters</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">105</context> <context context-type="linenumber">104</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>
@@ -1931,14 +1924,14 @@
<source>{VAR_PLURAL, plural, =1 {1 task} other {<x id="INTERPOLATION"/> tasks}}</source> <source>{VAR_PLURAL, plural, =1 {1 task} other {<x id="INTERPOLATION"/> tasks}}</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">126</context> <context context-type="linenumber">125</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8953033926734869941" datatype="html"> <trans-unit id="8953033926734869941" datatype="html">
<source>Name</source> <source>Name</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">148</context> <context context-type="linenumber">147</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
@@ -2049,7 +2042,7 @@
<source>Created</source> <source>Created</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">149</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context>
@@ -2080,21 +2073,21 @@
<source>Results</source> <source>Results</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">151</context> <context context-type="linenumber">150</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="314315645942131479" datatype="html"> <trans-unit id="314315645942131479" datatype="html">
<source>Info</source> <source>Info</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">153</context> <context context-type="linenumber">152</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3193976279273491157" datatype="html"> <trans-unit id="3193976279273491157" datatype="html">
<source>Actions</source> <source>Actions</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">154</context> <context context-type="linenumber">153</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
@@ -2165,14 +2158,14 @@
<source>click for full output</source> <source>click for full output</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">205</context> <context context-type="linenumber">204</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1536087519743707362" datatype="html"> <trans-unit id="1536087519743707362" datatype="html">
<source>Dismiss</source> <source>Dismiss</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">218</context> <context context-type="linenumber">217</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
@@ -2188,35 +2181,35 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">51</context> <context context-type="linenumber">50</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2134950584701094962" datatype="html"> <trans-unit id="2134950584701094962" datatype="html">
<source>Open Document</source> <source>Open Document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">223</context> <context context-type="linenumber">222</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5404759957685833020" datatype="html"> <trans-unit id="5404759957685833020" datatype="html">
<source>Result message</source> <source>Result message</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">236</context> <context context-type="linenumber">235</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6621329748219109148" datatype="html"> <trans-unit id="6621329748219109148" datatype="html">
<source>Duplicate</source> <source>Duplicate</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">243</context> <context context-type="linenumber">242</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7593555694782789615" datatype="html"> <trans-unit id="7593555694782789615" datatype="html">
<source>Open</source> <source>Open</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">251</context> <context context-type="linenumber">250</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
@@ -2247,21 +2240,21 @@
<source>Input data</source> <source>Input data</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">261</context> <context context-type="linenumber">260</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1585185618099050920" datatype="html"> <trans-unit id="1585185618099050920" datatype="html">
<source>Result data</source> <source>Result data</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">267</context> <context context-type="linenumber">266</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7976920528153858271" datatype="html"> <trans-unit id="7976920528153858271" datatype="html">
<source>No tasks match the current filters.</source> <source>No tasks match the current filters.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">289</context> <context context-type="linenumber">288</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2525230676386818985" datatype="html"> <trans-unit id="2525230676386818985" datatype="html">
@@ -2520,11 +2513,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">256</context> <context context-type="linenumber">255</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">259</context> <context context-type="linenumber">258</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3818027200170621545" datatype="html"> <trans-unit id="3818027200170621545" datatype="html">
@@ -2877,11 +2870,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">287</context> <context context-type="linenumber">286</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">289</context> <context context-type="linenumber">288</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4569276013106377105" datatype="html"> <trans-unit id="4569276013106377105" datatype="html">
@@ -3218,11 +3211,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">317</context> <context context-type="linenumber">316</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">320</context> <context context-type="linenumber">319</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="472206565520537964" datatype="html"> <trans-unit id="472206565520537964" datatype="html">
@@ -3247,36 +3240,36 @@
<source>Close all</source> <source>Close all</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">163</context> <context context-type="linenumber">162</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">165</context> <context context-type="linenumber">164</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3897348120591552265" datatype="html"> <trans-unit id="3897348120591552265" datatype="html">
<source>Manage</source> <source>Manage</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">174</context> <context context-type="linenumber">173</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8008131619909556709" datatype="html"> <trans-unit id="8008131619909556709" datatype="html">
<source>Attributes</source> <source>Attributes</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">181</context> <context context-type="linenumber">180</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">183</context> <context context-type="linenumber">182</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7437910965833684826" datatype="html"> <trans-unit id="7437910965833684826" datatype="html">
<source>Correspondents</source> <source>Correspondents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">209</context> <context context-type="linenumber">208</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context>
@@ -3295,7 +3288,7 @@
<source>Document types</source> <source>Document types</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">214</context> <context context-type="linenumber">213</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context> <context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
@@ -3306,7 +3299,7 @@
<source>Storage paths</source> <source>Storage paths</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">219</context> <context context-type="linenumber">218</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context> <context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
@@ -3317,7 +3310,7 @@
<source>Custom fields</source> <source>Custom fields</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">224</context> <context context-type="linenumber">223</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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -3336,11 +3329,11 @@
<source>Workflows</source> <source>Workflows</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">242</context> <context context-type="linenumber">241</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">244</context> <context context-type="linenumber">243</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
@@ -3351,71 +3344,71 @@
<source>Mail</source> <source>Mail</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">249</context> <context context-type="linenumber">248</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">252</context> <context context-type="linenumber">251</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7844706011418789951" datatype="html"> <trans-unit id="7844706011418789951" datatype="html">
<source>Administration</source> <source>Administration</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">267</context> <context context-type="linenumber">266</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3008420115644088420" datatype="html"> <trans-unit id="3008420115644088420" datatype="html">
<source>Configuration</source> <source>Configuration</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">280</context> <context context-type="linenumber">279</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">282</context> <context context-type="linenumber">281</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1534029177398918729" datatype="html"> <trans-unit id="1534029177398918729" datatype="html">
<source>GitHub</source> <source>GitHub</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">327</context> <context context-type="linenumber">326</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4112664765954374539" datatype="html"> <trans-unit id="4112664765954374539" datatype="html">
<source>is available.</source> <source>is available.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">336,337</context> <context context-type="linenumber">335,336</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1175891574282637937" datatype="html"> <trans-unit id="1175891574282637937" datatype="html">
<source>Click to view.</source> <source>Click to view.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">337</context> <context context-type="linenumber">336</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9811291095862612" datatype="html"> <trans-unit id="9811291095862612" datatype="html">
<source>Paperless-ngx can automatically check for updates</source> <source>Paperless-ngx can automatically check for updates</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">341</context> <context context-type="linenumber">340</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="894819944961861800" datatype="html"> <trans-unit id="894819944961861800" datatype="html">
<source> How does this work? </source> <source> How does this work? </source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">348,350</context> <context context-type="linenumber">347,349</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="509090351011426949" datatype="html"> <trans-unit id="509090351011426949" datatype="html">
<source>Update available</source> <source>Update available</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">361</context> <context context-type="linenumber">360</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1542489069631984294" datatype="html"> <trans-unit id="1542489069631984294" datatype="html">
@@ -3611,17 +3604,6 @@
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8066608938393600549" datatype="html">
<source>Message</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/chat/chat/chat.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="3167745892412525370" datatype="html"> <trans-unit id="3167745892412525370" datatype="html">
<source>Ask a question about this document...</source> <source>Ask a question about this document...</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -5846,6 +5828,13 @@
<context context-type="linenumber">32</context> <context context-type="linenumber">32</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8066608938393600549" datatype="html">
<source>Message</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.html</context>
<context context-type="linenumber">19</context>
</context-group>
</trans-unit>
<trans-unit id="5867799091834207531" datatype="html"> <trans-unit id="5867799091834207531" datatype="html">
<source>Use archive version</source> <source>Use archive version</source>
<context-group purpose="location"> <context-group purpose="location">
@@ -6705,7 +6694,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">88</context> <context context-type="linenumber">87</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context>
@@ -6782,25 +6771,18 @@
<context context-type="linenumber">66</context> <context context-type="linenumber">66</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4306169234658044987" datatype="html">
<source>Share link</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="2618101617221155143" datatype="html"> <trans-unit id="2618101617221155143" datatype="html">
<source>Copy link</source> <source>Copy link</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">82</context> <context context-type="linenumber">81</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8372007266188249803" datatype="html"> <trans-unit id="8372007266188249803" datatype="html">
<source>Never</source> <source>Never</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">94</context> <context context-type="linenumber">93</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context>
@@ -6815,7 +6797,7 @@
<source>File version</source> <source>File version</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">97</context> <context context-type="linenumber">96</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context>
@@ -6826,7 +6808,7 @@
<source>Size</source> <source>Size</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">100</context> <context context-type="linenumber">99</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component.html</context>
@@ -6837,14 +6819,14 @@
<source>A zip file containing the selected documents will be created for this share link bundle. This process happens in the background and may take some time, especially for large bundles.</source> <source>A zip file containing the selected documents will be created for this share link bundle. This process happens in the background and may take some time, especially for large bundles.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">110</context> <context context-type="linenumber">109</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7274497464693086242" datatype="html"> <trans-unit id="7274497464693086242" datatype="html">
<source>Manage share link bundles</source> <source>Manage share link bundles</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">113</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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -7835,8 +7817,8 @@
<context context-type="linenumber">314</context> <context context-type="linenumber">314</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2659735245739197634" datatype="html"> <trans-unit id="7985558498848210210" datatype="html">
<source>Original SHA256 checksum</source> <source>Original MD5 checksum</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">318</context> <context context-type="linenumber">318</context>
@@ -7856,8 +7838,8 @@
<context context-type="linenumber">326</context> <context context-type="linenumber">326</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6714358112223607756" datatype="html"> <trans-unit id="342875990758166588" datatype="html">
<source>Archive SHA256 checksum</source> <source>Archive MD5 checksum</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">331</context> <context context-type="linenumber">331</context>
@@ -8255,67 +8237,63 @@
<source>Optional</source> <source>Optional</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">16</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="787787001194154763" datatype="html"> <trans-unit id="787787001194154763" datatype="html">
<source>Add new version</source> <source>Add new version</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">32</context> <context context-type="linenumber">31</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3119565892291077820" datatype="html"> <trans-unit id="3119565892291077820" datatype="html">
<source>Uploading version...</source> <source>Uploading version...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">39</context> <context context-type="linenumber">38</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8538204518127762311" datatype="html"> <trans-unit id="8538204518127762311" datatype="html">
<source>Processing version...</source> <source>Processing version...</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">44</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6587183611950398104" datatype="html"> <trans-unit id="6587183611950398104" datatype="html">
<source>Version upload failed.</source> <source>Version upload failed.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">49</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6328986243040954523" datatype="html"> <trans-unit id="6328986243040954523" datatype="html">
<source>Version label</source> <source>Version label</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">75</context> <context context-type="linenumber">80</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">85</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2724055831234181057" datatype="html"> <trans-unit id="2724055831234181057" datatype="html">
<source>Version</source> <source>Version</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">96</context> <context context-type="linenumber">91</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2491751767883178298" datatype="html"> <trans-unit id="2491751767883178298" datatype="html">
<source>Delete this version?</source> <source>Delete this version?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">127</context> <context context-type="linenumber">122</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="894108684749537200" datatype="html"> <trans-unit id="894108684749537200" datatype="html">
<source>Delete version</source> <source>Delete version</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html</context>
<context context-type="linenumber">132</context> <context context-type="linenumber">127</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9111284310237278846" datatype="html"> <trans-unit id="9111284310237278846" datatype="html">
@@ -10218,13 +10196,6 @@
<context context-type="linenumber">20</context> <context context-type="linenumber">20</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8669280833944376006" datatype="html">
<source>Select all processed email messages</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/processed-mail-dialog/processed-mail-dialog.component.html</context>
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
<trans-unit id="8691920320483720007" datatype="html"> <trans-unit id="8691920320483720007" datatype="html">
<source>Received</source> <source>Received</source>
<context-group purpose="location"> <context-group purpose="location">
+1856 -1256
View File
File diff suppressed because it is too large Load Diff
@@ -5,9 +5,8 @@
i18n-info> i18n-info>
<div class="input-group input-group-sm align-items-center"> <div class="input-group input-group-sm align-items-center">
<div class="input-group input-group-sm me-3"> <div class="input-group input-group-sm me-3">
<label class="input-group-text text-muted" for="logLineLimit" i18n>Show</label> <span class="input-group-text text-muted" i18n>Show</span>
<input <input
id="logLineLimit"
class="form-control" class="form-control"
type="number" type="number"
min="100" min="100"
@@ -18,7 +17,7 @@
<span class="input-group-text text-muted" i18n>lines</span> <span class="input-group-text text-muted" i18n>lines</span>
</div> </div>
<div class="form-check form-switch mt-1"> <div class="form-check form-switch mt-1">
<input id="autoRefreshSwitch" class="form-check-input" type="checkbox" role="switch" [ngModel]="autoRefreshEnabled()" (ngModelChange)="autoRefreshEnabled.set($event)"> <input class="form-check-input" type="checkbox" role="switch" [ngModel]="autoRefreshEnabled()" (ngModelChange)="autoRefreshEnabled.set($event)">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label> <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div> </div>
</div> </div>
@@ -100,7 +100,9 @@ export class LogsComponent
parsed.some((log, idx) => { parsed.some((log, idx) => {
const current = this.logs()[idx] const current = this.logs()[idx]
return ( return (
current?.message !== log.message || current?.level !== log.level !current ||
current.message !== log.message ||
current.level !== log.level
) )
}) })
if (hasChanges) { if (hasChanges) {
@@ -37,7 +37,7 @@
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()"> <form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [activeId]="activeNavID()" (activeIdChange)="activeNavID.set($event)" class="nav-tabs"> <ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-tabs">
<li [ngbNavItem]="SettingsNavIDs.General"> <li [ngbNavItem]="SettingsNavIDs.General">
<a ngbNavLink i18n>General</a> <a ngbNavLink i18n>General</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@@ -150,10 +150,10 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3 col-form-label pt-0"> <div class="col-md-3 col-form-label pt-0">
<label for="searchLink" i18n>Full search links to</label> <span i18n>Full search links to</span>
</div> </div>
<div class="col mb-3"> <div class="col mb-3">
<select id="searchLink" class="form-select" formControlName="searchLink"> <select class="form-select" formControlName="searchLink">
<option [ngValue]="GlobalSearchType.TITLE_CONTENT" i18n>Title and content search</option> <option [ngValue]="GlobalSearchType.TITLE_CONTENT" i18n>Title and content search</option>
<option [ngValue]="GlobalSearchType.ADVANCED" i18n>Advanced search</option> <option [ngValue]="GlobalSearchType.ADVANCED" i18n>Advanced search</option>
</select> </select>
@@ -199,10 +199,10 @@
<h5 i18n>Documents</h5> <h5 i18n>Documents</h5>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-3 col-form-label pt-0"> <div class="col-md-3 col-form-label pt-0">
<label for="documentListItemPerPage" i18n>Items per page</label> <span i18n>Items per page</span>
</div> </div>
<div class="col"> <div class="col">
<select id="documentListItemPerPage" class="form-select" formControlName="documentListItemPerPage"> <select class="form-select" formControlName="documentListItemPerPage">
<option [ngValue]="10">10</option> <option [ngValue]="10">10</option>
<option [ngValue]="25">25</option> <option [ngValue]="25">25</option>
<option [ngValue]="50">50</option> <option [ngValue]="50">50</option>
@@ -274,10 +274,10 @@
<h5 class="mt-3" i18n>PDF Editor</h5> <h5 class="mt-3" i18n>PDF Editor</h5>
<div class="row"> <div class="row">
<div class="col-md-3 col-form-label pt-0"> <div class="col-md-3 col-form-label pt-0">
<label for="pdfEditorDefaultEditMode" i18n>Default editing mode</label> <span i18n>Default editing mode</span>
</div> </div>
<div class="col"> <div class="col">
<select id="pdfEditorDefaultEditMode" class="form-select" formControlName="pdfEditorDefaultEditMode"> <select class="form-select" formControlName="pdfEditorDefaultEditMode">
<option [ngValue]="PdfEditorEditMode.Create" i18n>Create new document(s)</option> <option [ngValue]="PdfEditorEditMode.Create" i18n>Create new document(s)</option>
<option [ngValue]="PdfEditorEditMode.Update" i18n>Add document version</option> <option [ngValue]="PdfEditorEditMode.Update" i18n>Add document version</option>
</select> </select>
@@ -242,7 +242,7 @@ describe('SettingsComponent', () => {
activatedRoute.snapshot.fragment = '#notifications' activatedRoute.snapshot.fragment = '#notifications'
const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor') const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor')
component.ngOnInit() component.ngOnInit()
expect(component.activeNavID()).toEqual(4) // Notifications expect(component.activeNavID).toEqual(4) // Notifications
component.ngAfterViewInit() component.ngAfterViewInit()
expect(scrollSpy).toHaveBeenCalledWith('#notifications') expect(scrollSpy).toHaveBeenCalledWith('#notifications')
}) })
@@ -399,17 +399,17 @@ describe('SettingsComponent', () => {
completeSetup() completeSetup()
const field = 'storage_path' const field = 'storage_path'
expect( expect(
component.settingsForm.get('documentDetailsHiddenFields').value component.settingsForm.get('documentDetailsHiddenFields').value.length
).toHaveLength(0) ).toEqual(0)
component.toggleDocumentDetailField(field, false) component.toggleDocumentDetailField(field, false)
expect( expect(
component.settingsForm.get('documentDetailsHiddenFields').value component.settingsForm.get('documentDetailsHiddenFields').value.length
).toHaveLength(1) ).toEqual(1)
expect(component.isDocumentDetailFieldShown(field)).toBeFalsy() expect(component.isDocumentDetailFieldShown(field)).toBeFalsy()
component.toggleDocumentDetailField(field, true) component.toggleDocumentDetailField(field, true)
expect( expect(
component.settingsForm.get('documentDetailsHiddenFields').value component.settingsForm.get('documentDetailsHiddenFields').value.length
).toHaveLength(0) ).toEqual(0)
expect(component.isDocumentDetailFieldShown(field)).toBeTruthy() expect(component.isDocumentDetailFieldShown(field)).toBeTruthy()
}) })
}) })
@@ -142,7 +142,7 @@ export class SettingsComponent
private systemStatusService = inject(SystemStatusService) private systemStatusService = inject(SystemStatusService)
private savedViewsService = inject(SavedViewService) private savedViewsService = inject(SavedViewService)
readonly activeNavID = signal<number>(undefined) activeNavID: number
settingsForm = new FormGroup({ settingsForm = new FormGroup({
bulkEditConfirmationDialogs: new FormControl(null), bulkEditConfirmationDialogs: new FormControl(null),
@@ -283,7 +283,7 @@ export class SettingsComponent
(navID) => navID.toLowerCase() == section (navID) => navID.toLowerCase() == section
) )
if (navIDKey) { if (navIDKey) {
this.activeNavID.set(SettingsNavIDs[navIDKey]) this.activeNavID = SettingsNavIDs[navIDKey]
} }
} }
}) })
@@ -386,7 +386,7 @@ export class SettingsComponent
.navigate(['settings', foundNavIDkey.toLowerCase()]) .navigate(['settings', foundNavIDkey.toLowerCase()])
.then((navigated) => { .then((navigated) => {
if (!navigated && this.isDirty) { if (!navigated && this.isDirty) {
this.activeNavID.set(navChangeEvent.activeId) this.activeNavID = navChangeEvent.activeId
} else if (navigated && this.isDirty) { } else if (navigated && this.isDirty) {
this.initialize() this.initialize()
} }
@@ -15,7 +15,7 @@
<i-bs name="check2-all" class="me-1"></i-bs><ng-container i18n>Dismiss all</ng-container> <i-bs name="check2-all" class="me-1"></i-bs><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 class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label> <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
</div> </div>
</div> </div>
@@ -93,8 +93,7 @@
<i-bs width="1em" height="1em" name="x"></i-bs> <i-bs width="1em" height="1em" name="x"></i-bs>
</button> </button>
} }
<label class="visually-hidden" for="taskSearch" i18n>Search tasks</label> <input #filterInput class="form-control form-control-sm" type="text"
<input #filterInput id="taskSearch" class="form-control form-control-sm" type="text"
(keyup)="filterInputKeyup($event)" (keyup)="filterInputKeyup($event)"
[(ngModel)]="filterText"> [(ngModel)]="filterText">
</div> </div>
@@ -212,7 +211,7 @@
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs> <i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</button> </button>
</td> </td>
<td class="actions-col"> <td scope="row" 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> <i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
@@ -150,8 +150,7 @@
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" [disablePopover]="!slimSidebarEnabled" 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> <i-bs class="me-2" name="file-text"></i-bs><span>{{d.title | documentTitle}}</span>
<span class="close flex-column justify-content-center" <span class="close flex-column justify-content-center" (click)="closeDocument(d); $event.preventDefault()">
(click)="$event.preventDefault(); $event.stopPropagation(); closeDocument(d)">
<i-bs name="x"></i-bs> <i-bs name="x"></i-bs>
</span> </span>
</a> </a>
@@ -6,7 +6,7 @@
<div ngbDropdownMenu class="dropdown-menu-end shadow p-3" aria-labelledby="chatDropdown"> <div ngbDropdownMenu class="dropdown-menu-end shadow p-3" aria-labelledby="chatDropdown">
<div class="chat-container bg-light p-2"> <div class="chat-container bg-light p-2">
<div class="chat-messages font-monospace small"> <div class="chat-messages font-monospace small">
@for (message of messages(); track message) { @for (message of messages; track message) {
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'"> <div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
<div class="p-2 m-2" [class.bg-body]="message.role === 'user'"> <div class="p-2 m-2" [class.bg-body]="message.role === 'user'">
<span> <span>
@@ -29,19 +29,16 @@
</div> </div>
<form class="chat-input"> <form class="chat-input">
<label class="visually-hidden" for="chatInput" i18n>Message</label>
<div class="input-group"> <div class="input-group">
<input <input
#chatInput #chatInput
id="chatInput"
class="form-control form-control-sm" name="chatInput" type="text" class="form-control form-control-sm" name="chatInput" type="text"
[placeholder]="placeholder" [placeholder]="placeholder"
[disabled]="loading()" [disabled]="loading"
[ngModel]="input()" [(ngModel)]="input"
(ngModelChange)="input.set($event)"
(keydown)="searchInputKeyDown($event)" (keydown)="searchInputKeyDown($event)"
/> />
<button class="btn btn-sm btn-secondary" type="button" (click)="sendMessage()" [disabled]="loading()">Send</button> <button class="btn btn-sm btn-secondary" type="button" (click)="sendMessage()" [disabled]="loading">Send</button>
</div> </div>
</form> </form>
</div> </div>
@@ -56,53 +56,44 @@ describe('ChatComponent', () => {
it('should update documentId on initialization', () => { it('should update documentId on initialization', () => {
jest.spyOn(router, 'url', 'get').mockReturnValue('/documents/123') jest.spyOn(router, 'url', 'get').mockReturnValue('/documents/123')
component.ngOnInit() component.ngOnInit()
expect(component.documentId()).toBe(123) expect(component.documentId).toBe(123)
}) })
it('should update documentId on navigation', () => { it('should update documentId on navigation', () => {
component.ngOnInit() component.ngOnInit()
routerEvents$.next(new NavigationEnd(1, '/documents/456', '/documents/456')) routerEvents$.next(new NavigationEnd(1, '/documents/456', '/documents/456'))
expect(component.documentId()).toBe(456) expect(component.documentId).toBe(456)
}) })
it('should return correct placeholder based on documentId', () => { it('should return correct placeholder based on documentId', () => {
component.documentId.set(123) component.documentId = 123
expect(component.placeholder).toBe('Ask a question about this document...') expect(component.placeholder).toBe('Ask a question about this document...')
component.documentId.set(undefined) component.documentId = undefined
expect(component.placeholder).toBe('Ask a question about a document...') expect(component.placeholder).toBe('Ask a question about a document...')
}) })
it('should send a message and render the streaming response', async () => { it('should send a message and handle streaming response', () => {
component.input.set('Hello') component.input = 'Hello'
component.sendMessage() component.sendMessage()
expect(component.messages()).toHaveLength(2) expect(component.messages.length).toBe(2)
expect(component.messages()[0].content).toBe('Hello') expect(component.messages[0].content).toBe('Hello')
expect(component.loading()).toBe(true) expect(component.loading).toBe(true)
mockStream$.next('Hi') mockStream$.next('Hi')
expect(component.messages()[1].content).toBe('H') expect(component.messages[1].content).toBe('H')
mockStream$.next('Hi there') mockStream$.next('Hi there')
// advance time to process the typewriter effect // advance time to process the typewriter effect
await jest.runAllTimersAsync() jest.advanceTimersByTime(1000)
await fixture.whenStable() expect(component.messages[1].content).toBe('Hi there')
expect(component.messages()[1].content).toBe('Hi there')
expect(
fixture.nativeElement.querySelector('.chat-messages').textContent
).toContain('Hi there')
mockStream$.complete() mockStream$.complete()
await jest.runAllTimersAsync() expect(component.loading).toBe(false)
await fixture.whenStable() expect(component.messages[1].isStreaming).toBe(false)
expect(component.loading()).toBe(false)
expect(component.messages()[1].isStreaming).toBe(false)
expect(fixture.nativeElement.querySelector('#chatInput').disabled).toBe(
false
)
}) })
it('should parse references from the metadata trailer without showing it', () => { it('should parse references from the metadata trailer without showing it', () => {
component.input.set('Hello') component.input = 'Hello'
component.sendMessage() component.sendMessage()
mockStream$.next( mockStream$.next(
@@ -110,14 +101,14 @@ describe('ChatComponent', () => {
) )
jest.advanceTimersByTime(1000) jest.advanceTimersByTime(1000)
expect(component.messages()[1].content).toBe('Hi there') expect(component.messages[1].content).toBe('Hi there')
expect(component.messages()[1].references).toEqual([ expect(component.messages[1].references).toEqual([
{ id: 42, title: 'Bread Recipe' }, { id: 42, title: 'Bread Recipe' },
]) ])
}) })
it('should render document reference links under assistant messages', () => { it('should render document reference links under assistant messages', () => {
component.input.set('Hello') component.input = 'Hello'
component.sendMessage() component.sendMessage()
mockStream$.next( mockStream$.next(
@@ -132,12 +123,12 @@ describe('ChatComponent', () => {
}) })
it('should remove delimiter fragments that were already streamed', () => { it('should remove delimiter fragments that were already streamed', () => {
component.input.set('Hello') component.input = 'Hello'
component.sendMessage() component.sendMessage()
mockStream$.next(`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`) mockStream$.next(`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`)
jest.advanceTimersByTime(1000) jest.advanceTimersByTime(1000)
expect(component.messages()[1].content).toBe( expect(component.messages[1].content).toBe(
`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}` `Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`
) )
@@ -146,28 +137,28 @@ describe('ChatComponent', () => {
) )
jest.advanceTimersByTime(1000) jest.advanceTimersByTime(1000)
expect(component.messages()[1].content).toBe('Hi there') expect(component.messages[1].content).toBe('Hi there')
expect(component.messages()[1].references).toEqual([ expect(component.messages[1].references).toEqual([
{ id: 42, title: 'Bread Recipe' }, { id: 42, title: 'Bread Recipe' },
]) ])
}) })
it('should handle errors during streaming', () => { it('should handle errors during streaming', () => {
component.input.set('Hello') component.input = 'Hello'
component.sendMessage() component.sendMessage()
mockStream$.error('Error') mockStream$.error('Error')
expect(component.messages()[1].content).toContain( expect(component.messages[1].content).toContain(
'⚠️ Error receiving response.' '⚠️ Error receiving response.'
) )
expect(component.loading()).toBe(false) expect(component.loading).toBe(false)
}) })
it('should enqueue typewriter chunks correctly', () => { it('should enqueue typewriter chunks correctly', () => {
const message = { content: '', role: 'assistant', isStreaming: true } const message = { content: '', role: 'assistant', isStreaming: true }
component.enqueueTypewriter(null, message as any) // coverage for null component.enqueueTypewriter(null, message as any) // coverage for null
component.enqueueTypewriter('Hello', message as any) component.enqueueTypewriter('Hello', message as any)
expect(component['typewriterBuffer']).toHaveLength(4) expect(component['typewriterBuffer'].length).toBe(4)
}) })
it('should scroll to bottom after sending a message', () => { it('should scroll to bottom after sending a message', () => {
@@ -175,7 +166,7 @@ describe('ChatComponent', () => {
ChatComponent.prototype as any, ChatComponent.prototype as any,
'scrollToBottom' 'scrollToBottom'
) )
component.input.set('Test') component.input = 'Test'
component.sendMessage() component.sendMessage()
expect(scrollSpy).toHaveBeenCalled() expect(scrollSpy).toHaveBeenCalled()
}) })
@@ -1,11 +1,4 @@
import { import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
Component,
ElementRef,
inject,
OnInit,
signal,
ViewChild,
} from '@angular/core'
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'
@@ -30,10 +23,10 @@ import {
styleUrl: './chat.component.scss', styleUrl: './chat.component.scss',
}) })
export class ChatComponent implements OnInit { export class ChatComponent implements OnInit {
readonly messages = signal<ChatMessage[]>([]) public messages: ChatMessage[] = []
readonly loading = signal(false) public loading = false
readonly input = signal('') public input: string = ''
readonly documentId = signal<number>(undefined) public documentId!: number
private chatService: ChatService = inject(ChatService) private chatService: ChatService = inject(ChatService)
private router: Router = inject(Router) private router: Router = inject(Router)
@@ -45,7 +38,7 @@ export class ChatComponent implements OnInit {
private typewriterActive = false private typewriterActive = false
public get placeholder(): string { public get placeholder(): string {
return this.documentId() return this.documentId
? $localize`Ask a question about this document...` ? $localize`Ask a question about this document...`
: $localize`Ask a question about a document...` : $localize`Ask a question about a document...`
} }
@@ -64,14 +57,14 @@ export class ChatComponent implements OnInit {
private updateDocumentId(url: string): void { private updateDocumentId(url: string): void {
const docIdRe = url.match(/^\/documents\/(\d+)/) const docIdRe = url.match(/^\/documents\/(\d+)/)
this.documentId.set(docIdRe ? +docIdRe[1] : undefined) this.documentId = docIdRe ? +docIdRe[1] : undefined
} }
sendMessage(): void { sendMessage(): void {
if (!this.input().trim()) return if (!this.input.trim()) return
const userMessage: ChatMessage = { role: 'user', content: this.input() } const userMessage: ChatMessage = { role: 'user', content: this.input }
this.messages.update((messages) => [...messages, userMessage]) this.messages.push(userMessage)
this.scrollToBottom() this.scrollToBottom()
const assistantMessage: ChatMessage = { const assistantMessage: ChatMessage = {
@@ -79,12 +72,12 @@ export class ChatComponent implements OnInit {
content: '', content: '',
isStreaming: true, isStreaming: true,
} }
this.messages.update((messages) => [...messages, assistantMessage]) this.messages.push(assistantMessage)
this.loading.set(true) this.loading = true
let lastVisibleContent = '' let lastVisibleContent = ''
this.chatService.streamChat(this.documentId(), this.input()).subscribe({ this.chatService.streamChat(this.documentId, this.input).subscribe({
next: (chunk) => { next: (chunk) => {
const nextResponse = parseChatResponse(chunk) const nextResponse = parseChatResponse(chunk)
@@ -100,30 +93,26 @@ export class ChatComponent implements OnInit {
} }
assistantMessage.references = nextResponse.references assistantMessage.references = nextResponse.references
this.notifyMessagesChanged()
}, },
error: () => { error: () => {
assistantMessage.content += '\n\n⚠️ Error receiving response.' assistantMessage.content += '\n\n⚠️ Error receiving response.'
assistantMessage.isStreaming = false assistantMessage.isStreaming = false
this.notifyMessagesChanged() this.loading = false
this.loading.set(false)
}, },
complete: () => { complete: () => {
assistantMessage.isStreaming = false assistantMessage.isStreaming = false
this.notifyMessagesChanged() this.loading = false
this.loading.set(false)
this.scrollToBottom() this.scrollToBottom()
}, },
}) })
this.input.set('') this.input = ''
} }
private resetTypewriter(message: ChatMessage, content: string): void { private resetTypewriter(message: ChatMessage, content: string): void {
this.typewriterBuffer = [] this.typewriterBuffer = []
this.typewriterActive = false this.typewriterActive = false
message.content = content message.content = content
this.notifyMessagesChanged()
this.scrollToBottom() this.scrollToBottom()
} }
@@ -146,16 +135,11 @@ export class ChatComponent implements OnInit {
const nextChar = this.typewriterBuffer.shift() const nextChar = this.typewriterBuffer.shift()
message.content += nextChar message.content += nextChar
this.notifyMessagesChanged()
this.scrollToBottom() this.scrollToBottom()
setTimeout(() => this.playTypewriter(message), 10) // 10ms per character setTimeout(() => this.playTypewriter(message), 10) // 10ms per character
} }
private notifyMessagesChanged(): void {
this.messages.update((messages) => [...messages])
}
private scrollToBottom(): void { private scrollToBottom(): void {
setTimeout(() => { setTimeout(() => {
this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' }) this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' })
@@ -1,12 +1,5 @@
import { CurrencyPipe, getLocaleCurrencyCode, SlicePipe } from '@angular/common' import { CurrencyPipe, getLocaleCurrencyCode, SlicePipe } from '@angular/common'
import { import { Component, inject, Input, LOCALE_ID, OnInit } from '@angular/core'
ChangeDetectorRef,
Component,
inject,
Input,
LOCALE_ID,
OnInit,
} from '@angular/core'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
@@ -29,7 +22,6 @@ export class CustomFieldDisplayComponent
{ {
private customFieldService = inject(CustomFieldsService) private customFieldService = inject(CustomFieldsService)
private documentService = inject(DocumentService) private documentService = inject(DocumentService)
private changeDetector = inject(ChangeDetectorRef)
CustomFieldDataType = CustomFieldDataType CustomFieldDataType = CustomFieldDataType
@@ -82,7 +74,6 @@ export class CustomFieldDisplayComponent
this.customFieldService.listAll().subscribe((r) => { this.customFieldService.listAll().subscribe((r) => {
this.customFields = r.results this.customFields = r.results
this.init() this.init()
this.changeDetector.markForCheck()
}) })
} }
@@ -120,7 +111,6 @@ export class CustomFieldDisplayComponent
this.docLinkDocuments = this.value this.docLinkDocuments = this.value
.map((id) => result.results.find((d) => d.id === id)) .map((id) => result.results.find((d) => d.id === id))
.filter((d) => d) .filter((d) => d)
this.changeDetector.markForCheck()
}) })
} }
@@ -95,8 +95,8 @@ describe('CustomFieldsDropdownComponent', () => {
it('should support update unused fields', () => { it('should support update unused fields', () => {
component.existingFields = [{ field: fields[0].id } as any] component.existingFields = [{ field: fields[0].id } as any]
component['updateUnusedFields']() component['updateUnusedFields']()
expect(component['unusedFields']().length).toEqual(1) expect(component['unusedFields'].length).toEqual(1)
expect(component['unusedFields']()[0].name).toEqual('Field 2') expect(component['unusedFields'][0].name).toEqual('Field 2')
}) })
it('should support getting data type label', () => { it('should support getting data type label', () => {
@@ -8,7 +8,6 @@ import {
ViewChild, ViewChild,
ViewChildren, ViewChildren,
inject, inject,
signal,
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -66,11 +65,11 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
@ViewChildren('button') buttons: QueryList<ElementRef> @ViewChildren('button') buttons: QueryList<ElementRef>
private customFields: CustomField[] = [] private customFields: CustomField[] = []
private readonly unusedFields = signal<CustomField[]>([]) private unusedFields: CustomField[] = []
private keyboardIndex: number private keyboardIndex: number
public get filteredFields(): CustomField[] { public get filteredFields(): CustomField[] {
return this.unusedFields().filter( return this.unusedFields.filter(
(f) => !this.filterText || matchesSearchText(f.name, this.filterText) (f) => !this.filterText || matchesSearchText(f.name, this.filterText)
) )
} }
@@ -100,10 +99,8 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
} }
private updateUnusedFields() { private updateUnusedFields() {
this.unusedFields.set( this.unusedFields = this.customFields.filter(
this.customFields.filter( (f) => !this.existingFields?.find((e) => e.field === f.id)
(f) => !this.existingFields?.find((e) => e.field === f.id)
)
) )
} }
@@ -77,7 +77,7 @@
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<ng-select #fieldSelects <ng-select #fieldSelects
class="paperless-input-select" class="paperless-input-select"
[items]="customFields()" [items]="customFields"
[(ngModel)]="atom.field" [(ngModel)]="atom.field"
[disabled]="disabled" [disabled]="disabled"
bindLabel="name" bindLabel="name"
@@ -79,7 +79,7 @@ describe('CustomFieldsQueryDropdownComponent', () => {
}) })
it('should initialize custom fields on creation', () => { it('should initialize custom fields on creation', () => {
expect(component.customFields()).toEqual(customFields) expect(component.customFields).toEqual(customFields)
}) })
it('should add an expression when opened if queries are empty', () => { it('should add an expression when opened if queries are empty', () => {
@@ -101,7 +101,7 @@ describe('CustomFieldsQueryDropdownComponent', () => {
data_type: CustomFieldDataType.String, data_type: CustomFieldDataType.String,
extra_data: {}, extra_data: {},
} }
component.customFields.set([field]) component.customFields = [field]
const operators = component.getOperatorsForField(1) const operators = component.getOperatorsForField(1)
expect(operators.length).toEqual( expect(operators.length).toEqual(
[ [
@@ -138,7 +138,7 @@ describe('CustomFieldsQueryDropdownComponent', () => {
], ],
}, },
} }
component.customFields.set([field]) component.customFields = [field]
const options = component.getSelectOptionsForField(1) const options = component.getSelectOptionsForField(1)
expect(options).toEqual([ expect(options).toEqual([
{ label: 'Option 1', id: 'abc-123' }, { label: 'Option 1', id: 'abc-123' },
@@ -6,7 +6,6 @@ import {
Input, Input,
Output, Output,
QueryList, QueryList,
signal,
ViewChild, ViewChild,
ViewChildren, ViewChildren,
} from '@angular/core' } from '@angular/core'
@@ -279,7 +278,7 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
@Output() @Output()
selectionModelChange = new EventEmitter<CustomFieldQueriesModel>() selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
readonly customFields = signal<CustomField[]>([]) customFields: CustomField[] = []
public readonly today: string = new Date().toLocaleDateString('en-CA') public readonly today: string = new Date().toLocaleDateString('en-CA')
@@ -326,12 +325,12 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
.listAll() .listAll()
.pipe(first(), takeUntil(this.unsubscribeNotifier)) .pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe((result) => { .subscribe((result) => {
this.customFields.set(result.results) this.customFields = result.results
}) })
} }
public getCustomFieldByID(id: number): CustomField { public getCustomFieldByID(id: number): CustomField {
return this.customFields().find((field) => field.id === id) return this.customFields.find((field) => field.id === id)
} }
public addAtom(expression: CustomFieldQueryExpression) { public addAtom(expression: CustomFieldQueryExpression) {
@@ -354,7 +353,7 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
getOperatorsForField( getOperatorsForField(
fieldID: number fieldID: number
): Array<{ value: string; label: string }> { ): Array<{ value: string; label: string }> {
const field = this.customFields().find((field) => field.id === fieldID) const field = this.customFields.find((field) => field.id === fieldID)
const groups: CustomFieldQueryOperatorGroups[] = field const groups: CustomFieldQueryOperatorGroups[] = field
? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type] ? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type]
: [CustomFieldQueryOperatorGroups.Basic] : [CustomFieldQueryOperatorGroups.Basic]
@@ -370,7 +369,7 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
getSelectOptionsForField( getSelectOptionsForField(
fieldID: number fieldID: number
): Array<{ label: string; id: string }> { ): Array<{ label: string; id: string }> {
const field = this.customFields().find((field) => field.id === fieldID) const field = this.customFields.find((field) => field.id === fieldID)
if (field) { if (field) {
return field.extra_data['select_options'] return field.extra_data['select_options']
} }
@@ -22,11 +22,11 @@
<ng-template> <ng-template>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body p-2"> <div class="card-body p-2">
@if (testLoading()) { @if (testLoading) {
<ng-container [ngTemplateOutlet]="loadingTemplate"></ng-container> <ng-container [ngTemplateOutlet]="loadingTemplate"></ng-container>
} @else if (testResult()) { } @else if (testResult) {
<code>{{testResult()}}</code> <code>{{testResult}}</code>
} @else if (testFailed()) { } @else if (testFailed) {
<div class="text-danger" i18n>Path test failed</div> <div class="text-danger" i18n>Path test failed</div>
} @else { } @else {
<div class="text-muted small" i18n>No document selected</div> <div class="text-muted small" i18n>No document selected</div>
@@ -42,7 +42,7 @@
[compareWith]="compareDocuments" [compareWith]="compareDocuments"
[trackByFn]="trackByFn" [trackByFn]="trackByFn"
[minTermLength]="2" [minTermLength]="2"
[loading]="loading()" [loading]="loading"
[typeahead]="documentsInput$" [typeahead]="documentsInput$"
(change)="testPath($event)"> (change)="testPath($event)">
<ng-template #loadingTemplate ng-loadingspinner-tmp> <ng-template #loadingTemplate ng-loadingspinner-tmp>
@@ -58,17 +58,17 @@ describe('StoragePathEditDialogComponent', () => {
fixture.detectChanges() fixture.detectChanges()
component.testPath({ id: 1 }) component.testPath({ id: 1 })
expect(testSpy).toHaveBeenCalledWith('test/{{title}}', 1) expect(testSpy).toHaveBeenCalledWith('test/{{title}}', 1)
expect(component.testResult()).toBe('test/abc123') expect(component.testResult).toBe('test/abc123')
expect(component.testFailed()).toBeFalsy() expect(component.testFailed).toBeFalsy()
// test failed // test failed
testSpy.mockReturnValueOnce(of('')) testSpy.mockReturnValueOnce(of(''))
component.testPath({ id: 1 }) component.testPath({ id: 1 })
expect(component.testResult()).toBeNull() expect(component.testResult).toBeNull()
expect(component.testFailed()).toBeTruthy() expect(component.testFailed).toBeTruthy()
component.testPath(null) component.testPath(null)
expect(component.testResult()).toBeNull() expect(component.testResult).toBeNull()
}) })
it('should compare two documents by id', () => { it('should compare two documents by id', () => {
@@ -1,5 +1,5 @@
import { AsyncPipe, NgTemplateOutlet } from '@angular/common' import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
import { Component, OnDestroy, inject, signal } from '@angular/core' import { Component, OnDestroy, inject } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
@@ -65,9 +65,9 @@ export class StoragePathEditDialogComponent
public documentsInput$ = new Subject<string>() public documentsInput$ = new Subject<string>()
public foundDocuments$: Observable<Document[]> public foundDocuments$: Observable<Document[]>
private testDocument: Document private testDocument: Document
readonly testResult = signal<string>(undefined) public testResult: string
readonly testFailed = signal(false) public testFailed: boolean = false
readonly testLoading = signal(false) public testLoading = false
constructor() { constructor() {
super() super()
@@ -99,22 +99,22 @@ export class StoragePathEditDialogComponent
public testPath(document: Document) { public testPath(document: Document) {
if (!document) { if (!document) {
this.testResult.set(null) this.testResult = null
return return
} }
this.testDocument = document this.testDocument = document
this.testLoading.set(true) this.testLoading = true
;(this.service as StoragePathService) ;(this.service as StoragePathService)
.testPath(this.objectForm.get('path').value, document.id) .testPath(this.objectForm.get('path').value, document.id)
.subscribe((result) => { .subscribe((result) => {
if (result?.length) { if (result?.length) {
this.testResult.set(result) this.testResult = result
this.testFailed.set(false) this.testFailed = false
} else { } else {
this.testResult.set(null) this.testResult = null
this.testFailed.set(true) this.testFailed = true
} }
this.testLoading.set(false) this.testLoading = false
}) })
} }
@@ -12,7 +12,7 @@
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color> <pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>
<pngx-input-select i18n-title title="Parent" formControlName="parent" [items]="tags()" [allowNull]="true" [error]="error?.parent"></pngx-input-select> <pngx-input-select i18n-title title="Parent" formControlName="parent" [items]="tags" [allowNull]="true" [error]="error?.parent"></pngx-input-select>
<pngx-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></pngx-input-check> <pngx-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></pngx-input-check>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@@ -1,4 +1,4 @@
import { Component, inject, signal } from '@angular/core' import { Component, inject } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
@@ -35,7 +35,7 @@ import { TextComponent } from '../../input/text/text.component'
], ],
}) })
export class TagEditDialogComponent extends EditDialogComponent<Tag> { export class TagEditDialogComponent extends EditDialogComponent<Tag> {
readonly tags = signal<Tag[]>([]) tags: Tag[]
constructor() { constructor() {
super() super()
@@ -43,7 +43,7 @@ export class TagEditDialogComponent extends EditDialogComponent<Tag> {
this.userService = inject(UserService) this.userService = inject(UserService)
this.settingsService = inject(SettingsService) this.settingsService = inject(SettingsService)
this.service.listAll().subscribe((result) => { this.service.listAll().subscribe((result) => {
this.tags.set(result.results) this.tags = result.results
}) })
} }
@@ -180,7 +180,7 @@
<div class="col"> <div class="col">
<div class="trigger-filters mb-3"> <div class="trigger-filters mb-3">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="form-label mb-0" i18n>Advanced Filters</span> <label class="form-label mb-0" i18n>Advanced Filters</label>
<button <button
type="button" type="button"
class="btn btn-sm btn-outline-primary ms-auto" class="btn btn-sm btn-outline-primary ms-auto"
@@ -401,7 +401,7 @@ describe('WorkflowEditDialogComponent', () => {
component.addFilter(triggerGroup as FormGroup) component.addFilter(triggerGroup as FormGroup)
const filters = component.getFiltersFormArray(triggerGroup as FormGroup) const filters = component.getFiltersFormArray(triggerGroup as FormGroup)
expect(filters).toHaveLength(3) expect(filters.length).toBe(3)
filters.at(0).get('values').setValue([1]) filters.at(0).get('values').setValue([1])
filters.at(1).get('values').setValue([2, 3]) filters.at(1).get('values').setValue([2, 3])
@@ -672,7 +672,7 @@ describe('WorkflowEditDialogComponent', () => {
} as any } as any
const filters = component['buildFiltersFormArray'](trigger) const filters = component['buildFiltersFormArray'](trigger)
expect(filters).toHaveLength(0) expect(filters.length).toBe(0)
component.filterDefinitions = originalDefinitions component.filterDefinitions = originalDefinitions
}) })
@@ -739,7 +739,7 @@ describe('WorkflowEditDialogComponent', () => {
component.ngOnInit() component.ngOnInit()
const triggerGroup = component.triggerFields.at(0) as FormGroup const triggerGroup = component.triggerFields.at(0) as FormGroup
const filters = component.getFiltersFormArray(triggerGroup) const filters = component.getFiltersFormArray(triggerGroup)
expect(filters).toHaveLength(13) expect(filters.length).toBe(13)
const customFieldFilter = filters.at(12) as FormGroup const customFieldFilter = filters.at(12) as FormGroup
expect(customFieldFilter.get('type').value).toBe( expect(customFieldFilter.get('type').value).toBe(
TriggerFilterType.CustomFieldQuery TriggerFilterType.CustomFieldQuery
@@ -969,12 +969,12 @@ describe('WorkflowEditDialogComponent', () => {
component.addFilter(triggerGroup) component.addFilter(triggerGroup)
component.removeFilter(triggerGroup, 0) component.removeFilter(triggerGroup, 0)
expect(component.getFiltersFormArray(triggerGroup)).toHaveLength(0) expect(component.getFiltersFormArray(triggerGroup).length).toBe(0)
component.addFilter(triggerGroup) component.addFilter(triggerGroup)
const filterArrayAfterAdd = component.getFiltersFormArray(triggerGroup) const filterArrayAfterAdd = component.getFiltersFormArray(triggerGroup)
filterArrayAfterAdd.at(0).get('type').setValue(TriggerFilterType.TagsAll) filterArrayAfterAdd.at(0).get('type').setValue(TriggerFilterType.TagsAll)
expect(component.getFiltersFormArray(triggerGroup)).toHaveLength(1) expect(component.getFiltersFormArray(triggerGroup).length).toBe(1)
}) })
it('should remove selected custom field from the form group', () => { it('should remove selected custom field from the form group', () => {
@@ -933,7 +933,7 @@ export class WorkflowEditDialogComponent
getFilterSelectItems(type: TriggerFilterType) { getFilterSelectItems(type: TriggerFilterType) {
const definition = this.getFilterDefinition(type) const definition = this.getFilterDefinition(type)
if (definition?.inputType !== 'select') { if (!definition || definition.inputType !== 'select') {
return [] return []
} }
@@ -58,8 +58,8 @@
</button> </button>
} }
@if ((selectionModel.items | filter: filterText:'name').length > 0) { @if ((selectionModel.items | filter: filterText:'name').length > 0) {
<button class="list-group-item list-group-item-action bg-light d-flex align-items-center" (click)="applyClicked()" [disabled]="!modelIsDirty() || disabled"> <button class="list-group-item list-group-item-action bg-light d-flex align-items-center" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty()}" i18n>Apply</small> <small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs> <i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
</button> </button>
} }
@@ -221,7 +221,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
.dispatchEvent(new MouseEvent('click')) // open .dispatchEvent(new MouseEvent('click')) // open
selectionModel.toggle(items[0].id) selectionModel.toggle(items[0].id)
fixture.detectChanges() fixture.detectChanges()
expect(component.modelIsDirty()).toBeTruthy() expect(component.modelIsDirty).toBeTruthy()
let applyResult: ChangedItems let applyResult: ChangedItems
const closeSpy = jest.spyOn(component.dropdown, 'close') const closeSpy = jest.spyOn(component.dropdown, 'close')
component.apply.subscribe((result) => (applyResult = result)) component.apply.subscribe((result) => (applyResult = result))
@@ -244,7 +244,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
.dispatchEvent(new MouseEvent('click')) // open .dispatchEvent(new MouseEvent('click')) // open
selectionModel.toggle(items[0].id) selectionModel.toggle(items[0].id)
fixture.detectChanges() fixture.detectChanges()
expect(component.modelIsDirty()).toBeTruthy() expect(component.modelIsDirty).toBeTruthy()
let applyResult: ChangedItems let applyResult: ChangedItems
component.apply.subscribe((result) => (applyResult = result)) component.apply.subscribe((result) => (applyResult = result))
component.dropdown.close() component.dropdown.close()
@@ -12,7 +12,6 @@ import {
Output, Output,
ViewChild, ViewChild,
inject, inject,
signal,
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
@@ -797,7 +796,7 @@ export class FilterableDropdownComponent
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
} }
readonly modelIsDirty = signal(false) modelIsDirty: boolean = false
private keyboardIndex: number private keyboardIndex: number
@@ -812,7 +811,7 @@ export class FilterableDropdownComponent
constructor() { constructor() {
super() super()
this.selectionModelChange.subscribe((updatedModel) => { this.selectionModelChange.subscribe((updatedModel) => {
this.modelIsDirty.set(updatedModel.isDirty()) this.modelIsDirty = updatedModel.isDirty()
}) })
} }
@@ -859,7 +858,7 @@ export class FilterableDropdownComponent
}, 0) }, 0)
if (this.editing) { if (this.editing) {
this.selectionModel.reset() this.selectionModel.reset()
this.modelIsDirty.set(false) this.modelIsDirty = false
} }
this.selectionModel.singleSelect = this.selectionModel.singleSelect =
this.editing && !this.selectionModel.manyToOne this.editing && !this.selectionModel.manyToOne
@@ -5,7 +5,6 @@ import {
inject, inject,
Input, Input,
Output, Output,
signal,
} from '@angular/core' } from '@angular/core'
import { import {
FormsModule, FormsModule,
@@ -64,11 +63,11 @@ export class CustomFieldsValuesComponent extends AbstractInputComponent<Object>
super() super()
customFieldsService.listAll().subscribe((items) => { customFieldsService.listAll().subscribe((items) => {
this.fields.set(items.results) this.fields = items.results
}) })
} }
private readonly fields = signal<CustomField[]>([]) private fields: CustomField[]
private _selectedFields: number[] private _selectedFields: number[]
@@ -91,6 +90,6 @@ export class CustomFieldsValuesComponent extends AbstractInputComponent<Object>
public removeSelectedField: EventEmitter<number> = new EventEmitter<number>() public removeSelectedField: EventEmitter<number> = new EventEmitter<number>()
public getCustomField(id: number): CustomField { public getCustomField(id: number): CustomField {
return this.fields().find((field) => field.id === id) return this.fields.find((field) => field.id === id)
} }
} }
@@ -33,7 +33,7 @@
[compareWith]="compareDocuments" [compareWith]="compareDocuments"
[trackByFn]="trackByFn" [trackByFn]="trackByFn"
[minTermLength]="2" [minTermLength]="2"
[loading]="loading()" [loading]="loading"
[typeahead]="documentsInput$" [typeahead]="documentsInput$"
(mousedown)="$event.stopImmediatePropagation()" (mousedown)="$event.stopImmediatePropagation()"
(change)="onChange(selectedDocumentIDs)"> (change)="onChange(selectedDocumentIDs)">
@@ -2,11 +2,7 @@ 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 { NG_VALUE_ACCESSOR } from '@angular/forms' import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { By } from '@angular/platform-browser' import { of, throwError } from 'rxjs'
import { provideRouter } from '@angular/router'
import { NgSelectComponent } from '@ng-select/ng-select'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of, Subject, throwError } from 'rxjs'
import { FILTER_SIMPLE_TITLE } from 'src/app/data/filter-rule-type' import { FILTER_SIMPLE_TITLE } from 'src/app/data/filter-rule-type'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { DocumentLinkComponent } from './document-link.component' import { DocumentLinkComponent } from './document-link.component'
@@ -37,11 +33,10 @@ describe('DocumentLinkComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [DocumentLinkComponent, NgxBootstrapIconsModule.pick(allIcons)], imports: [DocumentLinkComponent],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
provideRouter([]),
], ],
}) })
documentService = TestBed.inject(DocumentService) documentService = TestBed.inject(DocumentService)
@@ -65,25 +60,6 @@ describe('DocumentLinkComponent', () => {
expect(getSpy).toHaveBeenCalled() expect(getSpy).toHaveBeenCalled()
}) })
it('should render loading and selected documents after async updates', async () => {
const result$ = new Subject<any>()
jest.spyOn(documentService, 'getFew').mockReturnValue(result$)
component.writeValue([1])
await fixture.whenStable()
const select = fixture.debugElement.query(By.directive(NgSelectComponent))
.componentInstance as NgSelectComponent
expect(select.loading()).toBe(true)
result$.next({ count: 1, all: [1], results: [documents[0]] })
result$.complete()
await fixture.whenStable()
expect(select.loading()).toBe(false)
expect(fixture.nativeElement.textContent).toContain(documents[0].title)
})
it('shoud maintain ordering of selected documents', () => { it('shoud maintain ordering of selected documents', () => {
const getSpy = jest.spyOn(documentService, 'getFew') const getSpy = jest.spyOn(documentService, 'getFew')
getSpy.mockImplementation((ids) => { getSpy.mockImplementation((ids) => {
@@ -6,7 +6,6 @@ import {
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
signal,
} from '@angular/core' } from '@angular/core'
import { import {
FormsModule, FormsModule,
@@ -64,7 +63,7 @@ export class DocumentLinkComponent
documentsInput$ = new Subject<string>() documentsInput$ = new Subject<string>()
foundDocuments$: Observable<Document[]> foundDocuments$: Observable<Document[]>
readonly loading = signal(false) loading = false
selectedDocuments: Document[] = [] selectedDocuments: Document[] = []
private unsubscribeNotifier: Subject<any> = new Subject() private unsubscribeNotifier: Subject<any> = new Subject()
@@ -94,12 +93,12 @@ export class DocumentLinkComponent
this.selectedDocuments = [] this.selectedDocuments = []
super.writeValue([]) super.writeValue([])
} else { } else {
this.loading.set(true) this.loading = true
this.documentsService this.documentsService
.getFew(documentIDs, { fields: 'id,title' }) .getFew(documentIDs, { fields: 'id,title' })
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((documentResults) => { .subscribe((documentResults) => {
this.loading.set(false) this.loading = false
this.selectedDocuments = documentIDs.map( this.selectedDocuments = documentIDs.map(
(id) => documentResults.results.find((d) => d.id === id) ?? {} (id) => documentResults.results.find((d) => d.id === id) ?? {}
) )
@@ -114,7 +113,7 @@ export class DocumentLinkComponent
this.documentsInput$.pipe( this.documentsInput$.pipe(
distinctUntilChanged(), distinctUntilChanged(),
takeUntil(this.unsubscribeNotifier), takeUntil(this.unsubscribeNotifier),
tap(() => this.loading.set(true)), tap(() => (this.loading = true)),
switchMap((title) => switchMap((title) =>
this.documentsService this.documentsService
.listFiltered( .listFiltered(
@@ -134,7 +133,7 @@ export class DocumentLinkComponent
) )
), ),
catchError(() => of([])), // empty on error catchError(() => of([])), // empty on error
tap(() => this.loading.set(false)) tap(() => (this.loading = false))
) )
) )
) )
@@ -37,7 +37,6 @@ export class NumberComponent extends AbstractInputComponent<number> {
this.documentService.getNextAsn().subscribe((nextAsn) => { this.documentService.getNextAsn().subscribe((nextAsn) => {
this.value = nextAsn this.value = nextAsn
this.onChange(this.value) this.onChange(this.value)
this.changeDetector.markForCheck()
}) })
} }
@@ -157,8 +157,8 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
private removeChildren(tagIDs: number[], tag: Tag) { private removeChildren(tagIDs: number[], tag: Tag) {
if (tag.children?.length) { if (tag.children?.length) {
const childIDs = new Set(tag.children.map((child) => child.id)) const childIDs = tag.children.map((child) => child.id)
tagIDs = tagIDs.filter((id) => !childIDs.has(id)) tagIDs = tagIDs.filter((id) => !childIDs.includes(id))
for (const child of tag.children) { for (const child of tag.children) {
tagIDs = this.removeChildren(tagIDs, child) tagIDs = this.removeChildren(tagIDs, child)
} }
@@ -14,7 +14,7 @@ import { environment } from 'src/environments/environment'
}) })
export class PageHeaderComponent { export class PageHeaderComponent {
private titleService = inject(Title) private titleService = inject(Title)
private readonly clipboard = inject(Clipboard) private clipboard = inject(Clipboard)
readonly id = input<number>(undefined) readonly id = input<number>(undefined)
readonly subTitle = input('') readonly subTitle = input('')
@@ -49,7 +49,8 @@ describe('PngxPdfViewerComponent', () => {
new URL('assets/js/pdf.worker.min.mjs', document.baseURI).toString() new URL('assets/js/pdf.worker.min.mjs', document.baseURI).toString()
) )
const isVisible = (component as any).findController.onIsPageVisible as const isVisible = (component as any).findController.onIsPageVisible as
(() => boolean) | undefined | (() => boolean)
| undefined
expect(isVisible?.()).toBe(true) expect(isVisible?.()).toBe(true)
expect(loadSpy).toHaveBeenCalledWith( expect(loadSpy).toHaveBeenCalledWith(
expect.objectContaining({ numPages: 1 }) expect.objectContaining({ numPages: 1 })
@@ -60,7 +61,6 @@ describe('PngxPdfViewerComponent', () => {
it('resolves the worker source relative to the document base URI', async () => { it('resolves the worker source relative to the document base URI', async () => {
setBaseHref('/paperless/') setBaseHref('/paperless/')
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
await initComponent() await initComponent()
@@ -70,13 +70,6 @@ describe('PngxPdfViewerComponent', () => {
expect(pdfjs.GlobalWorkerOptions.workerSrc).toContain( expect(pdfjs.GlobalWorkerOptions.workerSrc).toContain(
'/paperless/assets/js/pdf.worker.min.mjs' '/paperless/assets/js/pdf.worker.min.mjs'
) )
expect(getDocumentSpy).toHaveBeenCalledWith({
url: 'test.pdf',
password: undefined,
withCredentials: true,
wasmUrl: expect.stringContaining('/paperless/assets/wasm/'),
iccUrl: expect.stringContaining('/paperless/assets/iccs/'),
})
}) })
it('initializes single-page viewer and disables text layer', async () => { it('initializes single-page viewer and disables text layer', async () => {
@@ -174,12 +174,10 @@ export class PngxPdfViewerComponent
'assets/js/pdf.worker.min.mjs', 'assets/js/pdf.worker.min.mjs',
this.document.baseURI this.document.baseURI
).toString() ).toString()
const initOptions = { let initOptions = {
url: this.src, url: this.src,
password: this.password, password: this.password,
withCredentials: true, withCredentials: true,
wasmUrl: new URL('assets/wasm/', this.document.baseURI).toString(),
iccUrl: new URL('assets/iccs/', this.document.baseURI).toString(),
} }
this.loadingTask = getDocument(initOptions) this.loadingTask = getDocument(initOptions)
try { try {
@@ -68,7 +68,7 @@
[(ngModel)]="selectionModel.includeUsers" [(ngModel)]="selectionModel.includeUsers"
[disabled]="disabled" [disabled]="disabled"
[clearable]="false" [clearable]="false"
[items]="users()" [items]="users"
bindLabel="username" bindLabel="username"
multiple="true" multiple="true"
bindValue="id" bindValue="id"
@@ -1,12 +1,5 @@
import { NgClass } from '@angular/common' import { NgClass } from '@angular/common'
import { import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
Component,
EventEmitter,
Input,
Output,
inject,
signal,
} from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectComponent } from '@ng-select/ng-select' import { NgSelectComponent } from '@ng-select/ng-select'
@@ -82,7 +75,7 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
@Output() @Output()
ownerFilterSet = new EventEmitter<PermissionsSelectionModel>() ownerFilterSet = new EventEmitter<PermissionsSelectionModel>()
readonly users = signal<User[]>([]) users: User[]
hideUnowned: boolean hideUnowned: boolean
@@ -109,7 +102,7 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
next: (result) => this.users.set(result.results), next: (result) => (this.users = result.results),
}) })
} }
} }
@@ -57,15 +57,15 @@
</div> </div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
@if (socialAccounts().length > 0) { @if (socialAccounts?.length > 0) {
<div class="mb-3"> <div class="mb-3">
<p i18n>Connected social accounts</p> <p i18n>Connected social accounts</p>
<ul class="list-group"> <ul class="list-group">
@for (account of socialAccounts(); track account.id) { @for (account of socialAccounts; track account.id) {
<li class="list-group-item" <li class="list-group-item"
ngbPopover="Set a password before disconnecting social account." ngbPopover="Set a password before disconnecting social account."
i18n-ngbPopover i18n-ngbPopover
[disablePopover]="hasUsablePassword()" [disablePopover]="hasUsablePassword"
triggers="mouseenter:mouseleave"> triggers="mouseenter:mouseleave">
{{account.name}} ({{account.provider}}) {{account.name}} ({{account.provider}})
<pngx-confirm-button <pngx-confirm-button
@@ -75,7 +75,7 @@
i18n-title i18n-title
buttonClasses="btn-outline-danger btn-sm ms-2 align-baseline" buttonClasses="btn-outline-danger btn-sm ms-2 align-baseline"
iconName="trash" iconName="trash"
[disabled]="!hasUsablePassword()" [disabled]="!hasUsablePassword"
(confirm)="disconnectSocialAccount(account.id)"> (confirm)="disconnectSocialAccount(account.id)">
</pngx-confirm-button> </pngx-confirm-button>
</li> </li>
@@ -84,11 +84,11 @@
<div class="form-text text-muted text-end fst-italic" i18n>Warning: disconnecting social accounts cannot be undone</div> <div class="form-text text-muted text-end fst-italic" i18n>Warning: disconnecting social accounts cannot be undone</div>
</div> </div>
} }
@if (socialAccountProviders().length > 0) { @if (socialAccountProviders?.length > 0) {
<div class="mb-3"> <div class="mb-3">
<p i18n>Connect new social account</p> <p i18n>Connect new social account</p>
<div class="list-group"> <div class="list-group">
@for (provider of socialAccountProviders(); track provider.name) { @for (provider of socialAccountProviders; track provider.name) {
<a class="list-group-item list-group-item-action text-primary d-flex align-items-center" href="{{ provider.login_url }}" rel="noopener noreferrer"> <a class="list-group-item list-group-item-action text-primary d-flex align-items-center" href="{{ provider.login_url }}" rel="noopener noreferrer">
{{provider.name}}<i-bs class="pb-1 ms-2" name="box-arrow-up-right"></i-bs> {{provider.name}}<i-bs class="pb-1 ms-2" name="box-arrow-up-right"></i-bs>
</a> </a>
@@ -96,7 +96,7 @@
</div> </div>
</div> </div>
} }
@if (!isTotpEnabled()) { @if (!isTotpEnabled) {
<div ngbAccordion> <div ngbAccordion>
<div ngbAccordionItem> <div ngbAccordionItem>
<h2 ngbAccordionHeader> <h2 ngbAccordionHeader>
@@ -105,10 +105,10 @@
<div ngbAccordionCollapse> <div ngbAccordionCollapse>
<div ngbAccordionBody> <div ngbAccordionBody>
<ng-template> <ng-template>
@if (totpSettingsLoading()) { @if (totpSettingsLoading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
} @else if (totpSettings()) { } @else if (totpSettings) {
<figure class="figure"> <figure class="figure">
@if (qrSvgDataUrl) { @if (qrSvgDataUrl) {
<img class="bg-white d-inline-block" [src]="qrSvgDataUrl" alt="Authenticator QR code"> <img class="bg-white d-inline-block" [src]="qrSvgDataUrl" alt="Authenticator QR code">
@@ -116,14 +116,14 @@
<figcaption class="figure-caption text-end mt-2" i18n>Scan the QR code with your authenticator app and then enter the code below</figcaption> <figcaption class="figure-caption text-end mt-2" i18n>Scan the QR code with your authenticator app and then enter the code below</figcaption>
</figure> </figure>
<p> <p>
<ng-container i18n>Authenticator secret</ng-container>: <code>{{totpSettings().secret}}</code>. <ng-container i18n>Authenticator secret</ng-container>: <code>{{totpSettings.secret}}</code>.
<ng-container i18n>You can store this secret and use it to reinstall your authenticator app at a later time.</ng-container> <ng-container i18n>You can store this secret and use it to reinstall your authenticator app at a later time.</ng-container>
</p> </p>
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="text" class="form-control" formControlName="totp_code" placeholder="Code" i18n-placeholder> <input type="text" class="form-control" formControlName="totp_code" placeholder="Code" i18n-placeholder>
<button type="button" class="btn btn-primary ml-auto" (click)="activateTotp()" [disabled]="totpLoading()"> <button type="button" class="btn btn-primary ml-auto" (click)="activateTotp()" [disabled]="totpLoading">
<ng-container i18n>Enable</ng-container> <ng-container i18n>Enable</ng-container>
@if (totpLoading()) { @if (totpLoading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div> <div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>
} }
@@ -137,18 +137,18 @@
</div> </div>
} @else { } @else {
<label class="d-block mb-2" i18n>Two-factor Authentication</label> <label class="d-block mb-2" i18n>Two-factor Authentication</label>
@if (recoveryCodes()) { @if (recoveryCodes) {
<div class="alert alert-warning" role="alert"> <div class="alert alert-warning" role="alert">
<i-bs name="exclamation-triangle" class="me-1"></i-bs><ng-container i18n>Recovery codes will not be shown again, make sure to save them.</ng-container> <i-bs name="exclamation-triangle" class="me-1"></i-bs><ng-container i18n>Recovery codes will not be shown again, make sure to save them.</ng-container>
</div> </div>
<div class="d-flex flex-row align-items-start mb-3"> <div class="d-flex flex-row align-items-start mb-3">
<ul class="list-group w-50"> <ul class="list-group w-50">
@for (code of recoveryCodes(); track code; let i = $index) { @for (code of recoveryCodes; track code; let i = $index) {
@if (i % 2 === 0) { @if (i % 2 === 0) {
<li class="list-group-item d-flex justify-content-around align-items-center"> <li class="list-group-item d-flex justify-content-around align-items-center">
<code>{{code}}</code> <code>{{code}}</code>
@if (recoveryCodes()[i + 1]) { @if (recoveryCodes[i + 1]) {
<code>{{recoveryCodes()[i + 1]}}</code> <code>{{recoveryCodes[i + 1]}}</code>
} }
</li> </li>
} }
@@ -171,7 +171,7 @@
i18n-title i18n-title
buttonClasses="btn-outline-danger btn-sm" buttonClasses="btn-outline-danger btn-sm"
iconName="trash" iconName="trash"
[disabled]="totpLoading()" [disabled]="totpLoading"
(confirm)="deactivateTotp()"> (confirm)="deactivateTotp()">
</pngx-confirm-button> </pngx-confirm-button>
} }
@@ -10,7 +10,7 @@ import {
NgbPopoverModule, NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { NEVER, of, Subject, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { ProfileService } from 'src/app/services/profile.service' import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import * as navUtils from 'src/app/utils/navigation' import * as navUtils from 'src/app/utils/navigation'
@@ -60,10 +60,6 @@ describe('ProfileEditDialogComponent', () => {
providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())], providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())],
}) })
profileService = TestBed.inject(ProfileService) profileService = TestBed.inject(ProfileService)
jest.spyOn(profileService, 'get').mockReturnValue(NEVER)
jest
.spyOn(profileService, 'getSocialAccountProviders')
.mockReturnValue(of([]))
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)
clipboard = TestBed.inject(Clipboard) clipboard = TestBed.inject(Clipboard)
fixture = TestBed.createComponent(ProfileEditDialogComponent) fixture = TestBed.createComponent(ProfileEditDialogComponent)
@@ -159,7 +155,7 @@ describe('ProfileEditDialogComponent', () => {
'getSocialAccountProviders' 'getSocialAccountProviders'
) )
getProvidersSpy.mockReturnValue(of(socialAccountProviders)) getProvidersSpy.mockReturnValue(of(socialAccountProviders))
component.hasUsablePassword.set(true) component.hasUsablePassword = true
component.ngOnInit() component.ngOnInit()
component.form.get('password').patchValue('new*pass') component.form.get('password').patchValue('new*pass')
component.onPasswordKeyUp({ component.onPasswordKeyUp({
@@ -272,27 +268,6 @@ describe('ProfileEditDialogComponent', () => {
expect(getProvidersSpy).toHaveBeenCalled() expect(getProvidersSpy).toHaveBeenCalled()
}) })
it('should render social account providers after an async update', async () => {
const providers$ = new Subject<typeof socialAccountProviders>()
jest.spyOn(profileService, 'get').mockReturnValue(of(profile))
jest
.spyOn(profileService, 'getSocialAccountProviders')
.mockReturnValue(providers$)
component.ngOnInit()
await fixture.whenStable()
expect(
fixture.nativeElement.querySelector('a[href="https://example.com"]')
).toBeNull()
providers$.next(socialAccountProviders)
await fixture.whenStable()
expect(
fixture.nativeElement.querySelector('a[href="https://example.com"]')
).not.toBeNull()
})
it('should remove disconnected social account from component, show error if needed', () => { it('should remove disconnected social account from component, show error if needed', () => {
const disconnectSpy = jest.spyOn(profileService, 'disconnectSocialAccount') const disconnectSpy = jest.spyOn(profileService, 'disconnectSocialAccount')
const getSpy = jest.spyOn(profileService, 'get') const getSpy = jest.spyOn(profileService, 'get')
@@ -301,7 +276,7 @@ describe('ProfileEditDialogComponent', () => {
const errorSpy = jest.spyOn(toastService, 'showError') const errorSpy = jest.spyOn(toastService, 'showError')
expect(component.socialAccounts()).toContainEqual(socialAccount) expect(component.socialAccounts).toContainEqual(socialAccount)
// fail first // fail first
disconnectSpy.mockReturnValueOnce( disconnectSpy.mockReturnValueOnce(
@@ -314,7 +289,7 @@ describe('ProfileEditDialogComponent', () => {
disconnectSpy.mockReturnValue(of(socialAccount.id)) disconnectSpy.mockReturnValue(of(socialAccount.id))
component.disconnectSocialAccount(socialAccount.id) component.disconnectSocialAccount(socialAccount.id)
expect(disconnectSpy).toHaveBeenCalled() expect(disconnectSpy).toHaveBeenCalled()
expect(component.socialAccounts()).not.toContainEqual(socialAccount) expect(component.socialAccounts).not.toContainEqual(socialAccount)
}) })
it('should get totp settings', () => { it('should get totp settings', () => {
@@ -335,7 +310,7 @@ describe('ProfileEditDialogComponent', () => {
getSpy.mockReturnValue(of(settings)) getSpy.mockReturnValue(of(settings))
component.gettotpSettings() component.gettotpSettings()
expect(getSpy).toHaveBeenCalled() expect(getSpy).toHaveBeenCalled()
expect(component.totpSettings()).toEqual(settings) expect(component.totpSettings).toEqual(settings)
}) })
it('should activate totp', () => { it('should activate totp', () => {
@@ -344,15 +319,15 @@ describe('ProfileEditDialogComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const error = new Error('failed to activate totp') const error = new Error('failed to activate totp')
activateSpy.mockReturnValueOnce(throwError(() => error)) activateSpy.mockReturnValueOnce(throwError(() => error))
component.totpSettings.set({ component.totpSettings = {
url: 'http://localhost/', url: 'http://localhost/',
qr_svg: 'svg', qr_svg: 'svg',
secret: 'secret', secret: 'secret',
}) }
component.form.get('totp_code').patchValue('123456') component.form.get('totp_code').patchValue('123456')
component.activateTotp() component.activateTotp()
expect(activateSpy).toHaveBeenCalledWith( expect(activateSpy).toHaveBeenCalledWith(
component.totpSettings().secret, component.totpSettings.secret,
component.form.get('totp_code').value component.form.get('totp_code').value
) )
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
@@ -366,8 +341,8 @@ describe('ProfileEditDialogComponent', () => {
) )
component.activateTotp() component.activateTotp()
expect(toastInfoSpy).toHaveBeenCalled() expect(toastInfoSpy).toHaveBeenCalled()
expect(component.isTotpEnabled()).toBeTruthy() expect(component.isTotpEnabled).toBeTruthy()
expect(component.recoveryCodes()).toEqual(['1', '2', '3']) expect(component.recoveryCodes).toEqual(['1', '2', '3'])
}) })
it('should deactivate totp', () => { it('should deactivate totp', () => {
@@ -387,13 +362,13 @@ describe('ProfileEditDialogComponent', () => {
deactivateSpy.mockReturnValueOnce(of(true)) deactivateSpy.mockReturnValueOnce(of(true))
component.deactivateTotp() component.deactivateTotp()
expect(toastInfoSpy).toHaveBeenCalled() expect(toastInfoSpy).toHaveBeenCalled()
expect(component.isTotpEnabled()).toBeFalsy() expect(component.isTotpEnabled).toBeFalsy()
}) })
it('should copy recovery codes', () => { it('should copy recovery codes', () => {
jest.useFakeTimers() jest.useFakeTimers()
const copySpy = jest.spyOn(clipboard, 'copy') const copySpy = jest.spyOn(clipboard, 'copy')
component.recoveryCodes.set(['1', '2', '3']) component.recoveryCodes = ['1', '2', '3']
component.copyRecoveryCodes() component.copyRecoveryCodes()
expect(copySpy).toHaveBeenCalledWith('1\n2\n3') expect(copySpy).toHaveBeenCalledWith('1\n2\n3')
jest.advanceTimersByTime(3000) jest.advanceTimersByTime(3000)
@@ -56,14 +56,6 @@ export class ProfileEditDialogComponent
readonly showEmailConfirm = signal(false) readonly showEmailConfirm = signal(false)
readonly copied = signal(false) readonly copied = signal(false)
readonly codesCopied = signal(false) readonly codesCopied = signal(false)
readonly hasUsablePassword = signal(false)
readonly isTotpEnabled = signal(false)
readonly totpSettings = signal<TotpSettings>(undefined)
readonly totpSettingsLoading = signal(false)
readonly totpLoading = signal(false)
readonly recoveryCodes = signal<string[]>(undefined)
readonly socialAccounts = signal<SocialAccount[]>([])
readonly socialAccountProviders = signal<SocialAccountProvider[]>([])
public form = new FormGroup({ public form = new FormGroup({
email: new FormControl(''), email: new FormControl(''),
@@ -80,15 +72,25 @@ export class ProfileEditDialogComponent
private newPassword: string private newPassword: string
private passwordConfirm: string private passwordConfirm: string
public hasUsablePassword: boolean = false
private currentEmail: string private currentEmail: string
private newEmail: string private newEmail: string
private emailConfirm: string private emailConfirm: string
public isTotpEnabled: boolean = false
public totpSettings: TotpSettings
public totpSettingsLoading: boolean = false
public totpLoading: boolean = false
public recoveryCodes: string[]
public socialAccounts: SocialAccount[] = []
public socialAccountProviders: SocialAccountProvider[] = []
get qrSvgDataUrl(): string | null { get qrSvgDataUrl(): string | null {
if (!this.totpSettings()?.qr_svg) { if (!this.totpSettings?.qr_svg) {
return null return null
} }
return `data:image/svg+xml;utf8,${encodeURIComponent(this.totpSettings().qr_svg)}` return `data:image/svg+xml;utf8,${encodeURIComponent(this.totpSettings.qr_svg)}`
} }
ngOnInit(): void { ngOnInit(): void {
@@ -105,20 +107,20 @@ export class ProfileEditDialogComponent
this.onEmailChange() this.onEmailChange()
}) })
this.currentPassword = profile.password this.currentPassword = profile.password
this.hasUsablePassword.set(profile.has_usable_password) this.hasUsablePassword = profile.has_usable_password
this.form.get('password').valueChanges.subscribe((newPassword) => { this.form.get('password').valueChanges.subscribe((newPassword) => {
this.newPassword = newPassword this.newPassword = newPassword
this.onPasswordChange() this.onPasswordChange()
}) })
this.socialAccounts.set(profile.social_accounts ?? []) this.socialAccounts = profile.social_accounts
this.isTotpEnabled.set(profile.is_mfa_enabled) this.isTotpEnabled = profile.is_mfa_enabled
}) })
this.profileService this.profileService
.getSocialAccountProviders() .getSocialAccountProviders()
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((providers) => { .subscribe((providers) => {
this.socialAccountProviders.set(providers ?? []) this.socialAccountProviders = providers
}) })
} }
@@ -257,9 +259,7 @@ export class ProfileEditDialogComponent
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({ .subscribe({
next: (id: number) => { next: (id: number) => {
this.socialAccounts.update((accounts) => this.socialAccounts = this.socialAccounts.filter((a) => a.id != id)
accounts.filter((account) => account.id != id)
)
}, },
error: (error) => { error: (error) => {
this.toastService.showError( this.toastService.showError(
@@ -271,39 +271,36 @@ export class ProfileEditDialogComponent
} }
public gettotpSettings(): void { public gettotpSettings(): void {
this.totpSettingsLoading.set(true) this.totpSettingsLoading = true
this.profileService this.profileService
.getTotpSettings() .getTotpSettings()
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({ .subscribe({
next: (totpSettings) => { next: (totpSettings) => {
this.totpSettingsLoading.set(false) this.totpSettingsLoading = false
this.totpSettings.set(totpSettings) this.totpSettings = totpSettings
}, },
error: (error) => { error: (error) => {
this.toastService.showError( this.toastService.showError(
$localize`Error fetching TOTP settings`, $localize`Error fetching TOTP settings`,
error error
) )
this.totpSettingsLoading.set(false) this.totpSettingsLoading = false
}, },
}) })
} }
public activateTotp(): void { public activateTotp(): void {
this.totpLoading.set(true) this.totpLoading = true
this.form.get('totp_code').disable() this.form.get('totp_code').disable()
this.profileService this.profileService
.activateTotp( .activateTotp(this.totpSettings.secret, this.form.get('totp_code').value)
this.totpSettings().secret,
this.form.get('totp_code').value
)
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({ .subscribe({
next: (activationResponse) => { next: (activationResponse) => {
this.totpLoading.set(false) this.totpLoading = false
this.isTotpEnabled.set(activationResponse.success) this.isTotpEnabled = activationResponse.success
this.recoveryCodes.set(activationResponse.recovery_codes) this.recoveryCodes = activationResponse.recovery_codes
this.form.get('totp_code').enable() this.form.get('totp_code').enable()
if (activationResponse.success) { if (activationResponse.success) {
this.toastService.showInfo($localize`TOTP activated successfully`) this.toastService.showInfo($localize`TOTP activated successfully`)
@@ -312,7 +309,7 @@ export class ProfileEditDialogComponent
} }
}, },
error: (error) => { error: (error) => {
this.totpLoading.set(false) this.totpLoading = false
this.form.get('totp_code').enable() this.form.get('totp_code').enable()
this.toastService.showError($localize`Error activating TOTP`, error) this.toastService.showError($localize`Error activating TOTP`, error)
}, },
@@ -320,15 +317,15 @@ export class ProfileEditDialogComponent
} }
public deactivateTotp(): void { public deactivateTotp(): void {
this.totpLoading.set(true) this.totpLoading = true
this.profileService this.profileService
.deactivateTotp() .deactivateTotp()
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({ .subscribe({
next: (success) => { next: (success) => {
this.totpLoading.set(false) this.totpLoading = false
this.isTotpEnabled.set(!success) this.isTotpEnabled = !success
this.recoveryCodes.set(null) this.recoveryCodes = null
if (success) { if (success) {
this.toastService.showInfo($localize`TOTP deactivated successfully`) this.toastService.showInfo($localize`TOTP deactivated successfully`)
} else { } else {
@@ -336,14 +333,14 @@ export class ProfileEditDialogComponent
} }
}, },
error: (error) => { error: (error) => {
this.totpLoading.set(false) this.totpLoading = false
this.toastService.showError($localize`Error deactivating TOTP`, error) this.toastService.showError($localize`Error deactivating TOTP`, error)
}, },
}) })
} }
public copyRecoveryCodes(): void { public copyRecoveryCodes(): void {
this.clipboard.copy(this.recoveryCodes().join('\n')) this.clipboard.copy(this.recoveryCodes.join('\n'))
this.codesCopied.set(true) this.codesCopied.set(true)
setTimeout(() => { setTimeout(() => {
this.codesCopied.set(false) this.codesCopied.set(false)
@@ -65,9 +65,8 @@
<dd class="col-sm-8"><code>{{ createdBundle.slug }}</code></dd> <dd class="col-sm-8"><code>{{ createdBundle.slug }}</code></dd>
<dt class="col-sm-4" i18n>Link</dt> <dt class="col-sm-4" i18n>Link</dt>
<dd class="col-sm-8"> <dd class="col-sm-8">
<label class="visually-hidden" for="shareBundleLink" i18n>Share link</label>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input id="shareBundleLink" class="form-control" type="text" [value]="getShareUrl(createdBundle)" readonly> <input class="form-control" type="text" [value]="getShareUrl(createdBundle)" readonly>
<button <button
class="btn btn-outline-primary" class="btn btn-outline-primary"
type="button" type="button"
@@ -1 +1 @@
<p i18n>Searching document with asn {{asn()}}</p> <p i18n>Searching document with asn {{asn}}</p>
@@ -1,4 +1,4 @@
import { Component, OnInit, inject, signal } from '@angular/core' import { Component, OnInit, inject } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { FILTER_ASN } from '../../data/filter-rule-type' import { FILTER_ASN } from '../../data/filter-rule-type'
import { DocumentService } from '../../services/rest/document.service' import { DocumentService } from '../../services/rest/document.service'
@@ -13,13 +13,13 @@ export class DocumentAsnComponent implements OnInit {
private route = inject(ActivatedRoute) private route = inject(ActivatedRoute)
private router = inject(Router) private router = inject(Router)
readonly asn = signal<string>(undefined) asn: string
ngOnInit(): void { ngOnInit(): void {
this.route.paramMap.subscribe((paramMap) => { this.route.paramMap.subscribe((paramMap) => {
this.asn.set(paramMap.get('id')) this.asn = paramMap.get('id')
this.documentsService this.documentsService
.listAllFilteredIds([{ rule_type: FILTER_ASN, value: this.asn() }]) .listAllFilteredIds([{ rule_type: FILTER_ASN, value: this.asn }])
.subscribe((documentId) => { .subscribe((documentId) => {
if (documentId.length == 1) { if (documentId.length == 1) {
this.router.navigate(['documents', documentId[0]]) this.router.navigate(['documents', documentId[0]])
@@ -315,7 +315,7 @@
<td>{{metadata()?.original_filename}}</td> <td>{{metadata()?.original_filename}}</td>
</tr> </tr>
<tr> <tr>
<td i18n>Original SHA256 checksum</td> <td i18n>Original MD5 checksum</td>
<td>{{metadata()?.original_checksum}}</td> <td>{{metadata()?.original_checksum}}</td>
</tr> </tr>
<tr> <tr>
@@ -328,7 +328,7 @@
</tr> </tr>
@if (metadata()?.has_archive_version) { @if (metadata()?.has_archive_version) {
<tr> <tr>
<td i18n>Archive SHA256 checksum</td> <td i18n>Archive MD5 checksum</td>
<td>{{metadata()?.archive_checksum}}</td> <td>{{metadata()?.archive_checksum}}</td>
</tr> </tr>
} }
@@ -510,7 +510,7 @@ describe('DocumentDetailComponent', () => {
const deleteButtons = fixture.debugElement.queryAll( const deleteButtons = fixture.debugElement.queryAll(
By.css('pngx-confirm-button') By.css('pngx-confirm-button')
) )
expect(deleteButtons).toHaveLength(1) expect(deleteButtons.length).toEqual(1)
}) })
it('should fall back to details tab when duplicates tab is active but no duplicates', () => { it('should fall back to details tab when duplicates tab is active but no duplicates', () => {
@@ -1,6 +1,6 @@
@if (loading()) { @if (loading()) {
<div class="d-flex"> <div class="d-flex">
<output class="spinner-border spinner-border-sm fw-normal"></output> <output class="spinner-border spinner-border-sm fw-normal" role="status"></output>
</div> </div>
} @else { } @else {
<ul class="list-group"> <ul class="list-group">
@@ -26,11 +26,11 @@ import { UserService } from 'src/app/services/rest/user.service'
], ],
}) })
export class DocumentHistoryComponent implements OnInit { export class DocumentHistoryComponent implements OnInit {
private readonly documentService = inject(DocumentService) private documentService = inject(DocumentService)
private readonly correspondentService = inject(CorrespondentService) private correspondentService = inject(CorrespondentService)
private readonly storagePathService = inject(StoragePathService) private storagePathService = inject(StoragePathService)
private readonly documentTypeService = inject(DocumentTypeService) private documentTypeService = inject(DocumentTypeService)
private readonly userService = inject(UserService) private userService = inject(UserService)
public AuditLogAction = AuditLogAction public AuditLogAction = AuditLogAction
@@ -70,7 +70,7 @@ export class DocumentHistoryComponent implements OnInit {
return cached return cached
} }
const idInt = Number.parseInt(id, 10) const idInt = parseInt(id, 10)
const fallback$ = of(id) const fallback$ = of(id)
let result$: Observable<string> let result$: Observable<string>
@@ -5,11 +5,10 @@
</button> </button>
<div class="dropdown-menu shadow" ngbDropdownMenu> <div class="dropdown-menu shadow" ngbDropdownMenu>
<div class="px-3 py-2 mb-2"> <div class="px-3 py-2 mb-2">
@if (versionUploadState() === UploadState.Idle) { @if (versionUploadState === UploadState.Idle) {
<div class="input-group input-group-sm mb-2"> <div class="input-group input-group-sm mb-2">
<label class="input-group-text" for="newVersionLabel" i18n>Label</label> <span class="input-group-text" i18n>Label</span>
<input <input
id="newVersionLabel"
class="form-control" class="form-control"
type="text" type="text"
[(ngModel)]="newVersionLabel" [(ngModel)]="newVersionLabel"
@@ -32,7 +31,7 @@
<i-bs name="file-earmark-plus"></i-bs><span class="ps-1" i18n>Add new version</span> <i-bs name="file-earmark-plus"></i-bs><span class="ps-1" i18n>Add new version</span>
</button> </button>
} @else { } @else {
@switch (versionUploadState()) { @switch (versionUploadState) {
@case (UploadState.Uploading) { @case (UploadState.Uploading) {
<div class="small text-muted mt-1 d-flex align-items-center"> <div class="small text-muted mt-1 d-flex align-items-center">
<output class="spinner-border spinner-border-sm me-2" aria-hidden="true"></output> <output class="spinner-border spinner-border-sm me-2" aria-hidden="true"></output>
@@ -50,8 +49,8 @@
<span i18n>Version upload failed.</span> <span i18n>Version upload failed.</span>
<button type="button" class="btn btn-link btn-sm p-0 ms-2" (click)="clearVersionUploadStatus()" i18n>Dismiss</button> <button type="button" class="btn btn-link btn-sm p-0 ms-2" (click)="clearVersionUploadStatus()" i18n>Dismiss</button>
</div> </div>
@if (versionUploadError()) { @if (versionUploadError) {
<div class="small text-muted mt-1">{{ versionUploadError() }}</div> <div class="small text-muted mt-1">{{ versionUploadError }}</div>
} }
} }
} }
@@ -71,19 +70,15 @@
} }
</div> </div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
@if (isEditingVersion(version.id)) {
<label class="visually-hidden" for="versionLabel{{ version.id }}" i18n>Version label</label>
}
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
@if (isEditingVersion(version.id)) { @if (isEditingVersion(version.id)) {
<input <input
id="versionLabel{{ version.id }}"
class="form-control" class="form-control"
type="text" type="text"
[(ngModel)]="versionLabelDraft" [(ngModel)]="versionLabelDraft"
i18n-placeholder i18n-placeholder
placeholder="Version label" placeholder="Version label"
[disabled]="savingVersionLabelId() !== null" [disabled]="savingVersionLabelId !== null"
(keydown.enter)="submitEditedVersionLabel(version, $event)" (keydown.enter)="submitEditedVersionLabel(version, $event)"
(keydown.escape)="cancelEditingVersion($event)" (keydown.escape)="cancelEditingVersion($event)"
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
@@ -101,7 +96,7 @@
<button <button
type="button" type="button"
class="btn btn-outline-secondary" class="btn btn-outline-secondary"
[disabled]="savingVersionLabelId() !== null" [disabled]="savingVersionLabelId !== null"
(click)="isEditingVersion(version.id) ? submitEditedVersionLabel(version, $event) : beginEditingVersion(version, $event)" (click)="isEditingVersion(version.id) ? submitEditedVersionLabel(version, $event) : beginEditingVersion(version, $event)"
> >
@if (isEditingVersion(version.id)) { @if (isEditingVersion(version.id)) {
@@ -205,7 +205,7 @@ describe('DocumentVersionDropdownComponent', () => {
{ id: 3, is_root: true, checksum: 'aaaa' }, { id: 3, is_root: true, checksum: 'aaaa' },
{ id: 10, is_root: false, checksum: 'bbbb', version_label: 'Updated' }, { id: 10, is_root: false, checksum: 'bbbb', version_label: 'Updated' },
]) ])
expect(component.savingVersionLabelId()).toBeNull() expect(component.savingVersionLabelId).toBeNull()
}) })
it('saveVersionLabel should show error toast on failure', () => { it('saveVersionLabel should show error toast on failure', () => {
@@ -218,7 +218,7 @@ describe('DocumentVersionDropdownComponent', () => {
'Error updating version label', 'Error updating version label',
error error
) )
expect(component.savingVersionLabelId()).toBeNull() expect(component.savingVersionLabelId).toBeNull()
}) })
it('onVersionFileSelected should upload and update versions after websocket success', () => { it('onVersionFileSelected should upload and update versions after websocket success', () => {
@@ -252,11 +252,11 @@ describe('DocumentVersionDropdownComponent', () => {
expect(versionsEmitSpy).toHaveBeenCalledWith(versions) expect(versionsEmitSpy).toHaveBeenCalledWith(versions)
expect(selectedEmitSpy).toHaveBeenCalledWith(20) expect(selectedEmitSpy).toHaveBeenCalledWith(20)
expect(component.newVersionLabel).toEqual('') expect(component.newVersionLabel).toEqual('')
expect(component.versionUploadState()).toEqual(UploadState.Idle) expect(component.versionUploadState).toEqual(UploadState.Idle)
expect(component.versionUploadError()).toBeNull() expect(component.versionUploadError).toBeNull()
}) })
it('onVersionFileSelected should render failed state after websocket failure', async () => { it('onVersionFileSelected should set failed state after websocket failure', () => {
const file = new File(['test'], 'new-version.pdf', { const file = new File(['test'], 'new-version.pdf', {
type: 'application/pdf', type: 'application/pdf',
}) })
@@ -266,11 +266,9 @@ describe('DocumentVersionDropdownComponent', () => {
component.onVersionFileSelected({ target: input } as Event) component.onVersionFileSelected({ target: input } as Event)
failed$.next({ taskId: 'task-1', message: 'processing failed' }) failed$.next({ taskId: 'task-1', message: 'processing failed' })
await fixture.whenStable()
expect(component.versionUploadState()).toEqual(UploadState.Failed) expect(component.versionUploadState).toEqual(UploadState.Failed)
expect(component.versionUploadError()).toEqual('processing failed') expect(component.versionUploadError).toEqual('processing failed')
expect(fixture.nativeElement.textContent).toContain('processing failed')
expect(documentService.getVersions).not.toHaveBeenCalled() expect(documentService.getVersions).not.toHaveBeenCalled()
}) })
@@ -284,8 +282,8 @@ describe('DocumentVersionDropdownComponent', () => {
component.onVersionFileSelected({ target: input } as Event) component.onVersionFileSelected({ target: input } as Event)
expect(component.versionUploadState()).toEqual(UploadState.Failed) expect(component.versionUploadState).toEqual(UploadState.Failed)
expect(component.versionUploadError()).toEqual('Missing task ID.') expect(component.versionUploadError).toEqual('Missing task ID.')
expect(documentService.getVersions).not.toHaveBeenCalled() expect(documentService.getVersions).not.toHaveBeenCalled()
}) })
@@ -300,8 +298,8 @@ describe('DocumentVersionDropdownComponent', () => {
component.onVersionFileSelected({ target: input } as Event) component.onVersionFileSelected({ target: input } as Event)
expect(component.versionUploadState()).toEqual(UploadState.Failed) expect(component.versionUploadState).toEqual(UploadState.Failed)
expect(component.versionUploadError()).toEqual('upload failed') expect(component.versionUploadError).toEqual('upload failed')
expect(toastService.showError).toHaveBeenCalledWith( expect(toastService.showError).toHaveBeenCalledWith(
'Error uploading new version', 'Error uploading new version',
error error
@@ -309,8 +307,8 @@ describe('DocumentVersionDropdownComponent', () => {
}) })
it('ngOnChanges should clear upload status on document switch', () => { it('ngOnChanges should clear upload status on document switch', () => {
component.versionUploadState.set(UploadState.Failed) component.versionUploadState = UploadState.Failed
component.versionUploadError.set('something failed') component.versionUploadError = 'something failed'
component.editingVersionId = 10 component.editingVersionId = 10
component.versionLabelDraft = 'draft' component.versionLabelDraft = 'draft'
@@ -318,8 +316,8 @@ describe('DocumentVersionDropdownComponent', () => {
documentId: new SimpleChange(3, 4, false), documentId: new SimpleChange(3, 4, false),
}) })
expect(component.versionUploadState()).toEqual(UploadState.Idle) expect(component.versionUploadState).toEqual(UploadState.Idle)
expect(component.versionUploadError()).toBeNull() expect(component.versionUploadError).toBeNull()
expect(component.editingVersionId).toBeNull() expect(component.editingVersionId).toBeNull()
expect(component.versionLabelDraft).toEqual('') expect(component.versionLabelDraft).toEqual('')
}) })
@@ -7,7 +7,6 @@ import {
OnChanges, OnChanges,
OnDestroy, OnDestroy,
Output, Output,
signal,
SimpleChanges, SimpleChanges,
} from '@angular/core' } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
@@ -60,9 +59,9 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
@Output() versionsUpdated = new EventEmitter<DocumentVersionInfo[]>() @Output() versionsUpdated = new EventEmitter<DocumentVersionInfo[]>()
newVersionLabel: string = '' newVersionLabel: string = ''
readonly versionUploadState = signal(UploadState.Idle) versionUploadState: UploadState = UploadState.Idle
readonly versionUploadError = signal<string | null>(null) versionUploadError: string | null = null
readonly savingVersionLabelId = signal<number | null>(null) savingVersionLabelId: number | null = null
editingVersionId: number | null = null editingVersionId: number | null = null
versionLabelDraft: string = '' versionLabelDraft: string = ''
@@ -102,7 +101,7 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
beginEditingVersion(version: DocumentVersionInfo, event?: Event): void { beginEditingVersion(version: DocumentVersionInfo, event?: Event): void {
event?.preventDefault() event?.preventDefault()
event?.stopPropagation() event?.stopPropagation()
if (!this.canEditLabels || this.savingVersionLabelId() !== null) return if (!this.canEditLabels || this.savingVersionLabelId !== null) return
this.editingVersionId = version.id this.editingVersionId = version.id
this.versionLabelDraft = version.version_label ?? '' this.versionLabelDraft = version.version_label ?? ''
} }
@@ -117,7 +116,7 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
submitEditedVersionLabel(version: DocumentVersionInfo, event?: Event): void { submitEditedVersionLabel(version: DocumentVersionInfo, event?: Event): void {
event?.preventDefault() event?.preventDefault()
event?.stopPropagation() event?.stopPropagation()
if (this.savingVersionLabelId() !== null) return if (this.savingVersionLabelId !== null) return
const nextLabel = this.versionLabelDraft?.trim() || null const nextLabel = this.versionLabelDraft?.trim() || null
const currentLabel = version.version_label?.trim() || null const currentLabel = version.version_label?.trim() || null
if (nextLabel === currentLabel) { if (nextLabel === currentLabel) {
@@ -159,15 +158,15 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
} }
saveVersionLabel(versionId: number, versionLabel: string | null): void { saveVersionLabel(versionId: number, versionLabel: string | null): void {
if (this.savingVersionLabelId() !== null) return if (this.savingVersionLabelId !== null) return
this.savingVersionLabelId.set(versionId) this.savingVersionLabelId = versionId
this.documentsService this.documentsService
.updateVersionLabel(this.documentId, versionId, versionLabel) .updateVersionLabel(this.documentId, versionId, versionLabel)
.pipe( .pipe(
first(), first(),
finalize(() => { finalize(() => {
if (this.savingVersionLabelId() === versionId) { if (this.savingVersionLabelId === versionId) {
this.savingVersionLabelId.set(null) this.savingVersionLabelId = null
} }
}), }),
takeUntil(this.destroy$) takeUntil(this.destroy$)
@@ -200,8 +199,8 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
const file = input.files[0] const file = input.files[0]
input.value = '' input.value = ''
const label = this.newVersionLabel?.trim() const label = this.newVersionLabel?.trim()
this.versionUploadState.set(UploadState.Uploading) this.versionUploadState = UploadState.Uploading
this.versionUploadError.set(null) this.versionUploadError = null
this.documentsService this.documentsService
.uploadVersion(uploadDocumentId, file, label) .uploadVersion(uploadDocumentId, file, label)
.pipe( .pipe(
@@ -211,7 +210,7 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
$localize`Uploading new version. Processing will happen in the background.` $localize`Uploading new version. Processing will happen in the background.`
) )
this.newVersionLabel = '' this.newVersionLabel = ''
this.versionUploadState.set(UploadState.Processing) this.versionUploadState = UploadState.Processing
}), }),
map((taskId) => map((taskId) =>
typeof taskId === 'string' typeof taskId === 'string'
@@ -220,8 +219,8 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
), ),
switchMap((taskId) => { switchMap((taskId) => {
if (!taskId) { if (!taskId) {
this.versionUploadState.set(UploadState.Failed) this.versionUploadState = UploadState.Failed
this.versionUploadError.set($localize`Missing task ID.`) this.versionUploadError = $localize`Missing task ID.`
return of(null) return of(null)
} }
return merge( return merge(
@@ -241,10 +240,9 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
switchMap((result) => { switchMap((result) => {
if (result?.state !== 'success') { if (result?.state !== 'success') {
if (result?.state === 'failed') { if (result?.state === 'failed') {
this.versionUploadState.set(UploadState.Failed) this.versionUploadState = UploadState.Failed
this.versionUploadError.set( this.versionUploadError =
result.message || $localize`Upload failed.` result.message || $localize`Upload failed.`
)
} }
return of(null) return of(null)
} }
@@ -266,10 +264,8 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
}, },
error: (error) => { error: (error) => {
if (uploadDocumentId !== this.documentId) return if (uploadDocumentId !== this.documentId) return
this.versionUploadState.set(UploadState.Failed) this.versionUploadState = UploadState.Failed
this.versionUploadError.set( this.versionUploadError = error?.message || $localize`Upload failed.`
error?.message || $localize`Upload failed.`
)
this.toastService.showError( this.toastService.showError(
$localize`Error uploading new version`, $localize`Error uploading new version`,
error error
@@ -279,7 +275,7 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
} }
clearVersionUploadStatus(): void { clearVersionUploadStatus(): void {
this.versionUploadState.set(UploadState.Idle) this.versionUploadState = UploadState.Idle
this.versionUploadError.set(null) this.versionUploadError = null
} }
} }
@@ -10,7 +10,7 @@
[createRef]="createTag.bind(this)" [createRef]="createTag.bind(this)"
(opened)="openTagsDropdown()" (opened)="openTagsDropdown()"
[(selectionModel)]="tagSelectionModel" [(selectionModel)]="tagSelectionModel"
[documentCounts]="tagDocumentCounts()" [documentCounts]="tagDocumentCounts"
(apply)="setTags($event)" (apply)="setTags($event)"
shortcutKey="t"> shortcutKey="t">
</pngx-filterable-dropdown> </pngx-filterable-dropdown>
@@ -24,7 +24,7 @@
[createRef]="createCorrespondent.bind(this)" [createRef]="createCorrespondent.bind(this)"
(opened)="openCorrespondentDropdown()" (opened)="openCorrespondentDropdown()"
[(selectionModel)]="correspondentSelectionModel" [(selectionModel)]="correspondentSelectionModel"
[documentCounts]="correspondentDocumentCounts()" [documentCounts]="correspondentDocumentCounts"
(apply)="setCorrespondents($event)" (apply)="setCorrespondents($event)"
shortcutKey="y"> shortcutKey="y">
</pngx-filterable-dropdown> </pngx-filterable-dropdown>
@@ -38,7 +38,7 @@
[createRef]="createDocumentType.bind(this)" [createRef]="createDocumentType.bind(this)"
(opened)="openDocumentTypeDropdown()" (opened)="openDocumentTypeDropdown()"
[(selectionModel)]="documentTypeSelectionModel" [(selectionModel)]="documentTypeSelectionModel"
[documentCounts]="documentTypeDocumentCounts()" [documentCounts]="documentTypeDocumentCounts"
(apply)="setDocumentTypes($event)" (apply)="setDocumentTypes($event)"
shortcutKey="u"> shortcutKey="u">
</pngx-filterable-dropdown> </pngx-filterable-dropdown>
@@ -52,7 +52,7 @@
[createRef]="createStoragePath.bind(this)" [createRef]="createStoragePath.bind(this)"
(opened)="openStoragePathDropdown()" (opened)="openStoragePathDropdown()"
[(selectionModel)]="storagePathsSelectionModel" [(selectionModel)]="storagePathsSelectionModel"
[documentCounts]="storagePathDocumentCounts()" [documentCounts]="storagePathDocumentCounts"
(apply)="setStoragePaths($event)" (apply)="setStoragePaths($event)"
shortcutKey="i"> shortcutKey="i">
</pngx-filterable-dropdown> </pngx-filterable-dropdown>
@@ -66,7 +66,7 @@
[createRef]="createCustomField.bind(this)" [createRef]="createCustomField.bind(this)"
(opened)="openCustomFieldsDropdown()" (opened)="openCustomFieldsDropdown()"
[(selectionModel)]="customFieldsSelectionModel" [(selectionModel)]="customFieldsSelectionModel"
[documentCounts]="customFieldDocumentCounts()" [documentCounts]="customFieldDocumentCounts"
extraButtonTitle="Set values" extraButtonTitle="Set values"
i18n-extraButtonTitle i18n-extraButtonTitle
(extraButton)="setCustomFieldValues($event)" (extraButton)="setCustomFieldValues($event)"
@@ -124,11 +124,11 @@
</div> </div>
</div> </div>
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload()" (click)="downloadSelected()"> <button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
@if (!awaitingDownload()) { @if (!awaitingDownload) {
<i-bs name="arrow-down"></i-bs> <i-bs name="arrow-down"></i-bs>
} }
@if (awaitingDownload()) { @if (awaitingDownload) {
<div class="spinner-border spinner-border-sm" role="status"> <div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Preparing download...</span> <span class="visually-hidden">Preparing download...</span>
</div> </div>
@@ -303,7 +303,7 @@ describe('BulkEditorComponent', () => {
component.openDocumentTypeDropdown() component.openDocumentTypeDropdown()
expect(getSelectionDataSpy).not.toHaveBeenCalled() expect(getSelectionDataSpy).not.toHaveBeenCalled()
expect(component.documentTypeDocumentCounts()).toEqual( expect(component.documentTypeDocumentCounts).toEqual(
selectionData.selected_document_types selectionData.selected_document_types
) )
}) })
@@ -320,7 +320,7 @@ describe('BulkEditorComponent', () => {
component.openCorrespondentDropdown() component.openCorrespondentDropdown()
expect(getSelectionDataSpy).not.toHaveBeenCalled() expect(getSelectionDataSpy).not.toHaveBeenCalled()
expect(component.correspondentDocumentCounts()).toEqual( expect(component.correspondentDocumentCounts).toEqual(
selectionData.selected_correspondents selectionData.selected_correspondents
) )
}) })
@@ -337,7 +337,7 @@ describe('BulkEditorComponent', () => {
component.openStoragePathDropdown() component.openStoragePathDropdown()
expect(getSelectionDataSpy).not.toHaveBeenCalled() expect(getSelectionDataSpy).not.toHaveBeenCalled()
expect(component.storagePathDocumentCounts()).toEqual( expect(component.storagePathDocumentCounts).toEqual(
selectionData.selected_storage_paths selectionData.selected_storage_paths
) )
}) })
@@ -354,7 +354,7 @@ describe('BulkEditorComponent', () => {
component.openCustomFieldsDropdown() component.openCustomFieldsDropdown()
expect(getSelectionDataSpy).not.toHaveBeenCalled() expect(getSelectionDataSpy).not.toHaveBeenCalled()
expect(component.customFieldDocumentCounts()).toEqual( expect(component.customFieldDocumentCounts).toEqual(
selectionData.selected_custom_fields selectionData.selected_custom_fields
) )
}) })
@@ -1,11 +1,4 @@
import { import { Component, inject, Input, OnDestroy, OnInit } from '@angular/core'
Component,
inject,
Input,
OnDestroy,
OnInit,
signal,
} from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
@@ -108,12 +101,12 @@ export class BulkEditorComponent
documentTypeSelectionModel = new FilterableDropdownSelectionModel() documentTypeSelectionModel = new FilterableDropdownSelectionModel()
storagePathsSelectionModel = new FilterableDropdownSelectionModel() storagePathsSelectionModel = new FilterableDropdownSelectionModel()
customFieldsSelectionModel = new FilterableDropdownSelectionModel(true) customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
readonly tagDocumentCounts = signal<SelectionDataItem[]>(undefined) tagDocumentCounts: SelectionDataItem[]
readonly correspondentDocumentCounts = signal<SelectionDataItem[]>(undefined) correspondentDocumentCounts: SelectionDataItem[]
readonly documentTypeDocumentCounts = signal<SelectionDataItem[]>(undefined) documentTypeDocumentCounts: SelectionDataItem[]
readonly storagePathDocumentCounts = signal<SelectionDataItem[]>(undefined) storagePathDocumentCounts: SelectionDataItem[]
readonly customFieldDocumentCounts = signal<SelectionDataItem[]>(undefined) customFieldDocumentCounts: SelectionDataItem[]
readonly awaitingDownload = signal(false) awaitingDownload: boolean
unsubscribeNotifier: Subject<any> = new Subject() unsubscribeNotifier: Subject<any> = new Subject()
@@ -372,8 +365,8 @@ export class BulkEditorComponent
openTagsDropdown() { openTagsDropdown() {
if (this.list.allSelected) { if (this.list.allSelected) {
const selectionData = this.list.selectionData const selectionData = this.list.selectionData
this.tagDocumentCounts.set(selectionData?.selected_tags ?? []) this.tagDocumentCounts = selectionData?.selected_tags ?? []
this.applySelectionData(this.tagDocumentCounts(), this.tagSelectionModel) this.applySelectionData(this.tagDocumentCounts, this.tagSelectionModel)
return return
} }
@@ -381,7 +374,7 @@ export class BulkEditorComponent
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first()) .pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.tagDocumentCounts.set(s.selected_tags) this.tagDocumentCounts = s.selected_tags
this.applySelectionData(s.selected_tags, this.tagSelectionModel) this.applySelectionData(s.selected_tags, this.tagSelectionModel)
}) })
} }
@@ -389,11 +382,10 @@ export class BulkEditorComponent
openDocumentTypeDropdown() { openDocumentTypeDropdown() {
if (this.list.allSelected) { if (this.list.allSelected) {
const selectionData = this.list.selectionData const selectionData = this.list.selectionData
this.documentTypeDocumentCounts.set( this.documentTypeDocumentCounts =
selectionData?.selected_document_types ?? [] selectionData?.selected_document_types ?? []
)
this.applySelectionData( this.applySelectionData(
this.documentTypeDocumentCounts(), this.documentTypeDocumentCounts,
this.documentTypeSelectionModel this.documentTypeSelectionModel
) )
return return
@@ -403,7 +395,7 @@ export class BulkEditorComponent
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first()) .pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.documentTypeDocumentCounts.set(s.selected_document_types) this.documentTypeDocumentCounts = s.selected_document_types
this.applySelectionData( this.applySelectionData(
s.selected_document_types, s.selected_document_types,
this.documentTypeSelectionModel this.documentTypeSelectionModel
@@ -414,11 +406,10 @@ export class BulkEditorComponent
openCorrespondentDropdown() { openCorrespondentDropdown() {
if (this.list.allSelected) { if (this.list.allSelected) {
const selectionData = this.list.selectionData const selectionData = this.list.selectionData
this.correspondentDocumentCounts.set( this.correspondentDocumentCounts =
selectionData?.selected_correspondents ?? [] selectionData?.selected_correspondents ?? []
)
this.applySelectionData( this.applySelectionData(
this.correspondentDocumentCounts(), this.correspondentDocumentCounts,
this.correspondentSelectionModel this.correspondentSelectionModel
) )
return return
@@ -428,7 +419,7 @@ export class BulkEditorComponent
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first()) .pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.correspondentDocumentCounts.set(s.selected_correspondents) this.correspondentDocumentCounts = s.selected_correspondents
this.applySelectionData( this.applySelectionData(
s.selected_correspondents, s.selected_correspondents,
this.correspondentSelectionModel this.correspondentSelectionModel
@@ -439,11 +430,10 @@ export class BulkEditorComponent
openStoragePathDropdown() { openStoragePathDropdown() {
if (this.list.allSelected) { if (this.list.allSelected) {
const selectionData = this.list.selectionData const selectionData = this.list.selectionData
this.storagePathDocumentCounts.set( this.storagePathDocumentCounts =
selectionData?.selected_storage_paths ?? [] selectionData?.selected_storage_paths ?? []
)
this.applySelectionData( this.applySelectionData(
this.storagePathDocumentCounts(), this.storagePathDocumentCounts,
this.storagePathsSelectionModel this.storagePathsSelectionModel
) )
return return
@@ -453,7 +443,7 @@ export class BulkEditorComponent
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first()) .pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.storagePathDocumentCounts.set(s.selected_storage_paths) this.storagePathDocumentCounts = s.selected_storage_paths
this.applySelectionData( this.applySelectionData(
s.selected_storage_paths, s.selected_storage_paths,
this.storagePathsSelectionModel this.storagePathsSelectionModel
@@ -464,11 +454,10 @@ export class BulkEditorComponent
openCustomFieldsDropdown() { openCustomFieldsDropdown() {
if (this.list.allSelected) { if (this.list.allSelected) {
const selectionData = this.list.selectionData const selectionData = this.list.selectionData
this.customFieldDocumentCounts.set( this.customFieldDocumentCounts =
selectionData?.selected_custom_fields ?? [] selectionData?.selected_custom_fields ?? []
)
this.applySelectionData( this.applySelectionData(
this.customFieldDocumentCounts(), this.customFieldDocumentCounts,
this.customFieldsSelectionModel this.customFieldsSelectionModel
) )
return return
@@ -478,7 +467,7 @@ export class BulkEditorComponent
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first()) .pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.customFieldDocumentCounts.set(s.selected_custom_fields) this.customFieldDocumentCounts = s.selected_custom_fields
this.applySelectionData( this.applySelectionData(
s.selected_custom_fields, s.selected_custom_fields,
this.customFieldsSelectionModel this.customFieldsSelectionModel
@@ -887,7 +876,7 @@ export class BulkEditorComponent
} }
downloadSelected() { downloadSelected() {
this.awaitingDownload.set(true) this.awaitingDownload = true
let downloadFileType: string = let downloadFileType: string =
this.downloadForm.get('downloadFileTypeArchive').value && this.downloadForm.get('downloadFileTypeArchive').value &&
this.downloadForm.get('downloadFileTypeOriginals').value this.downloadForm.get('downloadFileTypeOriginals').value
@@ -904,7 +893,7 @@ export class BulkEditorComponent
.pipe(first()) .pipe(first())
.subscribe((result: any) => { .subscribe((result: any) => {
saveAs(result, 'documents.zip') saveAs(result, 'documents.zip')
this.awaitingDownload.set(false) this.awaitingDownload = false
}) })
} }
@@ -115,7 +115,7 @@
</pngx-page-header> </pngx-page-header>
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body"> <div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [filterRules]="list.filterRules" (filterRulesChange)="onFilterRulesChange($event)" (resetFilterRules)="onFilterRulesReset($event)" [unmodifiedFilterRules]="unmodifiedFilterRules()" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor> <pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [filterRules]="list.filterRules" (filterRulesChange)="onFilterRulesChange($event)" (resetFilterRules)="onFilterRulesReset($event)" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor> <pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
</div> </div>
@@ -5,7 +5,6 @@ import {
OnDestroy, OnDestroy,
OnInit, OnInit,
QueryList, QueryList,
signal,
ViewChild, ViewChild,
ViewChildren, ViewChildren,
} from '@angular/core' } from '@angular/core'
@@ -150,7 +149,7 @@ export class DocumentListComponent
) )
} }
readonly unmodifiedFilterRules = signal<FilterRule[]>([]) unmodifiedFilterRules: FilterRule[] = []
private unmodifiedSavedView: SavedView private unmodifiedSavedView: SavedView
private activeSavedView: SavedView | null = null private activeSavedView: SavedView | null = null
@@ -300,7 +299,7 @@ export class DocumentListComponent
this.savedViewService.setDocumentCount(view, this.list.collectionSize) this.savedViewService.setDocumentCount(view, this.list.collectionSize)
}) })
this.updateDisplayCustomFields() this.updateDisplayCustomFields()
this.unmodifiedFilterRules.set(view.filter_rules) this.unmodifiedFilterRules = view.filter_rules
}) })
this.route.queryParamMap this.route.queryParamMap
@@ -317,7 +316,7 @@ export class DocumentListComponent
this.activeSavedView = null this.activeSavedView = null
this.list.activateSavedView(null) this.list.activateSavedView(null)
this.list.loadFromQueryParams(queryParams) this.list.loadFromQueryParams(queryParams)
this.unmodifiedFilterRules.set([]) this.unmodifiedFilterRules = []
} }
}) })
@@ -416,7 +415,7 @@ export class DocumentListComponent
this.toastService.showInfo( this.toastService.showInfo(
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.` $localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
) )
this.unmodifiedFilterRules.set(this.list.filterRules) this.unmodifiedFilterRules = this.list.filterRules
}, },
error: (err) => { error: (err) => {
this.toastService.showError( this.toastService.showError(
@@ -80,7 +80,7 @@
shortcutKey="i"></pngx-filterable-dropdown> shortcutKey="i"></pngx-filterable-dropdown>
} }
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.CustomField) && customFields().length > 0) { @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.CustomField) && customFields.length > 0) {
<pngx-custom-fields-query-dropdown class="flex-fill fade" [class.show]="show()" title="Custom fields" icon="ui-radios" i18n-title <pngx-custom-fields-query-dropdown class="flex-fill fade" [class.show]="show()" title="Custom fields" icon="ui-radios" i18n-title
[(selectionModel)]="customFieldQueriesModel" [(selectionModel)]="customFieldQueriesModel"
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
@@ -328,7 +328,7 @@ describe('FilterEditorComponent', () => {
}) })
it('should ingest legacy text filter rules for doc title + content', () => { it('should ingest legacy text filter rules for doc title + content', () => {
expect(component.textFilter).toBeNull() expect(component.textFilter).toEqual(null)
component.filterRules = [ component.filterRules = [
{ {
rule_type: FILTER_TITLE_CONTENT, rule_type: FILTER_TITLE_CONTENT,
@@ -1,6 +1,5 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
@@ -10,7 +9,6 @@ import {
Output, Output,
ViewChild, ViewChild,
inject, inject,
signal,
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { import {
@@ -351,7 +349,7 @@ export class FilterEditorComponent
@ViewChild('textFilterInput') @ViewChild('textFilterInput')
textFilterInput: ElementRef textFilterInput: ElementRef
readonly customFields = signal<CustomField[]>([]) customFields: CustomField[] = []
tagDocumentCounts: SelectionDataItem[] tagDocumentCounts: SelectionDataItem[]
correspondentDocumentCounts: SelectionDataItem[] correspondentDocumentCounts: SelectionDataItem[]
@@ -516,7 +514,6 @@ export class FilterEditorComponent
this.documentService.get(this._moreLikeId).subscribe((result) => { this.documentService.get(this._moreLikeId).subscribe((result) => {
this._moreLikeDoc = result this._moreLikeDoc = result
this._textFilter = result.title this._textFilter = result.title
this.changeDetector.markForCheck()
}) })
break break
case FILTER_CREATED_AFTER: case FILTER_CREATED_AFTER:
@@ -1165,7 +1162,6 @@ export class FilterEditorComponent
private loadingCountTotal: number = 0 private loadingCountTotal: number = 0
private loadingCount: number = 0 private loadingCount: number = 0
private readonly changeDetector = inject(ChangeDetectorRef)
private maybeCompleteLoading() { private maybeCompleteLoading() {
this.loadingCount++ this.loadingCount++
@@ -1233,7 +1229,7 @@ export class FilterEditorComponent
) { ) {
this.loadingCountTotal++ this.loadingCountTotal++
this.customFieldService.listAll().subscribe((result) => { this.customFieldService.listAll().subscribe((result) => {
this.customFields.set(result.results) this.customFields = result.results
this.maybeCompleteLoading() this.maybeCompleteLoading()
}) })
} }
@@ -1,18 +1,18 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col mb-2 mb-xl-0"> <div class="col mb-2 mb-xl-0">
<div class="form-inline d-flex align-items-center"> <div class="form-inline d-flex align-items-center">
<label class="text-muted me-2 mb-0" for="managementNameFilter" i18n>Filter by:</label> <label class="text-muted me-2 mb-0" i18n>Filter by:</label>
<input id="managementNameFilter" class="form-control form-control-sm flex-fill w-auto" type="text" autofocus [(ngModel)]="nameFilter" (keyup)="onNameFilterKeyUp($event)" placeholder="Name" i18n-placeholder> <input class="form-control form-control-sm flex-fill w-auto" type="text" autofocus [(ngModel)]="nameFilter" (keyup)="onNameFilterKeyUp($event)" placeholder="Name" i18n-placeholder>
</div> </div>
</div> </div>
<div class="col-auto mb-2 mb-xl-0"> <div class="col-auto mb-2 mb-xl-0">
<div class="form-inline d-flex align-items-center"> <div class="form-inline d-flex align-items-center">
<div class="input-group input-group-sm w-auto d-none d-md-flex"> <div class="input-group input-group-sm w-auto d-none d-md-flex">
<label class="input-group-text border-0" for="managementPageSize" i18n>Show:</label> <span class="input-group-text border-0" i18n>Show:</span>
</div> </div>
<div class="input-group input-group-sm w-auto me-3"> <div class="input-group input-group-sm w-auto me-3">
<select id="managementPageSize" class="form-select form-select-sm small" [(ngModel)]="pageSize"> <select class="form-select form-select-sm small" [(ngModel)]="pageSize">
<option [ngValue]="25">25</option> <option [ngValue]="25">25</option>
<option [ngValue]="50">50</option> <option [ngValue]="50">50</option>
<option [ngValue]="100">100</option> <option [ngValue]="100">100</option>
@@ -83,12 +83,12 @@ describe('TagListComponent', () => {
] ]
component['_nameFilter'] = null // Simulate empty name filter component['_nameFilter'] = null // Simulate empty name filter
const filtered = component.filterData(tags as any) const filtered = component.filterData(tags as any)
expect(filtered).toHaveLength(2) expect(filtered.length).toBe(2)
expect(filtered.find((t) => t.id === 2)).toBeUndefined() expect(filtered.find((t) => t.id === 2)).toBeUndefined()
component['_nameFilter'] = 'Tag2' // Simulate non-empty name filter component['_nameFilter'] = 'Tag2' // Simulate non-empty name filter
const filteredWithName = component.filterData(tags as any) const filteredWithName = component.filterData(tags as any)
expect(filteredWithName).toHaveLength(2) expect(filteredWithName.length).toBe(2)
expect(filteredWithName.find((t) => t.id === 2)).toBeUndefined() expect(filteredWithName.find((t) => t.id === 2)).toBeUndefined()
expect( expect(
filteredWithName filteredWithName
@@ -26,7 +26,7 @@
<th scope="col" style="width: 40px;"> <th scope="col" style="width: 40px;">
<div class="form-check m-0 ms-2 me-n2"> <div class="form-check m-0 ms-2 me-n2">
<input type="checkbox" class="form-check-input" id="all-objects" [ngModel]="toggleAllEnabled()" (ngModelChange)="toggleAllEnabled.set($event)" [disabled]="processedMails().length === 0" (click)="toggleAll($event); $event.stopPropagation();"> <input type="checkbox" class="form-check-input" id="all-objects" [ngModel]="toggleAllEnabled()" (ngModelChange)="toggleAllEnabled.set($event)" [disabled]="processedMails().length === 0" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-objects"><span class="visually-hidden" i18n>Select all processed email messages</span></label> <label class="form-check-label" for="all-objects"></label>
</div> </div>
</th> </th>
<th scope="col" i18n>Subject</th> <th scope="col" i18n>Subject</th>
@@ -5,6 +5,6 @@
pre { pre {
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: break-word; word-break: break-word;
} }
} }
+6 -1
View File
@@ -1,7 +1,12 @@
import { normalizeSync } from 'normalize-diacritics' import { normalizeSync } from 'normalize-diacritics'
export type SearchTextValue = export type SearchTextValue =
string | number | boolean | bigint | null | undefined | string
| number
| boolean
| bigint
| null
| undefined
export function normalizeSearchText(value: SearchTextValue): string { export function normalizeSearchText(value: SearchTextValue): string {
return normalizeSync(String(value ?? '')).toLocaleLowerCase() return normalizeSync(String(value ?? '')).toLocaleLowerCase()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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