mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-18 18:04:53 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c0df0506a | ||
|
|
f8a641b402 | ||
|
|
e85223929c | ||
|
|
f2e3304576 | ||
|
|
6938fb0e01 | ||
|
|
255c214d0b | ||
|
|
a0ab05a9a6 | ||
|
|
df1ddb15cc | ||
|
|
71557d7c64 | ||
|
|
66f5111dc1 | ||
|
|
0addb448f5 | ||
|
|
5428ea5caa | ||
|
|
2de2dfdb81 | ||
|
|
f0d934259c | ||
|
|
735c3b073a | ||
|
|
e2cf3633fe | ||
|
|
c0589a9ce9 | ||
|
|
2135572adb | ||
|
|
697e262b5d | ||
|
|
5a65c4448d | ||
|
|
370a0a29ba | ||
|
|
536007f42b | ||
|
|
71e2dd4b08 | ||
|
|
dabd7826d0 | ||
|
|
054b234739 | ||
|
|
77dd4d1b0c | ||
|
|
56dc3e426e | ||
|
|
be8890cfaf | ||
|
|
e03b09e2ea | ||
|
|
19960cc4fa | ||
|
|
e145fe8cc7 | ||
|
|
fce55a1609 | ||
|
|
1dabd2601d | ||
|
|
88f69841b3 | ||
|
|
5b8fbdcec7 | ||
|
|
beb048b94a | ||
|
|
b33d11778a |
@@ -26,6 +26,7 @@ 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
|
||||||
|
|||||||
@@ -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.8.4'
|
rev: 'v3.9.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.8.3
|
- prettier@3.9.4
|
||||||
- '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.17
|
rev: v0.15.20
|
||||||
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.24.1"
|
rev: "v2.25.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
|
|||||||
+5
-1
@@ -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.19-python3.12-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.11.28-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
@@ -238,6 +238,10 @@ 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 \
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# correct networking for the tests
|
# correct networking for the tests
|
||||||
services:
|
services:
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.33
|
image: docker.io/gotenberg/gotenberg:8.34
|
||||||
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.8
|
image: docker.io/greenmail/standalone:2.1.9
|
||||||
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.1-alpine
|
image: docker.io/nginx:1.31.2-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.33
|
image: docker.io/gotenberg/gotenberg:8.34
|
||||||
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.33
|
image: docker.io/gotenberg/gotenberg:8.34
|
||||||
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.33
|
image: docker.io/gotenberg/gotenberg:8.34
|
||||||
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.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -326,3 +326,11 @@ 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
@@ -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:
|
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:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webserver:
|
webserver:
|
||||||
|
|||||||
+4
-4
@@ -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.3.0",
|
"bleach~=6.4.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.9.1",
|
"nltk~=3.10.0",
|
||||||
"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.43",
|
"zensical>=0.0.47",
|
||||||
]
|
]
|
||||||
lint = [
|
lint = [
|
||||||
"prek~=0.3.10",
|
"prek~=0.3.10",
|
||||||
"ruff~=0.15.15",
|
"ruff~=0.15.20",
|
||||||
]
|
]
|
||||||
testing = [
|
testing = [
|
||||||
"daphne",
|
"daphne",
|
||||||
|
|||||||
@@ -79,6 +79,16 @@
|
|||||||
"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": [
|
||||||
|
|||||||
+151
-122
@@ -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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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<NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
|
<source> Slide <x id="INTERPOLATION" equiv-text="ueryList<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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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+_d3d748c9cb3098b2696b844e4082b488/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+_88eda8c8846cc5cfe00e25de83b3f075/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">85</context>
|
<context context-type="linenumber">86</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">272</context>
|
<context context-type="linenumber">273</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">274</context>
|
<context context-type="linenumber">275</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">307</context>
|
<context context-type="linenumber">308</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">310</context>
|
<context context-type="linenumber">311</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">17</context>
|
<context context-type="linenumber">18</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">21</context>
|
<context context-type="linenumber">22</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">38</context>
|
<context context-type="linenumber">39</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">48</context>
|
<context context-type="linenumber">49</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">62</context>
|
<context context-type="linenumber">63</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">232</context>
|
<context context-type="linenumber">233</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">234</context>
|
<context context-type="linenumber">235</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">203</context>
|
<context context-type="linenumber">204</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">295</context>
|
<context context-type="linenumber">296</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">297</context>
|
<context context-type="linenumber">298</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8492095365580052820" datatype="html">
|
<trans-unit id="8492095365580052820" datatype="html">
|
||||||
@@ -1905,11 +1905,18 @@
|
|||||||
<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">104</context>
|
<context context-type="linenumber">105</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>
|
||||||
@@ -1924,14 +1931,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">125</context>
|
<context context-type="linenumber">126</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">147</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/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
@@ -2042,7 +2049,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">148</context>
|
<context context-type="linenumber">149</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>
|
||||||
@@ -2073,21 +2080,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">150</context>
|
<context context-type="linenumber">151</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">152</context>
|
<context context-type="linenumber">153</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">153</context>
|
<context context-type="linenumber">154</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>
|
||||||
@@ -2158,14 +2165,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">204</context>
|
<context context-type="linenumber">205</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">217</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/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
@@ -2181,35 +2188,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">50</context>
|
<context context-type="linenumber">51</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">222</context>
|
<context context-type="linenumber">223</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">235</context>
|
<context context-type="linenumber">236</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">242</context>
|
<context context-type="linenumber">243</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">250</context>
|
<context context-type="linenumber">251</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>
|
||||||
@@ -2240,21 +2247,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">260</context>
|
<context context-type="linenumber">261</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">266</context>
|
<context context-type="linenumber">267</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">288</context>
|
<context context-type="linenumber">289</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2525230676386818985" datatype="html">
|
<trans-unit id="2525230676386818985" datatype="html">
|
||||||
@@ -2513,11 +2520,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">255</context>
|
<context context-type="linenumber">256</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">258</context>
|
<context context-type="linenumber">259</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3818027200170621545" datatype="html">
|
<trans-unit id="3818027200170621545" datatype="html">
|
||||||
@@ -2870,11 +2877,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">286</context>
|
<context context-type="linenumber">287</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">288</context>
|
<context context-type="linenumber">289</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4569276013106377105" datatype="html">
|
<trans-unit id="4569276013106377105" datatype="html">
|
||||||
@@ -3211,11 +3218,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">316</context>
|
<context context-type="linenumber">317</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">319</context>
|
<context context-type="linenumber">320</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="472206565520537964" datatype="html">
|
<trans-unit id="472206565520537964" datatype="html">
|
||||||
@@ -3240,36 +3247,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">162</context>
|
<context context-type="linenumber">163</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">164</context>
|
<context context-type="linenumber">165</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">173</context>
|
<context context-type="linenumber">174</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">180</context>
|
<context context-type="linenumber">181</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">182</context>
|
<context context-type="linenumber">183</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">208</context>
|
<context context-type="linenumber">209</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>
|
||||||
@@ -3288,7 +3295,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">213</context>
|
<context context-type="linenumber">214</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>
|
||||||
@@ -3299,7 +3306,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">218</context>
|
<context context-type="linenumber">219</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>
|
||||||
@@ -3310,7 +3317,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">223</context>
|
<context context-type="linenumber">224</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>
|
||||||
@@ -3329,11 +3336,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">241</context>
|
<context context-type="linenumber">242</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">243</context>
|
<context context-type="linenumber">244</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>
|
||||||
@@ -3344,71 +3351,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">248</context>
|
<context context-type="linenumber">249</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">251</context>
|
<context context-type="linenumber">252</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">266</context>
|
<context context-type="linenumber">267</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">279</context>
|
<context context-type="linenumber">280</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">281</context>
|
<context context-type="linenumber">282</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">326</context>
|
<context context-type="linenumber">327</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">335,336</context>
|
<context context-type="linenumber">336,337</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">336</context>
|
<context context-type="linenumber">337</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">340</context>
|
<context context-type="linenumber">341</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">347,349</context>
|
<context context-type="linenumber">348,350</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">360</context>
|
<context context-type="linenumber">361</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1542489069631984294" datatype="html">
|
<trans-unit id="1542489069631984294" datatype="html">
|
||||||
@@ -3604,6 +3611,17 @@
|
|||||||
<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">
|
||||||
@@ -5828,13 +5846,6 @@
|
|||||||
<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">
|
||||||
@@ -6694,7 +6705,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">87</context>
|
<context context-type="linenumber">88</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>
|
||||||
@@ -6771,18 +6782,25 @@
|
|||||||
<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">81</context>
|
<context context-type="linenumber">82</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">93</context>
|
<context context-type="linenumber">94</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>
|
||||||
@@ -6797,7 +6815,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">96</context>
|
<context context-type="linenumber">97</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>
|
||||||
@@ -6808,7 +6826,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">99</context>
|
<context context-type="linenumber">100</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>
|
||||||
@@ -6819,14 +6837,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">109</context>
|
<context context-type="linenumber">110</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">113</context>
|
<context context-type="linenumber">114</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>
|
||||||
@@ -7817,8 +7835,8 @@
|
|||||||
<context context-type="linenumber">314</context>
|
<context context-type="linenumber">314</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7985558498848210210" datatype="html">
|
<trans-unit id="2659735245739197634" datatype="html">
|
||||||
<source>Original MD5 checksum</source>
|
<source>Original SHA256 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>
|
||||||
@@ -7838,8 +7856,8 @@
|
|||||||
<context context-type="linenumber">326</context>
|
<context context-type="linenumber">326</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="342875990758166588" datatype="html">
|
<trans-unit id="6714358112223607756" datatype="html">
|
||||||
<source>Archive MD5 checksum</source>
|
<source>Archive SHA256 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>
|
||||||
@@ -8237,63 +8255,67 @@
|
|||||||
<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">16</context>
|
<context context-type="linenumber">17</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">31</context>
|
<context context-type="linenumber">32</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">38</context>
|
<context context-type="linenumber">39</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">44</context>
|
<context context-type="linenumber">45</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">49</context>
|
<context context-type="linenumber">50</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">80</context>
|
<context context-type="linenumber">75</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">91</context>
|
<context context-type="linenumber">96</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">122</context>
|
<context context-type="linenumber">127</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">127</context>
|
<context context-type="linenumber">132</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9111284310237278846" datatype="html">
|
<trans-unit id="9111284310237278846" datatype="html">
|
||||||
@@ -10196,6 +10218,13 @@
|
|||||||
<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">
|
||||||
|
|||||||
Generated
+1254
-1854
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,9 @@
|
|||||||
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">
|
||||||
<span class="input-group-text text-muted" i18n>Show</span>
|
<label class="input-group-text text-muted" for="logLineLimit" i18n>Show</label>
|
||||||
<input
|
<input
|
||||||
|
id="logLineLimit"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="number"
|
type="number"
|
||||||
min="100"
|
min="100"
|
||||||
@@ -17,7 +18,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 class="form-check-input" type="checkbox" role="switch" [ngModel]="autoRefreshEnabled()" (ngModelChange)="autoRefreshEnabled.set($event)">
|
<input id="autoRefreshSwitch" 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,9 +100,7 @@ export class LogsComponent
|
|||||||
parsed.some((log, idx) => {
|
parsed.some((log, idx) => {
|
||||||
const current = this.logs()[idx]
|
const current = this.logs()[idx]
|
||||||
return (
|
return (
|
||||||
!current ||
|
current?.message !== log.message || current?.level !== log.level
|
||||||
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" class="nav-tabs">
|
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [activeId]="activeNavID()" (activeIdChange)="activeNavID.set($event)" 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">
|
||||||
<span i18n>Full search links to</span>
|
<label for="searchLink" i18n>Full search links to</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col mb-3">
|
<div class="col mb-3">
|
||||||
<select class="form-select" formControlName="searchLink">
|
<select id="searchLink" 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">
|
||||||
<span i18n>Items per page</span>
|
<label for="documentListItemPerPage" i18n>Items per page</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<select class="form-select" formControlName="documentListItemPerPage">
|
<select id="documentListItemPerPage" 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">
|
||||||
<span i18n>Default editing mode</span>
|
<label for="pdfEditorDefaultEditMode" i18n>Default editing mode</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<select class="form-select" formControlName="pdfEditorDefaultEditMode">
|
<select id="pdfEditorDefaultEditMode" 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.length
|
component.settingsForm.get('documentDetailsHiddenFields').value
|
||||||
).toEqual(0)
|
).toHaveLength(0)
|
||||||
component.toggleDocumentDetailField(field, false)
|
component.toggleDocumentDetailField(field, false)
|
||||||
expect(
|
expect(
|
||||||
component.settingsForm.get('documentDetailsHiddenFields').value.length
|
component.settingsForm.get('documentDetailsHiddenFields').value
|
||||||
).toEqual(1)
|
).toHaveLength(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.length
|
component.settingsForm.get('documentDetailsHiddenFields').value
|
||||||
).toEqual(0)
|
).toHaveLength(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)
|
||||||
|
|
||||||
activeNavID: number
|
readonly activeNavID = signal<number>(undefined)
|
||||||
|
|
||||||
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 = SettingsNavIDs[navIDKey]
|
this.activeNavID.set(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 = navChangeEvent.activeId
|
this.activeNavID.set(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 class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
<input id="autoRefreshSwitch" 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,7 +93,8 @@
|
|||||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<input #filterInput class="form-control form-control-sm" type="text"
|
<label class="visually-hidden" for="taskSearch" i18n>Search tasks</label>
|
||||||
|
<input #filterInput id="taskSearch" class="form-control form-control-sm" type="text"
|
||||||
(keyup)="filterInputKeyup($event)"
|
(keyup)="filterInputKeyup($event)"
|
||||||
[(ngModel)]="filterText">
|
[(ngModel)]="filterText">
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +212,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 scope="row" class="actions-col">
|
<td class="actions-col">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
|
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
|
||||||
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
|
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
|
||||||
|
|||||||
@@ -150,7 +150,8 @@
|
|||||||
[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" (click)="closeDocument(d); $event.preventDefault()">
|
<span class="close flex-column justify-content-center"
|
||||||
|
(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,16 +29,19 @@
|
|||||||
</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,44 +56,53 @@ 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 = 123
|
component.documentId.set(123)
|
||||||
expect(component.placeholder).toBe('Ask a question about this document...')
|
expect(component.placeholder).toBe('Ask a question about this document...')
|
||||||
component.documentId = undefined
|
component.documentId.set(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 handle streaming response', () => {
|
it('should send a message and render the streaming response', async () => {
|
||||||
component.input = 'Hello'
|
component.input.set('Hello')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
|
|
||||||
expect(component.messages.length).toBe(2)
|
expect(component.messages()).toHaveLength(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
|
||||||
jest.advanceTimersByTime(1000)
|
await jest.runAllTimersAsync()
|
||||||
expect(component.messages[1].content).toBe('Hi there')
|
await fixture.whenStable()
|
||||||
|
expect(component.messages()[1].content).toBe('Hi there')
|
||||||
|
expect(
|
||||||
|
fixture.nativeElement.querySelector('.chat-messages').textContent
|
||||||
|
).toContain('Hi there')
|
||||||
|
|
||||||
mockStream$.complete()
|
mockStream$.complete()
|
||||||
expect(component.loading).toBe(false)
|
await jest.runAllTimersAsync()
|
||||||
expect(component.messages[1].isStreaming).toBe(false)
|
await fixture.whenStable()
|
||||||
|
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 = 'Hello'
|
component.input.set('Hello')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
|
|
||||||
mockStream$.next(
|
mockStream$.next(
|
||||||
@@ -101,14 +110,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 = 'Hello'
|
component.input.set('Hello')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
|
|
||||||
mockStream$.next(
|
mockStream$.next(
|
||||||
@@ -123,12 +132,12 @@ describe('ChatComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should remove delimiter fragments that were already streamed', () => {
|
it('should remove delimiter fragments that were already streamed', () => {
|
||||||
component.input = 'Hello'
|
component.input.set('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)}`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,28 +146,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 = 'Hello'
|
component.input.set('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'].length).toBe(4)
|
expect(component['typewriterBuffer']).toHaveLength(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should scroll to bottom after sending a message', () => {
|
it('should scroll to bottom after sending a message', () => {
|
||||||
@@ -166,7 +175,7 @@ describe('ChatComponent', () => {
|
|||||||
ChatComponent.prototype as any,
|
ChatComponent.prototype as any,
|
||||||
'scrollToBottom'
|
'scrollToBottom'
|
||||||
)
|
)
|
||||||
component.input = 'Test'
|
component.input.set('Test')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
expect(scrollSpy).toHaveBeenCalled()
|
expect(scrollSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
|
import {
|
||||||
|
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'
|
||||||
@@ -23,10 +30,10 @@ import {
|
|||||||
styleUrl: './chat.component.scss',
|
styleUrl: './chat.component.scss',
|
||||||
})
|
})
|
||||||
export class ChatComponent implements OnInit {
|
export class ChatComponent implements OnInit {
|
||||||
public messages: ChatMessage[] = []
|
readonly messages = signal<ChatMessage[]>([])
|
||||||
public loading = false
|
readonly loading = signal(false)
|
||||||
public input: string = ''
|
readonly input = signal('')
|
||||||
public documentId!: number
|
readonly documentId = signal<number>(undefined)
|
||||||
|
|
||||||
private chatService: ChatService = inject(ChatService)
|
private chatService: ChatService = inject(ChatService)
|
||||||
private router: Router = inject(Router)
|
private router: Router = inject(Router)
|
||||||
@@ -38,7 +45,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...`
|
||||||
}
|
}
|
||||||
@@ -57,14 +64,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 = docIdRe ? +docIdRe[1] : undefined
|
this.documentId.set(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.push(userMessage)
|
this.messages.update((messages) => [...messages, userMessage])
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
|
|
||||||
const assistantMessage: ChatMessage = {
|
const assistantMessage: ChatMessage = {
|
||||||
@@ -72,12 +79,12 @@ export class ChatComponent implements OnInit {
|
|||||||
content: '',
|
content: '',
|
||||||
isStreaming: true,
|
isStreaming: true,
|
||||||
}
|
}
|
||||||
this.messages.push(assistantMessage)
|
this.messages.update((messages) => [...messages, assistantMessage])
|
||||||
this.loading = true
|
this.loading.set(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)
|
||||||
|
|
||||||
@@ -93,26 +100,30 @@ 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.loading = false
|
this.notifyMessagesChanged()
|
||||||
|
this.loading.set(false)
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
assistantMessage.isStreaming = false
|
assistantMessage.isStreaming = false
|
||||||
this.loading = false
|
this.notifyMessagesChanged()
|
||||||
|
this.loading.set(false)
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
this.input = ''
|
this.input.set('')
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +146,16 @@ 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' })
|
||||||
|
|||||||
+11
-1
@@ -1,5 +1,12 @@
|
|||||||
import { CurrencyPipe, getLocaleCurrencyCode, SlicePipe } from '@angular/common'
|
import { CurrencyPipe, getLocaleCurrencyCode, SlicePipe } from '@angular/common'
|
||||||
import { Component, inject, Input, LOCALE_ID, OnInit } from '@angular/core'
|
import {
|
||||||
|
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'
|
||||||
@@ -22,6 +29,7 @@ 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
|
||||||
|
|
||||||
@@ -74,6 +82,7 @@ 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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +120,7 @@ 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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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', () => {
|
||||||
|
|||||||
+7
-4
@@ -8,6 +8,7 @@ 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'
|
||||||
@@ -65,11 +66,11 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
|
|||||||
@ViewChildren('button') buttons: QueryList<ElementRef>
|
@ViewChildren('button') buttons: QueryList<ElementRef>
|
||||||
|
|
||||||
private customFields: CustomField[] = []
|
private customFields: CustomField[] = []
|
||||||
private unusedFields: CustomField[] = []
|
private readonly unusedFields = signal<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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -99,8 +100,10 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateUnusedFields() {
|
private updateUnusedFields() {
|
||||||
this.unusedFields = this.customFields.filter(
|
this.unusedFields.set(
|
||||||
(f) => !this.existingFields?.find((e) => e.field === f.id)
|
this.customFields.filter(
|
||||||
|
(f) => !this.existingFields?.find((e) => e.field === f.id)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
|
|||||||
+3
-3
@@ -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 = [field]
|
component.customFields.set([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 = [field]
|
component.customFields.set([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
-5
@@ -6,6 +6,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
QueryList,
|
QueryList,
|
||||||
|
signal,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
@@ -278,7 +279,7 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
|||||||
@Output()
|
@Output()
|
||||||
selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
|
selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
|
||||||
|
|
||||||
customFields: CustomField[] = []
|
readonly customFields = signal<CustomField[]>([])
|
||||||
|
|
||||||
public readonly today: string = new Date().toLocaleDateString('en-CA')
|
public readonly today: string = new Date().toLocaleDateString('en-CA')
|
||||||
|
|
||||||
@@ -325,12 +326,12 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
|||||||
.listAll()
|
.listAll()
|
||||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((result) => {
|
.subscribe((result) => {
|
||||||
this.customFields = result.results
|
this.customFields.set(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) {
|
||||||
@@ -353,7 +354,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]
|
||||||
@@ -369,7 +370,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']
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -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>
|
||||||
|
|||||||
+5
-5
@@ -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', () => {
|
||||||
|
|||||||
+11
-11
@@ -1,5 +1,5 @@
|
|||||||
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
|
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, OnDestroy, inject } from '@angular/core'
|
import { Component, OnDestroy, inject, signal } 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
|
||||||
public testResult: string
|
readonly testResult = signal<string>(undefined)
|
||||||
public testFailed: boolean = false
|
readonly testFailed = signal(false)
|
||||||
public testLoading = false
|
readonly testLoading = signal(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 = null
|
this.testResult.set(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.testDocument = document
|
this.testDocument = document
|
||||||
this.testLoading = true
|
this.testLoading.set(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 = result
|
this.testResult.set(result)
|
||||||
this.testFailed = false
|
this.testFailed.set(false)
|
||||||
} else {
|
} else {
|
||||||
this.testResult = null
|
this.testResult.set(null)
|
||||||
this.testFailed = true
|
this.testFailed.set(true)
|
||||||
}
|
}
|
||||||
this.testLoading = false
|
this.testLoading.set(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject, signal } 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> {
|
||||||
tags: Tag[]
|
readonly tags = signal<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 = result.results
|
this.tags.set(result.results)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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">
|
||||||
<label class="form-label mb-0" i18n>Advanced Filters</label>
|
<span class="form-label mb-0" i18n>Advanced Filters</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline-primary ms-auto"
|
class="btn btn-sm btn-outline-primary ms-auto"
|
||||||
|
|||||||
+5
-5
@@ -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.length).toBe(3)
|
expect(filters).toHaveLength(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.length).toBe(0)
|
expect(filters).toHaveLength(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.length).toBe(13)
|
expect(filters).toHaveLength(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).length).toBe(0)
|
expect(component.getFiltersFormArray(triggerGroup)).toHaveLength(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).length).toBe(1)
|
expect(component.getFiltersFormArray(triggerGroup)).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should remove selected custom field from the form group', () => {
|
it('should remove selected custom field from the form group', () => {
|
||||||
|
|||||||
+1
-1
@@ -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 || definition.inputType !== 'select') {
|
if (definition?.inputType !== 'select') {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -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()
|
||||||
|
|||||||
+4
-3
@@ -12,6 +12,7 @@ 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'
|
||||||
@@ -796,7 +797,7 @@ export class FilterableDropdownComponent
|
|||||||
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
modelIsDirty: boolean = false
|
readonly modelIsDirty = signal(false)
|
||||||
|
|
||||||
private keyboardIndex: number
|
private keyboardIndex: number
|
||||||
|
|
||||||
@@ -811,7 +812,7 @@ export class FilterableDropdownComponent
|
|||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.selectionModelChange.subscribe((updatedModel) => {
|
this.selectionModelChange.subscribe((updatedModel) => {
|
||||||
this.modelIsDirty = updatedModel.isDirty()
|
this.modelIsDirty.set(updatedModel.isDirty())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,7 +859,7 @@ export class FilterableDropdownComponent
|
|||||||
}, 0)
|
}, 0)
|
||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
this.selectionModel.reset()
|
this.selectionModel.reset()
|
||||||
this.modelIsDirty = false
|
this.modelIsDirty.set(false)
|
||||||
}
|
}
|
||||||
this.selectionModel.singleSelect =
|
this.selectionModel.singleSelect =
|
||||||
this.editing && !this.selectionModel.manyToOne
|
this.editing && !this.selectionModel.manyToOne
|
||||||
|
|||||||
+4
-3
@@ -5,6 +5,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -63,11 +64,11 @@ export class CustomFieldsValuesComponent extends AbstractInputComponent<Object>
|
|||||||
|
|
||||||
super()
|
super()
|
||||||
customFieldsService.listAll().subscribe((items) => {
|
customFieldsService.listAll().subscribe((items) => {
|
||||||
this.fields = items.results
|
this.fields.set(items.results)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fields: CustomField[]
|
private readonly fields = signal<CustomField[]>([])
|
||||||
|
|
||||||
private _selectedFields: number[]
|
private _selectedFields: number[]
|
||||||
|
|
||||||
@@ -90,6 +91,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)">
|
||||||
|
|||||||
+26
-2
@@ -2,7 +2,11 @@ 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 { of, throwError } from 'rxjs'
|
import { By } from '@angular/platform-browser'
|
||||||
|
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'
|
||||||
@@ -33,10 +37,11 @@ describe('DocumentLinkComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [DocumentLinkComponent],
|
imports: [DocumentLinkComponent, NgxBootstrapIconsModule.pick(allIcons)],
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideRouter([]),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
documentService = TestBed.inject(DocumentService)
|
documentService = TestBed.inject(DocumentService)
|
||||||
@@ -60,6 +65,25 @@ 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,6 +6,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -63,7 +64,7 @@ export class DocumentLinkComponent
|
|||||||
|
|
||||||
documentsInput$ = new Subject<string>()
|
documentsInput$ = new Subject<string>()
|
||||||
foundDocuments$: Observable<Document[]>
|
foundDocuments$: Observable<Document[]>
|
||||||
loading = false
|
readonly loading = signal(false)
|
||||||
selectedDocuments: Document[] = []
|
selectedDocuments: Document[] = []
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
@@ -93,12 +94,12 @@ export class DocumentLinkComponent
|
|||||||
this.selectedDocuments = []
|
this.selectedDocuments = []
|
||||||
super.writeValue([])
|
super.writeValue([])
|
||||||
} else {
|
} else {
|
||||||
this.loading = true
|
this.loading.set(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 = false
|
this.loading.set(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) ?? {}
|
||||||
)
|
)
|
||||||
@@ -113,7 +114,7 @@ export class DocumentLinkComponent
|
|||||||
this.documentsInput$.pipe(
|
this.documentsInput$.pipe(
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
takeUntil(this.unsubscribeNotifier),
|
takeUntil(this.unsubscribeNotifier),
|
||||||
tap(() => (this.loading = true)),
|
tap(() => this.loading.set(true)),
|
||||||
switchMap((title) =>
|
switchMap((title) =>
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.listFiltered(
|
.listFiltered(
|
||||||
@@ -133,7 +134,7 @@ export class DocumentLinkComponent
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
catchError(() => of([])), // empty on error
|
catchError(() => of([])), // empty on error
|
||||||
tap(() => (this.loading = false))
|
tap(() => this.loading.set(false))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ 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 = tag.children.map((child) => child.id)
|
const childIDs = new Set(tag.children.map((child) => child.id))
|
||||||
tagIDs = tagIDs.filter((id) => !childIDs.includes(id))
|
tagIDs = tagIDs.filter((id) => !childIDs.has(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 clipboard = inject(Clipboard)
|
private readonly clipboard = inject(Clipboard)
|
||||||
|
|
||||||
readonly id = input<number>(undefined)
|
readonly id = input<number>(undefined)
|
||||||
readonly subTitle = input('')
|
readonly subTitle = input('')
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ 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)
|
(() => boolean) | undefined
|
||||||
| undefined
|
|
||||||
expect(isVisible?.()).toBe(true)
|
expect(isVisible?.()).toBe(true)
|
||||||
expect(loadSpy).toHaveBeenCalledWith(
|
expect(loadSpy).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ numPages: 1 })
|
expect.objectContaining({ numPages: 1 })
|
||||||
@@ -61,6 +60,7 @@ 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,6 +70,13 @@ 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,10 +174,12 @@ export class PngxPdfViewerComponent
|
|||||||
'assets/js/pdf.worker.min.mjs',
|
'assets/js/pdf.worker.min.mjs',
|
||||||
this.document.baseURI
|
this.document.baseURI
|
||||||
).toString()
|
).toString()
|
||||||
let initOptions = {
|
const 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 {
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
|
|||||||
+10
-3
@@ -1,5 +1,12 @@
|
|||||||
import { NgClass } from '@angular/common'
|
import { NgClass } from '@angular/common'
|
||||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
import {
|
||||||
|
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'
|
||||||
@@ -75,7 +82,7 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
@Output()
|
@Output()
|
||||||
ownerFilterSet = new EventEmitter<PermissionsSelectionModel>()
|
ownerFilterSet = new EventEmitter<PermissionsSelectionModel>()
|
||||||
|
|
||||||
users: User[]
|
readonly users = signal<User[]>([])
|
||||||
|
|
||||||
hideUnowned: boolean
|
hideUnowned: boolean
|
||||||
|
|
||||||
@@ -102,7 +109,7 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
.listAll()
|
.listAll()
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => (this.users = result.results),
|
next: (result) => this.users.set(result.results),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-12
@@ -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 { of, throwError } from 'rxjs'
|
import { NEVER, of, Subject, 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,6 +60,10 @@ 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)
|
||||||
@@ -155,7 +159,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
'getSocialAccountProviders'
|
'getSocialAccountProviders'
|
||||||
)
|
)
|
||||||
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
||||||
component.hasUsablePassword = true
|
component.hasUsablePassword.set(true)
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
component.form.get('password').patchValue('new*pass')
|
component.form.get('password').patchValue('new*pass')
|
||||||
component.onPasswordKeyUp({
|
component.onPasswordKeyUp({
|
||||||
@@ -268,6 +272,27 @@ 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')
|
||||||
@@ -276,7 +301,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(
|
||||||
@@ -289,7 +314,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', () => {
|
||||||
@@ -310,7 +335,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', () => {
|
||||||
@@ -319,15 +344,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 = {
|
component.totpSettings.set({
|
||||||
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()
|
||||||
@@ -341,8 +366,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', () => {
|
||||||
@@ -362,13 +387,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 = ['1', '2', '3']
|
component.recoveryCodes.set(['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)
|
||||||
|
|||||||
+36
-33
@@ -56,6 +56,14 @@ 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(''),
|
||||||
@@ -72,25 +80,15 @@ 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 {
|
||||||
@@ -107,20 +105,20 @@ export class ProfileEditDialogComponent
|
|||||||
this.onEmailChange()
|
this.onEmailChange()
|
||||||
})
|
})
|
||||||
this.currentPassword = profile.password
|
this.currentPassword = profile.password
|
||||||
this.hasUsablePassword = profile.has_usable_password
|
this.hasUsablePassword.set(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 = profile.social_accounts
|
this.socialAccounts.set(profile.social_accounts ?? [])
|
||||||
this.isTotpEnabled = profile.is_mfa_enabled
|
this.isTotpEnabled.set(profile.is_mfa_enabled)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.profileService
|
this.profileService
|
||||||
.getSocialAccountProviders()
|
.getSocialAccountProviders()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((providers) => {
|
.subscribe((providers) => {
|
||||||
this.socialAccountProviders = providers
|
this.socialAccountProviders.set(providers ?? [])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +257,9 @@ export class ProfileEditDialogComponent
|
|||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (id: number) => {
|
next: (id: number) => {
|
||||||
this.socialAccounts = this.socialAccounts.filter((a) => a.id != id)
|
this.socialAccounts.update((accounts) =>
|
||||||
|
accounts.filter((account) => account.id != id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
@@ -271,36 +271,39 @@ export class ProfileEditDialogComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public gettotpSettings(): void {
|
public gettotpSettings(): void {
|
||||||
this.totpSettingsLoading = true
|
this.totpSettingsLoading.set(true)
|
||||||
this.profileService
|
this.profileService
|
||||||
.getTotpSettings()
|
.getTotpSettings()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (totpSettings) => {
|
next: (totpSettings) => {
|
||||||
this.totpSettingsLoading = false
|
this.totpSettingsLoading.set(false)
|
||||||
this.totpSettings = totpSettings
|
this.totpSettings.set(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 = false
|
this.totpSettingsLoading.set(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public activateTotp(): void {
|
public activateTotp(): void {
|
||||||
this.totpLoading = true
|
this.totpLoading.set(true)
|
||||||
this.form.get('totp_code').disable()
|
this.form.get('totp_code').disable()
|
||||||
this.profileService
|
this.profileService
|
||||||
.activateTotp(this.totpSettings.secret, this.form.get('totp_code').value)
|
.activateTotp(
|
||||||
|
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 = false
|
this.totpLoading.set(false)
|
||||||
this.isTotpEnabled = activationResponse.success
|
this.isTotpEnabled.set(activationResponse.success)
|
||||||
this.recoveryCodes = activationResponse.recovery_codes
|
this.recoveryCodes.set(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`)
|
||||||
@@ -309,7 +312,7 @@ export class ProfileEditDialogComponent
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.totpLoading = false
|
this.totpLoading.set(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)
|
||||||
},
|
},
|
||||||
@@ -317,15 +320,15 @@ export class ProfileEditDialogComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public deactivateTotp(): void {
|
public deactivateTotp(): void {
|
||||||
this.totpLoading = true
|
this.totpLoading.set(true)
|
||||||
this.profileService
|
this.profileService
|
||||||
.deactivateTotp()
|
.deactivateTotp()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (success) => {
|
next: (success) => {
|
||||||
this.totpLoading = false
|
this.totpLoading.set(false)
|
||||||
this.isTotpEnabled = !success
|
this.isTotpEnabled.set(!success)
|
||||||
this.recoveryCodes = null
|
this.recoveryCodes.set(null)
|
||||||
if (success) {
|
if (success) {
|
||||||
this.toastService.showInfo($localize`TOTP deactivated successfully`)
|
this.toastService.showInfo($localize`TOTP deactivated successfully`)
|
||||||
} else {
|
} else {
|
||||||
@@ -333,14 +336,14 @@ export class ProfileEditDialogComponent
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.totpLoading = false
|
this.totpLoading.set(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)
|
||||||
|
|||||||
+2
-1
@@ -65,8 +65,9 @@
|
|||||||
<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 class="form-control" type="text" [value]="getShareUrl(createdBundle)" readonly>
|
<input id="shareBundleLink" 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 } from '@angular/core'
|
import { Component, OnInit, inject, signal } 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)
|
||||||
|
|
||||||
asn: string
|
readonly asn = signal<string>(undefined)
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.paramMap.subscribe((paramMap) => {
|
this.route.paramMap.subscribe((paramMap) => {
|
||||||
this.asn = paramMap.get('id')
|
this.asn.set(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 MD5 checksum</td>
|
<td i18n>Original SHA256 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 MD5 checksum</td>
|
<td i18n>Archive SHA256 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.length).toEqual(1)
|
expect(deleteButtons).toHaveLength(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
-1
@@ -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" role="status"></output>
|
<output class="spinner-border spinner-border-sm fw-normal"></output>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
|
|||||||
+6
-6
@@ -26,11 +26,11 @@ import { UserService } from 'src/app/services/rest/user.service'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentHistoryComponent implements OnInit {
|
export class DocumentHistoryComponent implements OnInit {
|
||||||
private documentService = inject(DocumentService)
|
private readonly documentService = inject(DocumentService)
|
||||||
private correspondentService = inject(CorrespondentService)
|
private readonly correspondentService = inject(CorrespondentService)
|
||||||
private storagePathService = inject(StoragePathService)
|
private readonly storagePathService = inject(StoragePathService)
|
||||||
private documentTypeService = inject(DocumentTypeService)
|
private readonly documentTypeService = inject(DocumentTypeService)
|
||||||
private userService = inject(UserService)
|
private readonly userService = inject(UserService)
|
||||||
|
|
||||||
public AuditLogAction = AuditLogAction
|
public AuditLogAction = AuditLogAction
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export class DocumentHistoryComponent implements OnInit {
|
|||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
const idInt = parseInt(id, 10)
|
const idInt = Number.parseInt(id, 10)
|
||||||
const fallback$ = of(id)
|
const fallback$ = of(id)
|
||||||
|
|
||||||
let result$: Observable<string>
|
let result$: Observable<string>
|
||||||
|
|||||||
+12
-7
@@ -5,10 +5,11 @@
|
|||||||
</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">
|
||||||
<span class="input-group-text" i18n>Label</span>
|
<label class="input-group-text" for="newVersionLabel" i18n>Label</label>
|
||||||
<input
|
<input
|
||||||
|
id="newVersionLabel"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
type="text"
|
type="text"
|
||||||
[(ngModel)]="newVersionLabel"
|
[(ngModel)]="newVersionLabel"
|
||||||
@@ -31,7 +32,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>
|
||||||
@@ -49,8 +50,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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,15 +71,19 @@
|
|||||||
}
|
}
|
||||||
</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()"
|
||||||
@@ -96,7 +101,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)) {
|
||||||
|
|||||||
+17
-15
@@ -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 set failed state after websocket failure', () => {
|
it('onVersionFileSelected should render failed state after websocket failure', async () => {
|
||||||
const file = new File(['test'], 'new-version.pdf', {
|
const file = new File(['test'], 'new-version.pdf', {
|
||||||
type: 'application/pdf',
|
type: 'application/pdf',
|
||||||
})
|
})
|
||||||
@@ -266,9 +266,11 @@ 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()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -282,8 +284,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()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -298,8 +300,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
|
||||||
@@ -307,8 +309,8 @@ describe('DocumentVersionDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('ngOnChanges should clear upload status on document switch', () => {
|
it('ngOnChanges should clear upload status on document switch', () => {
|
||||||
component.versionUploadState = UploadState.Failed
|
component.versionUploadState.set(UploadState.Failed)
|
||||||
component.versionUploadError = 'something failed'
|
component.versionUploadError.set('something failed')
|
||||||
component.editingVersionId = 10
|
component.editingVersionId = 10
|
||||||
component.versionLabelDraft = 'draft'
|
component.versionLabelDraft = 'draft'
|
||||||
|
|
||||||
@@ -316,8 +318,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('')
|
||||||
})
|
})
|
||||||
|
|||||||
+24
-20
@@ -7,6 +7,7 @@ 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'
|
||||||
@@ -59,9 +60,9 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
|
|||||||
@Output() versionsUpdated = new EventEmitter<DocumentVersionInfo[]>()
|
@Output() versionsUpdated = new EventEmitter<DocumentVersionInfo[]>()
|
||||||
|
|
||||||
newVersionLabel: string = ''
|
newVersionLabel: string = ''
|
||||||
versionUploadState: UploadState = UploadState.Idle
|
readonly versionUploadState = signal(UploadState.Idle)
|
||||||
versionUploadError: string | null = null
|
readonly versionUploadError = signal<string | null>(null)
|
||||||
savingVersionLabelId: number | null = null
|
readonly savingVersionLabelId = signal<number | null>(null)
|
||||||
editingVersionId: number | null = null
|
editingVersionId: number | null = null
|
||||||
versionLabelDraft: string = ''
|
versionLabelDraft: string = ''
|
||||||
|
|
||||||
@@ -101,7 +102,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 ?? ''
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,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) {
|
||||||
@@ -158,15 +159,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 = versionId
|
this.savingVersionLabelId.set(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 = null
|
this.savingVersionLabelId.set(null)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
takeUntil(this.destroy$)
|
takeUntil(this.destroy$)
|
||||||
@@ -199,8 +200,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 = UploadState.Uploading
|
this.versionUploadState.set(UploadState.Uploading)
|
||||||
this.versionUploadError = null
|
this.versionUploadError.set(null)
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.uploadVersion(uploadDocumentId, file, label)
|
.uploadVersion(uploadDocumentId, file, label)
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -210,7 +211,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 = UploadState.Processing
|
this.versionUploadState.set(UploadState.Processing)
|
||||||
}),
|
}),
|
||||||
map((taskId) =>
|
map((taskId) =>
|
||||||
typeof taskId === 'string'
|
typeof taskId === 'string'
|
||||||
@@ -219,8 +220,8 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
|
|||||||
),
|
),
|
||||||
switchMap((taskId) => {
|
switchMap((taskId) => {
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
this.versionUploadState = UploadState.Failed
|
this.versionUploadState.set(UploadState.Failed)
|
||||||
this.versionUploadError = $localize`Missing task ID.`
|
this.versionUploadError.set($localize`Missing task ID.`)
|
||||||
return of(null)
|
return of(null)
|
||||||
}
|
}
|
||||||
return merge(
|
return merge(
|
||||||
@@ -240,9 +241,10 @@ 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 = UploadState.Failed
|
this.versionUploadState.set(UploadState.Failed)
|
||||||
this.versionUploadError =
|
this.versionUploadError.set(
|
||||||
result.message || $localize`Upload failed.`
|
result.message || $localize`Upload failed.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return of(null)
|
return of(null)
|
||||||
}
|
}
|
||||||
@@ -264,8 +266,10 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
if (uploadDocumentId !== this.documentId) return
|
if (uploadDocumentId !== this.documentId) return
|
||||||
this.versionUploadState = UploadState.Failed
|
this.versionUploadState.set(UploadState.Failed)
|
||||||
this.versionUploadError = error?.message || $localize`Upload failed.`
|
this.versionUploadError.set(
|
||||||
|
error?.message || $localize`Upload failed.`
|
||||||
|
)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error uploading new version`,
|
$localize`Error uploading new version`,
|
||||||
error
|
error
|
||||||
@@ -275,7 +279,7 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearVersionUploadStatus(): void {
|
clearVersionUploadStatus(): void {
|
||||||
this.versionUploadState = UploadState.Idle
|
this.versionUploadState.set(UploadState.Idle)
|
||||||
this.versionUploadError = null
|
this.versionUploadError.set(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,4 +1,11 @@
|
|||||||
import { Component, inject, Input, OnDestroy, OnInit } from '@angular/core'
|
import {
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
@@ -101,12 +108,12 @@ export class BulkEditorComponent
|
|||||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
|
customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
|
||||||
tagDocumentCounts: SelectionDataItem[]
|
readonly tagDocumentCounts = signal<SelectionDataItem[]>(undefined)
|
||||||
correspondentDocumentCounts: SelectionDataItem[]
|
readonly correspondentDocumentCounts = signal<SelectionDataItem[]>(undefined)
|
||||||
documentTypeDocumentCounts: SelectionDataItem[]
|
readonly documentTypeDocumentCounts = signal<SelectionDataItem[]>(undefined)
|
||||||
storagePathDocumentCounts: SelectionDataItem[]
|
readonly storagePathDocumentCounts = signal<SelectionDataItem[]>(undefined)
|
||||||
customFieldDocumentCounts: SelectionDataItem[]
|
readonly customFieldDocumentCounts = signal<SelectionDataItem[]>(undefined)
|
||||||
awaitingDownload: boolean
|
readonly awaitingDownload = signal(false)
|
||||||
|
|
||||||
unsubscribeNotifier: Subject<any> = new Subject()
|
unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
|
|
||||||
@@ -365,8 +372,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 = selectionData?.selected_tags ?? []
|
this.tagDocumentCounts.set(selectionData?.selected_tags ?? [])
|
||||||
this.applySelectionData(this.tagDocumentCounts, this.tagSelectionModel)
|
this.applySelectionData(this.tagDocumentCounts(), this.tagSelectionModel)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +381,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 = s.selected_tags
|
this.tagDocumentCounts.set(s.selected_tags)
|
||||||
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
|
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -382,10 +389,11 @@ 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 =
|
this.documentTypeDocumentCounts.set(
|
||||||
selectionData?.selected_document_types ?? []
|
selectionData?.selected_document_types ?? []
|
||||||
|
)
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
this.documentTypeDocumentCounts,
|
this.documentTypeDocumentCounts(),
|
||||||
this.documentTypeSelectionModel
|
this.documentTypeSelectionModel
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -395,7 +403,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 = s.selected_document_types
|
this.documentTypeDocumentCounts.set(s.selected_document_types)
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
s.selected_document_types,
|
s.selected_document_types,
|
||||||
this.documentTypeSelectionModel
|
this.documentTypeSelectionModel
|
||||||
@@ -406,10 +414,11 @@ 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 =
|
this.correspondentDocumentCounts.set(
|
||||||
selectionData?.selected_correspondents ?? []
|
selectionData?.selected_correspondents ?? []
|
||||||
|
)
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
this.correspondentDocumentCounts,
|
this.correspondentDocumentCounts(),
|
||||||
this.correspondentSelectionModel
|
this.correspondentSelectionModel
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -419,7 +428,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 = s.selected_correspondents
|
this.correspondentDocumentCounts.set(s.selected_correspondents)
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
s.selected_correspondents,
|
s.selected_correspondents,
|
||||||
this.correspondentSelectionModel
|
this.correspondentSelectionModel
|
||||||
@@ -430,10 +439,11 @@ 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 =
|
this.storagePathDocumentCounts.set(
|
||||||
selectionData?.selected_storage_paths ?? []
|
selectionData?.selected_storage_paths ?? []
|
||||||
|
)
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
this.storagePathDocumentCounts,
|
this.storagePathDocumentCounts(),
|
||||||
this.storagePathsSelectionModel
|
this.storagePathsSelectionModel
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -443,7 +453,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 = s.selected_storage_paths
|
this.storagePathDocumentCounts.set(s.selected_storage_paths)
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
s.selected_storage_paths,
|
s.selected_storage_paths,
|
||||||
this.storagePathsSelectionModel
|
this.storagePathsSelectionModel
|
||||||
@@ -454,10 +464,11 @@ 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 =
|
this.customFieldDocumentCounts.set(
|
||||||
selectionData?.selected_custom_fields ?? []
|
selectionData?.selected_custom_fields ?? []
|
||||||
|
)
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
this.customFieldDocumentCounts,
|
this.customFieldDocumentCounts(),
|
||||||
this.customFieldsSelectionModel
|
this.customFieldsSelectionModel
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -467,7 +478,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 = s.selected_custom_fields
|
this.customFieldDocumentCounts.set(s.selected_custom_fields)
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
s.selected_custom_fields,
|
s.selected_custom_fields,
|
||||||
this.customFieldsSelectionModel
|
this.customFieldsSelectionModel
|
||||||
@@ -876,7 +887,7 @@ export class BulkEditorComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadSelected() {
|
downloadSelected() {
|
||||||
this.awaitingDownload = true
|
this.awaitingDownload.set(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
|
||||||
@@ -893,7 +904,7 @@ export class BulkEditorComponent
|
|||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe((result: any) => {
|
.subscribe((result: any) => {
|
||||||
saveAs(result, 'documents.zip')
|
saveAs(result, 'documents.zip')
|
||||||
this.awaitingDownload = false
|
this.awaitingDownload.set(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,6 +5,7 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
QueryList,
|
QueryList,
|
||||||
|
signal,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
@@ -149,7 +150,7 @@ export class DocumentListComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
unmodifiedFilterRules: FilterRule[] = []
|
readonly unmodifiedFilterRules = signal<FilterRule[]>([])
|
||||||
private unmodifiedSavedView: SavedView
|
private unmodifiedSavedView: SavedView
|
||||||
private activeSavedView: SavedView | null = null
|
private activeSavedView: SavedView | null = null
|
||||||
|
|
||||||
@@ -299,7 +300,7 @@ export class DocumentListComponent
|
|||||||
this.savedViewService.setDocumentCount(view, this.list.collectionSize)
|
this.savedViewService.setDocumentCount(view, this.list.collectionSize)
|
||||||
})
|
})
|
||||||
this.updateDisplayCustomFields()
|
this.updateDisplayCustomFields()
|
||||||
this.unmodifiedFilterRules = view.filter_rules
|
this.unmodifiedFilterRules.set(view.filter_rules)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.route.queryParamMap
|
this.route.queryParamMap
|
||||||
@@ -316,7 +317,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 = []
|
this.unmodifiedFilterRules.set([])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -415,7 +416,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 = this.list.filterRules
|
this.unmodifiedFilterRules.set(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()"
|
||||||
|
|||||||
+1
-1
@@ -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).toEqual(null)
|
expect(component.textFilter).toBeNull()
|
||||||
component.filterRules = [
|
component.filterRules = [
|
||||||
{
|
{
|
||||||
rule_type: FILTER_TITLE_CONTENT,
|
rule_type: FILTER_TITLE_CONTENT,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -9,6 +10,7 @@ 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 {
|
||||||
@@ -349,7 +351,7 @@ export class FilterEditorComponent
|
|||||||
@ViewChild('textFilterInput')
|
@ViewChild('textFilterInput')
|
||||||
textFilterInput: ElementRef
|
textFilterInput: ElementRef
|
||||||
|
|
||||||
customFields: CustomField[] = []
|
readonly customFields = signal<CustomField[]>([])
|
||||||
|
|
||||||
tagDocumentCounts: SelectionDataItem[]
|
tagDocumentCounts: SelectionDataItem[]
|
||||||
correspondentDocumentCounts: SelectionDataItem[]
|
correspondentDocumentCounts: SelectionDataItem[]
|
||||||
@@ -514,6 +516,7 @@ 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:
|
||||||
@@ -1162,6 +1165,7 @@ 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++
|
||||||
@@ -1229,7 +1233,7 @@ export class FilterEditorComponent
|
|||||||
) {
|
) {
|
||||||
this.loadingCountTotal++
|
this.loadingCountTotal++
|
||||||
this.customFieldService.listAll().subscribe((result) => {
|
this.customFieldService.listAll().subscribe((result) => {
|
||||||
this.customFields = result.results
|
this.customFields.set(result.results)
|
||||||
this.maybeCompleteLoading()
|
this.maybeCompleteLoading()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -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" i18n>Filter by:</label>
|
<label class="text-muted me-2 mb-0" for="managementNameFilter" i18n>Filter by:</label>
|
||||||
<input class="form-control form-control-sm flex-fill w-auto" type="text" autofocus [(ngModel)]="nameFilter" (keyup)="onNameFilterKeyUp($event)" placeholder="Name" i18n-placeholder>
|
<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>
|
||||||
</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">
|
||||||
<span class="input-group-text border-0" i18n>Show:</span>
|
<label class="input-group-text border-0" for="managementPageSize" i18n>Show:</label>
|
||||||
</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 class="form-select form-select-sm small" [(ngModel)]="pageSize">
|
<select id="managementPageSize" 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>
|
||||||
|
|||||||
+2
-2
@@ -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.length).toBe(2)
|
expect(filtered).toHaveLength(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.length).toBe(2)
|
expect(filteredWithName).toHaveLength(2)
|
||||||
expect(filteredWithName.find((t) => t.id === 2)).toBeUndefined()
|
expect(filteredWithName.find((t) => t.id === 2)).toBeUndefined()
|
||||||
expect(
|
expect(
|
||||||
filteredWithName
|
filteredWithName
|
||||||
|
|||||||
+1
-1
@@ -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"></label>
|
<label class="form-check-label" for="all-objects"><span class="visually-hidden" i18n>Select all processed email messages</span></label>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" i18n>Subject</th>
|
<th scope="col" i18n>Subject</th>
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,6 @@
|
|||||||
|
|
||||||
pre {
|
pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import { normalizeSync } from 'normalize-diacritics'
|
import { normalizeSync } from 'normalize-diacritics'
|
||||||
|
|
||||||
export type SearchTextValue =
|
export type SearchTextValue =
|
||||||
| string
|
string | number | boolean | bigint | null | undefined
|
||||||
| 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
+1338
-1224
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
+1394
-1279
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
Reference in New Issue
Block a user