Compare commits
115
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40c14c1545 | ||
|
|
52ab07c673 | ||
|
|
046d8456e2 | ||
|
|
3314c59828 | ||
|
|
2103a499eb | ||
|
|
7ab779e78a | ||
|
|
49390c9427 | ||
|
|
6f29d64325 | ||
|
|
065724befb | ||
|
|
e877beea4e | ||
|
|
3e848e6e0f | ||
|
|
2e5656e1ce | ||
|
|
438650bf17 | ||
|
|
7df0b621a5 | ||
|
|
4cd755f641 | ||
|
|
8597911d85 | ||
|
|
befb80bddf | ||
|
|
7af6983cab | ||
|
|
16d6bb7334 | ||
|
|
49b658a944 | ||
|
|
e1d8680698 | ||
|
|
ee72e2d1fd | ||
|
|
e0ea4a4625 | ||
|
|
c2a9ac332a | ||
|
|
bf368aadd0 | ||
|
|
54e72d5b60 | ||
|
|
cf7422346a | ||
|
|
5b8c9ef5fc | ||
|
|
f56974f158 | ||
|
|
427508edf1 | ||
|
|
311b259cff | ||
|
|
fce7b03324 | ||
|
|
79956d6a7b | ||
|
|
978b072bff | ||
|
|
9c6f695dbf | ||
|
|
0100fcbb23 | ||
|
|
1745da0d60 | ||
|
|
e4e906ce2b | ||
|
|
80c7b97fec | ||
|
|
270e70a958 | ||
|
|
082bf6fb8e | ||
|
|
38296d9426 | ||
|
|
8311313e6e | ||
|
|
ac292999ef | ||
|
|
f3cda54cd1 | ||
|
|
8f9a294529 | ||
|
|
702de0cac3 | ||
|
|
ca42762841 | ||
|
|
13cfd6f904 | ||
|
|
18c4e6029f | ||
|
|
6c34e37838 | ||
|
|
2c28348b56 | ||
|
|
79e541244e | ||
|
|
74afad5976 | ||
|
|
c694c9791b | ||
|
|
11ceb8bde5 | ||
|
|
20ec8cb57b | ||
|
|
bfc11a545b | ||
|
|
4866af31cb | ||
|
|
0ea4da03a7 | ||
|
|
41bcc12cc2 | ||
|
|
475c231c6f | ||
|
|
e00dd46b22 | ||
|
|
fd425aa618 | ||
|
|
e1dde85c59 | ||
|
|
01207a284d | ||
|
|
0f863ab378 | ||
|
|
258064b339 | ||
|
|
2bcb37f3e9 | ||
|
|
81f8c64b2c | ||
|
|
3b80112521 | ||
|
|
459feea31e | ||
|
|
eea5839390 | ||
|
|
c79414ebd9 | ||
|
|
ed1775e689 | ||
|
|
cd50f20a20 | ||
|
|
c8ec70c05f | ||
|
|
5e3ee3a80d | ||
|
|
29726c3ce1 | ||
|
|
6804c92861 | ||
|
|
a32077566b | ||
|
|
283bcb4c91 | ||
|
|
f68ee628d9 | ||
|
|
bd5ba97ee8 | ||
|
|
325594034e | ||
|
|
28261ac51c | ||
|
|
23ef52f405 | ||
|
|
2b45793bc2 | ||
|
|
2f96cc0050 | ||
|
|
d36e8254f3 | ||
|
|
405fab8514 | ||
|
|
ee4f62a1b3 | ||
|
|
d97e4a9a95 | ||
|
|
1cfba87114 | ||
|
|
b145ed315a | ||
|
|
1f47b8c090 | ||
|
|
1e3f2a1438 | ||
|
|
d61b2bbfc6 | ||
|
|
e1d6b4a9ac | ||
|
|
9f398337c6 | ||
|
|
765bf1d11c | ||
|
|
49a96ccee0 | ||
|
|
c45bbcaea2 | ||
|
|
ab87aedfc7 | ||
|
|
18e3ad8b22 | ||
|
|
c342eafa8d | ||
|
|
290c44f5d5 | ||
|
|
02015ec404 | ||
|
|
98b4f447d8 | ||
|
|
272691e386 | ||
|
|
1012cee39a | ||
|
|
19a5733d0d | ||
|
|
86788f1445 | ||
|
|
1d5e7e930e | ||
|
|
da6d568906 |
@@ -2,10 +2,10 @@ blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🤔 Questions and Help
|
||||
url: https://github.com/paperless-ngx/paperless-ngx/discussions
|
||||
about: This issue tracker is not for support questions. Please refer to our Discussions.
|
||||
about: General questions or support for using Paperless-ngx.
|
||||
- name: 💬 Chat
|
||||
url: https://matrix.to/#/#paperlessngx:matrix.org
|
||||
about: Want to discuss Paperless-ngx with others? Check out our chat.
|
||||
- name: 🚀 Feature Request
|
||||
url: https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=feature-requests
|
||||
about: Remember to search for existing feature requests and "up-vote" any you like
|
||||
about: Remember to search for existing feature requests and "up-vote" those that you like.
|
||||
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PIP_ENV_VERSION: "2024.4.0"
|
||||
DEFAULT_PIP_ENV_VERSION: "2024.4.1"
|
||||
# This is the default version of Python to use in most steps which aren't specific
|
||||
DEFAULT_PYTHON_VERSION: "3.11"
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ repos:
|
||||
- id: check-case-conflict
|
||||
- id: detect-private-key
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
|
||||
@@ -51,7 +51,7 @@ repos:
|
||||
- 'prettier-plugin-organize-imports@4.1.0'
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.6
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
||||
@@ -38,7 +38,6 @@ ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
[lint.per-file-ignores]
|
||||
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
||||
"docker/wait-for-redis.py" = ["INP001", "T201"]
|
||||
"src/documents/consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/file_handling.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/management/commands/document_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/management/commands/document_exporter.py" = ["PTH"] # TODO Enable & remove
|
||||
@@ -51,8 +50,6 @@ ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
"src/documents/signals/handlers.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tasks.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_api_app_config.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_api_bulk_download.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_api_documents.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_classifier.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_consumer.py" = ["PTH"] # TODO Enable & remove
|
||||
"src/documents/tests/test_file_handling.py" = ["PTH"] # TODO Enable & remove
|
||||
|
||||
+13
-25
@@ -39,7 +39,7 @@ COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.4.0 \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.4.1 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
@@ -127,32 +127,21 @@ RUN set -eux \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \
|
||||
&& echo "Installing pre-built updates" \
|
||||
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing qpdf ${QPDF_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing Ghostscript ${GS_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
||||
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing jbig2enc" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Cleaning up image layer" \
|
||||
&& rm --force --verbose *.deb \
|
||||
@@ -232,12 +221,11 @@ RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||
&& echo "Installing Python requirements" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output psycopg_c-3.2.3-cp312-cp312-linux_x86_64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.3/psycopg_c-3.2.3-cp312-cp312-linux_x86_64.whl \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output psycopg_c-3.2.3-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.3/psycopg_c-3.2.3-cp312-cp312-linux_aarch64.whl \
|
||||
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.4/psycopg_c-3.2.4-cp312-cp312-linux_x86_64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.4/psycopg_c-3.2.4-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl \
|
||||
&& python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||
|
||||
@@ -7,7 +7,7 @@ name = "pypi"
|
||||
dateparser = "~=1.2"
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
django = "~=5.1.4"
|
||||
django = "~=5.1.5"
|
||||
django-allauth = {extras = ["mfa", "socialaccount"], version = "*"}
|
||||
django-auditlog = "*"
|
||||
django-celery-results = "*"
|
||||
@@ -57,8 +57,8 @@ tqdm = "*"
|
||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
||||
watchdog = "~=6.0"
|
||||
whitenoise = "~=6.8"
|
||||
whoosh = "~=2.7"
|
||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
whoosh-reloaded = "*"
|
||||
zxing-cpp = "*"
|
||||
|
||||
|
||||
[dev-packages]
|
||||
|
||||
Generated
+1318
-1348
File diff suppressed because it is too large
Load Diff
@@ -308,7 +308,7 @@ Paperless provides the following variables for use within filenames:
|
||||
- `{{ tag_list }}`: A comma separated list of all tags assigned to the
|
||||
document.
|
||||
- `{{ title }}`: The title of the document.
|
||||
- `{{ created }}`: The full date (ISO format) the document was created.
|
||||
- `{{ created }}`: The full date (ISO 8601 format, e.g. `2024-03-14`) the document was created.
|
||||
- `{{ created_year }}`: Year created only, formatted as the year with
|
||||
century.
|
||||
- `{{ created_year_short }}`: Year created only, formatted as the year
|
||||
@@ -476,7 +476,7 @@ a document with an ASN of 355 would be placed in `somepath/asn-201-400/asn-3xx/T
|
||||
/{{ title }}
|
||||
```
|
||||
|
||||
For a PDF document, it would result in `pdfs/Title.pdf`, but for a PNG document, the path would be `pngs/Title.pdf`.
|
||||
For a PDF document, it would result in `pdfs/Title.pdf`, but for a PNG document, the path would be `pngs/Title.png`.
|
||||
|
||||
To use custom fields:
|
||||
|
||||
|
||||
+14
@@ -541,6 +541,12 @@ server, the following procedure should be performed:
|
||||
2. Determine whether the client is compatible with this server based on
|
||||
the presence/absence of these headers and their values if present.
|
||||
|
||||
### API Version Deprecation Policy
|
||||
|
||||
Older API versions are guaranteed to be supported for at least one year
|
||||
after the release of a new API version. After that, support for older
|
||||
API versions may be (but is not guaranteed to be) dropped.
|
||||
|
||||
### API Changelog
|
||||
|
||||
#### Version 1
|
||||
@@ -573,3 +579,11 @@ Initial API version.
|
||||
#### Version 6
|
||||
|
||||
- Moved acknowledge tasks endpoint to be under `/api/tasks/acknowledge/`.
|
||||
|
||||
#### Version 7
|
||||
|
||||
- The format of select type custom fields has changed to return the options
|
||||
as an array of objects with `id` and `label` fields as opposed to a simple
|
||||
list of strings. When creating or updating a custom field value of a
|
||||
document for a select type custom field, the value should be the `id` of
|
||||
the option whereas previously was the index of the option.
|
||||
|
||||
@@ -1,5 +1,327 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.14.7
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: require totp code for obtain auth token by [@shamoon](https://github.com/shamoon) [#8936](https://github.com/paperless-ngx/paperless-ngx/pull/8936)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Enhancement: require totp code for obtain auth token by [@shamoon](https://github.com/shamoon) [#8936](https://github.com/paperless-ngx/paperless-ngx/pull/8936)
|
||||
- Fix: reflect doc links in bulk modify custom fields by [@shamoon](https://github.com/shamoon) [#8962](https://github.com/paperless-ngx/paperless-ngx/pull/8962)
|
||||
- Fix: also ensure symmetric doc link removal on bulk edit by [@shamoon](https://github.com/shamoon) [#8963](https://github.com/paperless-ngx/paperless-ngx/pull/8963)
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Chore(deps-dev): Bump ruff from 0.9.2 to 0.9.3 in the development group by @[dependabot[bot]](https://github.com/apps/dependabot) [#8928](https://github.com/paperless-ngx/paperless-ngx/pull/8928)
|
||||
- Enhancement: require totp code for obtain auth token by [@shamoon](https://github.com/shamoon) [#8936](https://github.com/paperless-ngx/paperless-ngx/pull/8936)
|
||||
- Fix: reflect doc links in bulk modify custom fields by [@shamoon](https://github.com/shamoon) [#8962](https://github.com/paperless-ngx/paperless-ngx/pull/8962)
|
||||
- Fix: also ensure symmetric doc link removal on bulk edit by [@shamoon](https://github.com/shamoon) [#8963](https://github.com/paperless-ngx/paperless-ngx/pull/8963)
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.14.6
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: backwards-compatible versioned API response for custom field select fields, update default API version [@shamoon](https://github.com/shamoon) ([#8912](https://github.com/paperless-ngx/paperless-ngx/pull/8912))
|
||||
- Tweak: place items with 0 documents at bottom of filterable list, retain alphabetical [@shamoon](https://github.com/shamoon) ([#8924](https://github.com/paperless-ngx/paperless-ngx/pull/8924))
|
||||
- Fix: set larger page size for abstract service getFew [@shamoon](https://github.com/shamoon) ([#8920](https://github.com/paperless-ngx/paperless-ngx/pull/8920))
|
||||
- Fix/refactor: remove doc observables, fix username async [@shamoon](https://github.com/shamoon) ([#8908](https://github.com/paperless-ngx/paperless-ngx/pull/8908))
|
||||
- Fix: include missing fields for saved view widgets [@shamoon](https://github.com/shamoon) ([#8905](https://github.com/paperless-ngx/paperless-ngx/pull/8905))
|
||||
- Fix: force set document not dirty before close after save [@shamoon](https://github.com/shamoon) ([#8888](https://github.com/paperless-ngx/paperless-ngx/pull/8888))
|
||||
- Fixhancement: restore search highlighting and add for built-in viewer [@shamoon](https://github.com/shamoon) ([#8885](https://github.com/paperless-ngx/paperless-ngx/pull/8885))
|
||||
- Fix: resolve cpu usage due to incorrect interval use [@shamoon](https://github.com/shamoon) ([#8884](https://github.com/paperless-ngx/paperless-ngx/pull/8884))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>10 changes</summary>
|
||||
|
||||
- Fix: backwards-compatible versioned API response for custom field select fields, update default API version [@shamoon](https://github.com/shamoon) ([#8912](https://github.com/paperless-ngx/paperless-ngx/pull/8912))
|
||||
- Tweak: place items with 0 documents at bottom of filterable list, retain alphabetical [@shamoon](https://github.com/shamoon) ([#8924](https://github.com/paperless-ngx/paperless-ngx/pull/8924))
|
||||
- Fix: set larger page size for abstract service getFew [@shamoon](https://github.com/shamoon) ([#8920](https://github.com/paperless-ngx/paperless-ngx/pull/8920))
|
||||
- Fix/refactor: remove doc observables, fix username async [@shamoon](https://github.com/shamoon) ([#8908](https://github.com/paperless-ngx/paperless-ngx/pull/8908))
|
||||
- Fix: include missing fields for saved view widgets [@shamoon](https://github.com/shamoon) ([#8905](https://github.com/paperless-ngx/paperless-ngx/pull/8905))
|
||||
- Chore: Upgrades dependencies and hook versions [@stumpylog](https://github.com/stumpylog) ([#8895](https://github.com/paperless-ngx/paperless-ngx/pull/8895))
|
||||
- Fix: force set document not dirty before close after save [@shamoon](https://github.com/shamoon) ([#8888](https://github.com/paperless-ngx/paperless-ngx/pull/8888))
|
||||
- Change: Revert dropdown sorting by doc count [@shamoon](https://github.com/shamoon) ([#8887](https://github.com/paperless-ngx/paperless-ngx/pull/8887))
|
||||
- Fixhancement: restore search highlighting and add for built-in viewer [@shamoon](https://github.com/shamoon) ([#8885](https://github.com/paperless-ngx/paperless-ngx/pull/8885))
|
||||
- Fix: resolve cpu usage due to incorrect interval use [@shamoon](https://github.com/shamoon) ([#8884](https://github.com/paperless-ngx/paperless-ngx/pull/8884))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.14.5
|
||||
|
||||
### Features
|
||||
|
||||
- Change: restrict altering and creation of superusers to superusers only [@shamoon](https://github.com/shamoon) ([#8837](https://github.com/paperless-ngx/paperless-ngx/pull/8837))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: fix long tag visual wrapping [@shamoon](https://github.com/shamoon) ([#8833](https://github.com/paperless-ngx/paperless-ngx/pull/8833))
|
||||
- Fix: Enforce classifier training ordering to prevent extra training [@stumpylog](https://github.com/stumpylog) ([#8822](https://github.com/paperless-ngx/paperless-ngx/pull/8822))
|
||||
- Fix: import router module to not found component [@shamoon](https://github.com/shamoon) ([#8821](https://github.com/paperless-ngx/paperless-ngx/pull/8821))
|
||||
- Fix: better reflect some mail account / rule permissions in UI [@shamoon](https://github.com/shamoon) ([#8812](https://github.com/paperless-ngx/paperless-ngx/pull/8812))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps-dev): Bump undici from 5.28.4 to 5.28.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8851](https://github.com/paperless-ngx/paperless-ngx/pull/8851))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8841](https://github.com/paperless-ngx/paperless-ngx/pull/8841))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>9 changes</summary>
|
||||
|
||||
- Chore(deps-dev): Bump undici from 5.28.4 to 5.28.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8851](https://github.com/paperless-ngx/paperless-ngx/pull/8851))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8841](https://github.com/paperless-ngx/paperless-ngx/pull/8841))
|
||||
- Chore: use simpler method for attaching files to emails [@shamoon](https://github.com/shamoon) ([#8845](https://github.com/paperless-ngx/paperless-ngx/pull/8845))
|
||||
- Change: restrict altering and creation of superusers to superusers only [@shamoon](https://github.com/shamoon) ([#8837](https://github.com/paperless-ngx/paperless-ngx/pull/8837))
|
||||
- Fix: fix long tag visual wrapping [@shamoon](https://github.com/shamoon) ([#8833](https://github.com/paperless-ngx/paperless-ngx/pull/8833))
|
||||
- Change: allow generate auth token without a usable password [@shamoon](https://github.com/shamoon) ([#8824](https://github.com/paperless-ngx/paperless-ngx/pull/8824))
|
||||
- Fix: Enforce classifier training ordering to prevent extra training [@stumpylog](https://github.com/stumpylog) ([#8822](https://github.com/paperless-ngx/paperless-ngx/pull/8822))
|
||||
- Fix: import router module to not found component [@shamoon](https://github.com/shamoon) ([#8821](https://github.com/paperless-ngx/paperless-ngx/pull/8821))
|
||||
- Fix: better reflect some mail account / rule permissions in UI [@shamoon](https://github.com/shamoon) ([#8812](https://github.com/paperless-ngx/paperless-ngx/pull/8812))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.14.4
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: allow specifying JSON encoding for webhooks [@shamoon](https://github.com/shamoon) ([#8799](https://github.com/paperless-ngx/paperless-ngx/pull/8799))
|
||||
- Change: disable API basic auth if MFA enabled [@shamoon](https://github.com/shamoon) ([#8792](https://github.com/paperless-ngx/paperless-ngx/pull/8792))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Include email and webhook objects in the export [@stumpylog](https://github.com/stumpylog) ([#8790](https://github.com/paperless-ngx/paperless-ngx/pull/8790))
|
||||
- Fix: use MIMEBase for email attachments [@shamoon](https://github.com/shamoon) ([#8762](https://github.com/paperless-ngx/paperless-ngx/pull/8762))
|
||||
- Fix: handle page out of range in mgmt lists after delete [@shamoon](https://github.com/shamoon) ([#8771](https://github.com/paperless-ngx/paperless-ngx/pull/8771))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Enhancement: allow specifying JSON encoding for webhooks [@shamoon](https://github.com/shamoon) ([#8799](https://github.com/paperless-ngx/paperless-ngx/pull/8799))
|
||||
- Change: disable API basic auth if MFA enabled [@shamoon](https://github.com/shamoon) ([#8792](https://github.com/paperless-ngx/paperless-ngx/pull/8792))
|
||||
- Fix: Include email and webhook objects in the export [@stumpylog](https://github.com/stumpylog) ([#8790](https://github.com/paperless-ngx/paperless-ngx/pull/8790))
|
||||
- Fix: use MIMEBase for email attachments [@shamoon](https://github.com/shamoon) ([#8762](https://github.com/paperless-ngx/paperless-ngx/pull/8762))
|
||||
- Fix: handle page out of range in mgmt lists after delete [@shamoon](https://github.com/shamoon) ([#8771](https://github.com/paperless-ngx/paperless-ngx/pull/8771))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.14.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Adds a default 30s timeout for emails, instead of no timeout [@stumpylog](https://github.com/stumpylog) ([#8757](https://github.com/paperless-ngx/paperless-ngx/pull/8757))
|
||||
- Fix: import forms modules for entries component [@shamoon](https://github.com/shamoon) ([#8752](https://github.com/paperless-ngx/paperless-ngx/pull/8752))
|
||||
- Fix: fix email/wh actions on consume started [@shamoon](https://github.com/shamoon) ([#8750](https://github.com/paperless-ngx/paperless-ngx/pull/8750))
|
||||
- Fix: import date picker module in cf query dropdown [@shamoon](https://github.com/shamoon) ([#8749](https://github.com/paperless-ngx/paperless-ngx/pull/8749))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Fix: Adds a default 30s timeout for emails, instead of no timeout [@stumpylog](https://github.com/stumpylog) ([#8757](https://github.com/paperless-ngx/paperless-ngx/pull/8757))
|
||||
- Enhancement: set autofocus on MFA code field [@mxmehl](https://github.com/mxmehl) ([#8756](https://github.com/paperless-ngx/paperless-ngx/pull/8756))
|
||||
- Fix: import forms modules for entries component [@shamoon](https://github.com/shamoon) ([#8752](https://github.com/paperless-ngx/paperless-ngx/pull/8752))
|
||||
- Fix: fix email/wh actions on consume started [@shamoon](https://github.com/shamoon) ([#8750](https://github.com/paperless-ngx/paperless-ngx/pull/8750))
|
||||
- Fix: import date picker module in cf query dropdown [@shamoon](https://github.com/shamoon) ([#8749](https://github.com/paperless-ngx/paperless-ngx/pull/8749))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.14.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: dont try to parse empty webhook params [@shamoon](https://github.com/shamoon) ([#8742](https://github.com/paperless-ngx/paperless-ngx/pull/8742))
|
||||
- Fix: pass working file to workflows, pickle file bytes [@shamoon](https://github.com/shamoon) ([#8741](https://github.com/paperless-ngx/paperless-ngx/pull/8741))
|
||||
- Fix: use hard delete when bulk editing custom fields [@shamoon](https://github.com/shamoon) ([#8740](https://github.com/paperless-ngx/paperless-ngx/pull/8740))
|
||||
- Fix: Ensure email attachments use the latest document path for attachments [@stumpylog](https://github.com/stumpylog) ([#8737](https://github.com/paperless-ngx/paperless-ngx/pull/8737))
|
||||
- Fix: include tooltip module for custom fields display [@shamoon](https://github.com/shamoon) ([#8739](https://github.com/paperless-ngx/paperless-ngx/pull/8739))
|
||||
- Fix: remove id of webhook/email actions on copy [@shamoon](https://github.com/shamoon) ([#8729](https://github.com/paperless-ngx/paperless-ngx/pull/8729))
|
||||
- Fix: import dnd module for merge confirm dialog [@shamoon](https://github.com/shamoon) ([#8727](https://github.com/paperless-ngx/paperless-ngx/pull/8727))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps): Bump django from 5.1.4 to 5.1.5 [@dependabot](https://github.com/dependabot) ([#8738](https://github.com/paperless-ngx/paperless-ngx/pull/8738))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>7 changes</summary>
|
||||
|
||||
- Fix: dont try to parse empty webhook params [@shamoon](https://github.com/shamoon) ([#8742](https://github.com/paperless-ngx/paperless-ngx/pull/8742))
|
||||
- Fix: pass working file to workflows, pickle file bytes [@shamoon](https://github.com/shamoon) ([#8741](https://github.com/paperless-ngx/paperless-ngx/pull/8741))
|
||||
- Fix: use hard delete when bulk editing custom fields [@shamoon](https://github.com/shamoon) ([#8740](https://github.com/paperless-ngx/paperless-ngx/pull/8740))
|
||||
- Fix: Ensure email attachments use the latest document path for attachments [@stumpylog](https://github.com/stumpylog) ([#8737](https://github.com/paperless-ngx/paperless-ngx/pull/8737))
|
||||
- Fix: include tooltip module for custom fields display [@shamoon](https://github.com/shamoon) ([#8739](https://github.com/paperless-ngx/paperless-ngx/pull/8739))
|
||||
- Fix: remove id of webhook/email actions on copy [@shamoon](https://github.com/shamoon) ([#8729](https://github.com/paperless-ngx/paperless-ngx/pull/8729))
|
||||
- Fix: import dnd module for merge confirm dialog [@shamoon](https://github.com/shamoon) ([#8727](https://github.com/paperless-ngx/paperless-ngx/pull/8727))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.14.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: prevent error if bulk edit method not in MODIFIED_FIELD_BY_METHOD [@shamoon](https://github.com/shamoon) ([#8710](https://github.com/paperless-ngx/paperless-ngx/pull/8710))
|
||||
- Fix: include tag component in list view [@shamoon](https://github.com/shamoon) ([#8706](https://github.com/paperless-ngx/paperless-ngx/pull/8706))
|
||||
- Fix: use unmodified original for checksum if exists [@shamoon](https://github.com/shamoon) ([#8693](https://github.com/paperless-ngx/paperless-ngx/pull/8693))
|
||||
- Fix: complete load with native PDF viewer [@shamoon](https://github.com/shamoon) ([#8699](https://github.com/paperless-ngx/paperless-ngx/pull/8699))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Fix: prevent error if bulk edit method not in MODIFIED_FIELD_BY_METHOD [@shamoon](https://github.com/shamoon) ([#8710](https://github.com/paperless-ngx/paperless-ngx/pull/8710))
|
||||
- Fix: include tag component in list view [@shamoon](https://github.com/shamoon) ([#8706](https://github.com/paperless-ngx/paperless-ngx/pull/8706))
|
||||
- Fix: use unmodified original for checksum if exists [@shamoon](https://github.com/shamoon) ([#8693](https://github.com/paperless-ngx/paperless-ngx/pull/8693))
|
||||
- Fix: complete load with native PDF viewer [@shamoon](https://github.com/shamoon) ([#8699](https://github.com/paperless-ngx/paperless-ngx/pull/8699))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.14.0
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: custom field sorting [@shamoon](https://github.com/shamoon) ([#8494](https://github.com/paperless-ngx/paperless-ngx/pull/8494))
|
||||
- Enhancement: process mail button [@shamoon](https://github.com/shamoon) ([#8466](https://github.com/paperless-ngx/paperless-ngx/pull/8466))
|
||||
- Feature: bulk edit custom field values [@shamoon](https://github.com/shamoon) ([#8428](https://github.com/paperless-ngx/paperless-ngx/pull/8428))
|
||||
- Enhancement: improved loading visuals [@shamoon](https://github.com/shamoon) ([#8435](https://github.com/paperless-ngx/paperless-ngx/pull/8435))
|
||||
- Enhancement: prune audit logs and management command [@shamoon](https://github.com/shamoon) ([#8416](https://github.com/paperless-ngx/paperless-ngx/pull/8416))
|
||||
- Change: make saved views manage its own component [@shamoon](https://github.com/shamoon) ([#8423](https://github.com/paperless-ngx/paperless-ngx/pull/8423))
|
||||
- Enhancement: file task filtering [@shamoon](https://github.com/shamoon) ([#8421](https://github.com/paperless-ngx/paperless-ngx/pull/8421))
|
||||
- Enhancement: auto-link duplicate document [@shamoon](https://github.com/shamoon) ([#8415](https://github.com/paperless-ngx/paperless-ngx/pull/8415))
|
||||
- Feature: email, webhook workflow actions [@shamoon](https://github.com/shamoon) ([#8108](https://github.com/paperless-ngx/paperless-ngx/pull/8108))
|
||||
- Enhancement: use stable unique IDs for custom field select options [@shamoon](https://github.com/shamoon) ([#8299](https://github.com/paperless-ngx/paperless-ngx/pull/8299))
|
||||
- Enhancement: better TIFF display browser support [@shamoon](https://github.com/shamoon) ([#8087](https://github.com/paperless-ngx/paperless-ngx/pull/8087))
|
||||
- Enhancement: filterable list count sorting and opacification [@shamoon](https://github.com/shamoon) ([#8386](https://github.com/paperless-ngx/paperless-ngx/pull/8386))
|
||||
- Enhancement: preview button for document list and trash, refactor [@shamoon](https://github.com/shamoon) ([#8384](https://github.com/paperless-ngx/paperless-ngx/pull/8384))
|
||||
- Enhancement: use theme-color meta tag [@shamoon](https://github.com/shamoon) ([#8359](https://github.com/paperless-ngx/paperless-ngx/pull/8359))
|
||||
- Feature: scheduled workflow trigger [@shamoon](https://github.com/shamoon) ([#8036](https://github.com/paperless-ngx/paperless-ngx/pull/8036))
|
||||
- Enhancement: support owner permissions for file tasks [@shamoon](https://github.com/shamoon) ([#8195](https://github.com/paperless-ngx/paperless-ngx/pull/8195))
|
||||
- Fixhancement: change update content to handle archive disabled [@shamoon](https://github.com/shamoon) ([#8315](https://github.com/paperless-ngx/paperless-ngx/pull/8315))
|
||||
- Enhancement: next / previous shortcuts for document list [@shamoon](https://github.com/shamoon) ([#8309](https://github.com/paperless-ngx/paperless-ngx/pull/8309))
|
||||
- Feature: two-factor authentication [@shamoon](https://github.com/shamoon) ([#8012](https://github.com/paperless-ngx/paperless-ngx/pull/8012))
|
||||
- Enhancement: save \& next / close shortcut key [@shamoon](https://github.com/shamoon) ([#8243](https://github.com/paperless-ngx/paperless-ngx/pull/8243))
|
||||
- Feature: loading preview, better text popup preview [@shamoon](https://github.com/shamoon) ([#8011](https://github.com/paperless-ngx/paperless-ngx/pull/8011))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: add some minor frontend permissions checks [@shamoon](https://github.com/shamoon) ([#8524](https://github.com/paperless-ngx/paperless-ngx/pull/8524))
|
||||
- FIx: obliquely trim spaces from global search [@shamoon](https://github.com/shamoon) ([#8484](https://github.com/paperless-ngx/paperless-ngx/pull/8484))
|
||||
- Fix: include global perms for bulk edit endpoint [@shamoon](https://github.com/shamoon) ([#8468](https://github.com/paperless-ngx/paperless-ngx/pull/8468))
|
||||
- Fix: frontend better reflect global perms for bulk edit, disabled form state [@shamoon](https://github.com/shamoon) ([#8469](https://github.com/paperless-ngx/paperless-ngx/pull/8469))
|
||||
- Fixhancement: dispatch change event from current field prior to save [@shamoon](https://github.com/shamoon) ([#8369](https://github.com/paperless-ngx/paperless-ngx/pull/8369))
|
||||
- Fix: Fixes install script to handle languages with dashes or underscores [@stumpylog](https://github.com/stumpylog) ([#8341](https://github.com/paperless-ngx/paperless-ngx/pull/8341))
|
||||
- Fix: handle very old dates with positive offset too [@shamoon](https://github.com/shamoon) ([#8335](https://github.com/paperless-ngx/paperless-ngx/pull/8335))
|
||||
- Fixhancement: change update content to handle archive disabled [@shamoon](https://github.com/shamoon) ([#8315](https://github.com/paperless-ngx/paperless-ngx/pull/8315))
|
||||
- Fix: include db_index caveat in squashed migrations [@shamoon](https://github.com/shamoon) ([#8292](https://github.com/paperless-ngx/paperless-ngx/pull/8292))
|
||||
- Fix: prevent duplicate workflow runs [@shamoon](https://github.com/shamoon) ([#8268](https://github.com/paperless-ngx/paperless-ngx/pull/8268))
|
||||
- Fix: add note about select options to edit dialog [@shamoon](https://github.com/shamoon) ([#8267](https://github.com/paperless-ngx/paperless-ngx/pull/8267))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore(deps): Bump codecov/codecov-action from 4 to 5 in the actions group [@dependabot](https://github.com/dependabot) ([#8401](https://github.com/paperless-ngx/paperless-ngx/pull/8401))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>16 changes</summary>
|
||||
|
||||
- Chore(deps): Bump the small-changes group with 3 updates [@dependabot](https://github.com/dependabot) ([#8627](https://github.com/paperless-ngx/paperless-ngx/pull/8627))
|
||||
- Chore(deps-dev): Bump ruff from 0.8.4 to 0.8.6 in the development group [@dependabot](https://github.com/dependabot) ([#8626](https://github.com/paperless-ngx/paperless-ngx/pull/8626))
|
||||
- Chore(deps): Bump django-allauth from 65.3.0 to 65.3.1 in the django group [@dependabot](https://github.com/dependabot) ([#8574](https://github.com/paperless-ngx/paperless-ngx/pull/8574))
|
||||
- Chore(deps-dev): Bump ruff from 0.8.3 to 0.8.4 in the development group [@dependabot](https://github.com/dependabot) ([#8546](https://github.com/paperless-ngx/paperless-ngx/pull/8546))
|
||||
- Chore(deps): Bump the small-changes group with 6 updates [@dependabot](https://github.com/dependabot) ([#8547](https://github.com/paperless-ngx/paperless-ngx/pull/8547))
|
||||
- Chore: update ng2 pdf viewer [@shamoon](https://github.com/shamoon) ([#8462](https://github.com/paperless-ngx/paperless-ngx/pull/8462))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#8458](https://github.com/paperless-ngx/paperless-ngx/pull/8458))
|
||||
- Chore(deps): Bump django-soft-delete from 1.0.15 to 1.0.16 in the django group [@dependabot](https://github.com/dependabot) ([#8459](https://github.com/paperless-ngx/paperless-ngx/pull/8459))
|
||||
- Chore(deps): Bump the small-changes group with 4 updates [@dependabot](https://github.com/dependabot) ([#8460](https://github.com/paperless-ngx/paperless-ngx/pull/8460))
|
||||
- Chore(deps): Bump django from 5.1.3 to 5.1.4 [@dependabot](https://github.com/dependabot) ([#8445](https://github.com/paperless-ngx/paperless-ngx/pull/8445))
|
||||
- Chore(deps-dev): Bump the development group with 4 updates [@dependabot](https://github.com/dependabot) ([#8414](https://github.com/paperless-ngx/paperless-ngx/pull/8414))
|
||||
- Chore(deps): Bump codecov/codecov-action from 4 to 5 in the actions group [@dependabot](https://github.com/dependabot) ([#8401](https://github.com/paperless-ngx/paperless-ngx/pull/8401))
|
||||
- Chore(deps-dev): Bump the development group with 4 updates [@dependabot](https://github.com/dependabot) ([#8352](https://github.com/paperless-ngx/paperless-ngx/pull/8352))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 7 updates [@dependabot](https://github.com/dependabot) ([#8399](https://github.com/paperless-ngx/paperless-ngx/pull/8399))
|
||||
- Chore(deps): Bump tornado from 6.4.1 to 6.4.2 [@dependabot](https://github.com/dependabot) ([#8336](https://github.com/paperless-ngx/paperless-ngx/pull/8336))
|
||||
- Chore(deps): Bump watchdog from 5.0.3 to 6.0.0 in the major-versions group [@dependabot](https://github.com/dependabot) ([#8257](https://github.com/paperless-ngx/paperless-ngx/pull/8257))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>65 changes</summary>
|
||||
|
||||
- Fix: use state param with oauth [@shamoon](https://github.com/shamoon) ([#8636](https://github.com/paperless-ngx/paperless-ngx/pull/8636))
|
||||
- Fix: check permissions for all documents via bulk download [@shamoon](https://github.com/shamoon) ([#8631](https://github.com/paperless-ngx/paperless-ngx/pull/8631))
|
||||
- Chore(deps): Bump the small-changes group with 3 updates [@dependabot](https://github.com/dependabot) ([#8627](https://github.com/paperless-ngx/paperless-ngx/pull/8627))
|
||||
- Chore(deps-dev): Bump ruff from 0.8.4 to 0.8.6 in the development group [@dependabot](https://github.com/dependabot) ([#8626](https://github.com/paperless-ngx/paperless-ngx/pull/8626))
|
||||
- Chore: Switch from os.path to pathlib.Path [@gothicVI](https://github.com/gothicVI) ([#8325](https://github.com/paperless-ngx/paperless-ngx/pull/8325))
|
||||
- Chore: disable max-age for some document endpoints [@tsia](https://github.com/tsia) ([#8611](https://github.com/paperless-ngx/paperless-ngx/pull/8611))
|
||||
- Fix: do not accept empty string for doc link value via API [@shamoon](https://github.com/shamoon) ([#8596](https://github.com/paperless-ngx/paperless-ngx/pull/8596))
|
||||
- Enhancement: angular 19 [@shamoon](https://github.com/shamoon) ([#8584](https://github.com/paperless-ngx/paperless-ngx/pull/8584))
|
||||
- Fix: fix hotkey arrows [@shamoon](https://github.com/shamoon) ([#8583](https://github.com/paperless-ngx/paperless-ngx/pull/8583))
|
||||
- Chore: remove outdated admin logentry handler [@shamoon](https://github.com/shamoon) ([#8580](https://github.com/paperless-ngx/paperless-ngx/pull/8580))
|
||||
- Chore(deps): Bump django-allauth from 65.3.0 to 65.3.1 in the django group [@dependabot](https://github.com/dependabot) ([#8574](https://github.com/paperless-ngx/paperless-ngx/pull/8574))
|
||||
- Enhancement: custom field sorting [@shamoon](https://github.com/shamoon) ([#8494](https://github.com/paperless-ngx/paperless-ngx/pull/8494))
|
||||
- Fix: fix occasional error toast overflow [@shamoon](https://github.com/shamoon) ([#8552](https://github.com/paperless-ngx/paperless-ngx/pull/8552))
|
||||
- Fix: fix share link archive version detection [@shamoon](https://github.com/shamoon) ([#8551](https://github.com/paperless-ngx/paperless-ngx/pull/8551))
|
||||
- Chore(deps-dev): Bump ruff from 0.8.3 to 0.8.4 in the development group [@dependabot](https://github.com/dependabot) ([#8546](https://github.com/paperless-ngx/paperless-ngx/pull/8546))
|
||||
- Chore(deps): Bump the small-changes group with 6 updates [@dependabot](https://github.com/dependabot) ([#8547](https://github.com/paperless-ngx/paperless-ngx/pull/8547))
|
||||
- Enhancement: add timeout for Tika client [@HiranChaudhuri](https://github.com/HiranChaudhuri) ([#8520](https://github.com/paperless-ngx/paperless-ngx/pull/8520))
|
||||
- Fix: add some minor frontend permissions checks [@shamoon](https://github.com/shamoon) ([#8524](https://github.com/paperless-ngx/paperless-ngx/pull/8524))
|
||||
- FIx: obliquely trim spaces from global search [@shamoon](https://github.com/shamoon) ([#8484](https://github.com/paperless-ngx/paperless-ngx/pull/8484))
|
||||
- Fix: include global perms for bulk edit endpoint [@shamoon](https://github.com/shamoon) ([#8468](https://github.com/paperless-ngx/paperless-ngx/pull/8468))
|
||||
- Enhancement: process mail button [@shamoon](https://github.com/shamoon) ([#8466](https://github.com/paperless-ngx/paperless-ngx/pull/8466))
|
||||
- Fix: frontend better reflect global perms for bulk edit, disabled form state [@shamoon](https://github.com/shamoon) ([#8469](https://github.com/paperless-ngx/paperless-ngx/pull/8469))
|
||||
- Chore: update ng2 pdf viewer [@shamoon](https://github.com/shamoon) ([#8462](https://github.com/paperless-ngx/paperless-ngx/pull/8462))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#8458](https://github.com/paperless-ngx/paperless-ngx/pull/8458))
|
||||
- Chore(deps): Bump django-soft-delete from 1.0.15 to 1.0.16 in the django group [@dependabot](https://github.com/dependabot) ([#8459](https://github.com/paperless-ngx/paperless-ngx/pull/8459))
|
||||
- Chore(deps): Bump the small-changes group with 4 updates [@dependabot](https://github.com/dependabot) ([#8460](https://github.com/paperless-ngx/paperless-ngx/pull/8460))
|
||||
- Chore: use rxjs instead of JS setInterval for timers [@shamoon](https://github.com/shamoon) ([#8461](https://github.com/paperless-ngx/paperless-ngx/pull/8461))
|
||||
- Feature: bulk edit custom field values [@shamoon](https://github.com/shamoon) ([#8428](https://github.com/paperless-ngx/paperless-ngx/pull/8428))
|
||||
- Enhancement: improved loading visuals [@shamoon](https://github.com/shamoon) ([#8435](https://github.com/paperless-ngx/paperless-ngx/pull/8435))
|
||||
- Enhancement: prune audit logs and management command [@shamoon](https://github.com/shamoon) ([#8416](https://github.com/paperless-ngx/paperless-ngx/pull/8416))
|
||||
- Change: make saved views manage its own component [@shamoon](https://github.com/shamoon) ([#8423](https://github.com/paperless-ngx/paperless-ngx/pull/8423))
|
||||
- Enhancement: file task filtering [@shamoon](https://github.com/shamoon) ([#8421](https://github.com/paperless-ngx/paperless-ngx/pull/8421))
|
||||
- Enhancement: auto-link duplicate document [@shamoon](https://github.com/shamoon) ([#8415](https://github.com/paperless-ngx/paperless-ngx/pull/8415))
|
||||
- Enhancement: include current filename placeholder in workflows @Lu-Fi ([#8319](https://github.com/paperless-ngx/paperless-ngx/pull/8319))
|
||||
- Feature: email, webhook workflow actions [@shamoon](https://github.com/shamoon) ([#8108](https://github.com/paperless-ngx/paperless-ngx/pull/8108))
|
||||
- Chore(deps-dev): Bump the development group with 4 updates [@dependabot](https://github.com/dependabot) ([#8414](https://github.com/paperless-ngx/paperless-ngx/pull/8414))
|
||||
- Enhancement: use stable unique IDs for custom field select options [@shamoon](https://github.com/shamoon) ([#8299](https://github.com/paperless-ngx/paperless-ngx/pull/8299))
|
||||
- Chore(deps-dev): Bump the development group with 4 updates [@dependabot](https://github.com/dependabot) ([#8352](https://github.com/paperless-ngx/paperless-ngx/pull/8352))
|
||||
- Enhancement: better TIFF display browser support [@shamoon](https://github.com/shamoon) ([#8087](https://github.com/paperless-ngx/paperless-ngx/pull/8087))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 7 updates [@dependabot](https://github.com/dependabot) ([#8399](https://github.com/paperless-ngx/paperless-ngx/pull/8399))
|
||||
- Enhancement: History (audit log) for bulk edit operations [@shamoon](https://github.com/shamoon) ([#8196](https://github.com/paperless-ngx/paperless-ngx/pull/8196))
|
||||
- Enhancement: larger previews in action dialogs [@shamoon](https://github.com/shamoon) ([#8387](https://github.com/paperless-ngx/paperless-ngx/pull/8387))
|
||||
- Enhancement: filterable list count sorting and opacification [@shamoon](https://github.com/shamoon) ([#8386](https://github.com/paperless-ngx/paperless-ngx/pull/8386))
|
||||
- Enhancement: preview button for document list and trash, refactor [@shamoon](https://github.com/shamoon) ([#8384](https://github.com/paperless-ngx/paperless-ngx/pull/8384))
|
||||
- Fixhancement: dispatch change event from current field prior to save [@shamoon](https://github.com/shamoon) ([#8369](https://github.com/paperless-ngx/paperless-ngx/pull/8369))
|
||||
- Enhancement: use theme-color meta tag [@shamoon](https://github.com/shamoon) ([#8359](https://github.com/paperless-ngx/paperless-ngx/pull/8359))
|
||||
- Chore: cleanup urls, use actions for some views [@shamoon](https://github.com/shamoon) ([#8346](https://github.com/paperless-ngx/paperless-ngx/pull/8346))
|
||||
- Feature: scheduled workflow trigger [@shamoon](https://github.com/shamoon) ([#8036](https://github.com/paperless-ngx/paperless-ngx/pull/8036))
|
||||
- Fix: handle very old dates with positive offset too [@shamoon](https://github.com/shamoon) ([#8335](https://github.com/paperless-ngx/paperless-ngx/pull/8335))
|
||||
- Refactor: fix unnecessary use of filterable dropdown sorting [@shamoon](https://github.com/shamoon) ([#8328](https://github.com/paperless-ngx/paperless-ngx/pull/8328))
|
||||
- Enhancement: offer link to restored document [@shamoon](https://github.com/shamoon) ([#8321](https://github.com/paperless-ngx/paperless-ngx/pull/8321))
|
||||
- Enhancement: support owner permissions for file tasks [@shamoon](https://github.com/shamoon) ([#8195](https://github.com/paperless-ngx/paperless-ngx/pull/8195))
|
||||
- Fixhancement: change update content to handle archive disabled [@shamoon](https://github.com/shamoon) ([#8315](https://github.com/paperless-ngx/paperless-ngx/pull/8315))
|
||||
- Chore(deps): Bump watchdog from 5.0.3 to 6.0.0 in the major-versions group [@dependabot](https://github.com/dependabot) ([#8257](https://github.com/paperless-ngx/paperless-ngx/pull/8257))
|
||||
- Enhancement: Add --compare-json option to document_exporter to write json files only if changed [@kdoren](https://github.com/kdoren) ([#8261](https://github.com/paperless-ngx/paperless-ngx/pull/8261))
|
||||
- Enhancement: next / previous shortcuts for document list [@shamoon](https://github.com/shamoon) ([#8309](https://github.com/paperless-ngx/paperless-ngx/pull/8309))
|
||||
- Feature: two-factor authentication [@shamoon](https://github.com/shamoon) ([#8012](https://github.com/paperless-ngx/paperless-ngx/pull/8012))
|
||||
- Fix: include db_index caveat in squashed migrations [@shamoon](https://github.com/shamoon) ([#8292](https://github.com/paperless-ngx/paperless-ngx/pull/8292))
|
||||
- Tweak: use fixed position for navbar [@shamoon](https://github.com/shamoon) ([#8279](https://github.com/paperless-ngx/paperless-ngx/pull/8279))
|
||||
- Fix: prevent duplicate workflow runs [@shamoon](https://github.com/shamoon) ([#8268](https://github.com/paperless-ngx/paperless-ngx/pull/8268))
|
||||
- Fix: add note about select options to edit dialog [@shamoon](https://github.com/shamoon) ([#8267](https://github.com/paperless-ngx/paperless-ngx/pull/8267))
|
||||
- Enhancement: save \& next / close shortcut key [@shamoon](https://github.com/shamoon) ([#8243](https://github.com/paperless-ngx/paperless-ngx/pull/8243))
|
||||
- Feature: loading preview, better text popup preview [@shamoon](https://github.com/shamoon) ([#8011](https://github.com/paperless-ngx/paperless-ngx/pull/8011))
|
||||
- Chore: switch src/documents/bulk\*.py from os.path to pathlib.Path [@gothicVI](https://github.com/gothicVI) ([#7862](https://github.com/paperless-ngx/paperless-ngx/pull/7862))
|
||||
- Chore: Bulk backend dependency updates [@stumpylog](https://github.com/stumpylog) ([#8212](https://github.com/paperless-ngx/paperless-ngx/pull/8212))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.13.5
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1073,8 +1073,6 @@ or hidden folders some tools use to store data.
|
||||
If you have problems that your Barcodes/QR-Codes are not detected
|
||||
(especially with bad scan quality and/or small codes), try the other one.
|
||||
|
||||
zxing is not available on all platforms.
|
||||
|
||||
#### [`PAPERLESS_PRE_CONSUME_SCRIPT=<filename>`](#PAPERLESS_PRE_CONSUME_SCRIPT) {#PAPERLESS_PRE_CONSUME_SCRIPT}
|
||||
|
||||
: After some initial validation, Paperless can trigger an arbitrary
|
||||
|
||||
+2
-2
@@ -35,8 +35,8 @@ account. The script essentially automatically performs the steps described in [D
|
||||
|
||||
!!! note
|
||||
|
||||
macOS users will need to install e.g. [gnu-sed](https://formulae.brew.sh/formula/gnu-sed) with support
|
||||
for running as `sed`.
|
||||
macOS users will need to install [gnu-sed](https://formulae.brew.sh/formula/gnu-sed) with support
|
||||
for running as `sed` as well as [wget](https://formulae.brew.sh/formula/wget).
|
||||
|
||||
### Use Docker Compose {#docker}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ The following error occurred while consuming document.pdf: [Errno 13] Permission
|
||||
This happens when paperless does not have permission to delete files
|
||||
inside the consumption directory. Ensure that `USERMAP_UID` and
|
||||
`USERMAP_GID` are set to the user id and group id you use on the host
|
||||
operating system, if these are different from `1000`. See [Docker setup](setup.md#docker_hub).
|
||||
operating system, if these are different from `1000`. See [Docker setup](setup.md#docker).
|
||||
|
||||
Also ensure that you are able to read and write to the consumption
|
||||
directory on the host.
|
||||
|
||||
+56
-8
@@ -1,9 +1,9 @@
|
||||
# Usage Overview
|
||||
|
||||
Paperless is an application that manages your personal documents. With
|
||||
the help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)),
|
||||
paperless transforms your unwieldy physical document binders into a searchable archive
|
||||
and provides many utilities for finding and managing your documents.
|
||||
Paperless-ngx is an application that manages your personal documents. With
|
||||
the (optional) help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), Paperless-ngx transforms your unwieldy
|
||||
physical documents into a searchable archive and provides many utilities
|
||||
for finding and managing your documents.
|
||||
|
||||
## Terms and definitions
|
||||
|
||||
@@ -12,10 +12,10 @@ documents:
|
||||
|
||||
- The _consumer_ watches a specified folder and adds all documents in
|
||||
that folder to paperless.
|
||||
- The _web server_ provides a UI that you use to manage and search for
|
||||
your scanned documents.
|
||||
- The _web server_ (web UI) provides a UI that you use to manage and
|
||||
search documents.
|
||||
|
||||
Each document has a couple of fields that you can assign to them:
|
||||
Each document has data fields that you can assign to them:
|
||||
|
||||
- A _Document_ is a piece of paper that sometimes contains valuable
|
||||
information.
|
||||
@@ -41,6 +41,53 @@ Each document has a couple of fields that you can assign to them:
|
||||
- The _content_ of a document is the text that was OCR'ed from the
|
||||
document. This text is fed into the search engine and is used for
|
||||
matching tags, correspondents and document types.
|
||||
- Paperless-ngx also supports _custom fields_ which can be used to
|
||||
store additional metadata about a document.
|
||||
|
||||
## The Web UI
|
||||
|
||||
The web UI is the primary way to interact with Paperless-ngx. It is a
|
||||
single-page application that is built with modern web technologies and
|
||||
is designed to be fast and responsive. The web UI includes a robust
|
||||
interface for filtering, viewing, searching and editing documents.
|
||||
You can also manage tags, correspondents, document types, and other
|
||||
settings from the web UI.
|
||||
|
||||
The web UI also includes a 'tour' feature that can be accessed from the
|
||||
settings page or from the dashboard for new users. The tour highlights
|
||||
some of the key features of the web UI and can be useful for new users.
|
||||
|
||||
### Dashboard
|
||||
|
||||
The dashboard is the first page you see when you log in. By default, it
|
||||
does not show any documents, but you can add saved views to the dashboard
|
||||
to show documents that match certain criteria. The dashboard also includes
|
||||
a button to upload documents to Paperless-ngx but you can also drag and
|
||||
drop files anywhere in the app to initiate the consumption process.
|
||||
|
||||
### Document List
|
||||
|
||||
The document list is the primary way to view and interact with your documents.
|
||||
You can filter the list by tags, correspondents, document types, and other
|
||||
criteria. You can also edit documents in bulk including assigning tags,
|
||||
correspondents, document types, and custom fields. Selecting document(s) from
|
||||
the list will allow you to perform the various bulk edit operations. The
|
||||
document list also includes a search bar that allows you to search for documents
|
||||
by title, ASN, and use advanced search syntax.
|
||||
|
||||
### Document Detail
|
||||
|
||||
The document detail page shows all the information about a single document.
|
||||
You can view the document, edit its metadata, assign tags, correspondents,
|
||||
document types, and custom fields. You can also view the document history,
|
||||
download the document or share it via a share link.
|
||||
|
||||
### Management Lists
|
||||
|
||||
Paperless-ngx includes management lists for tags, correspondents, document types
|
||||
and more. These areas allow you to view, add, edit, delete and manage permissions
|
||||
for these objects. You can also manage saved views, mail accounts, mail rules,
|
||||
workflows and more from the management sections.
|
||||
|
||||
## Adding documents to paperless
|
||||
|
||||
@@ -252,7 +299,7 @@ permissions can be granted to limit access to certain parts of the UI (and corre
|
||||
|
||||
#### Superusers
|
||||
|
||||
Superusers can access all parts of the front and backend application as well as any and all objects.
|
||||
Superusers can access all parts of the front and backend application as well as any and all objects. Superuser status can only be granted by another superuser.
|
||||
|
||||
#### Admin Status
|
||||
|
||||
@@ -419,6 +466,7 @@ The following workflow action types are available:
|
||||
|
||||
- The URL to send the request to
|
||||
- The request body as text or as key-value pairs, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below.
|
||||
- Encoding for the request body, either JSON or form data
|
||||
- The request headers as key-value pairs
|
||||
|
||||
#### Workflow placeholders
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import os
|
||||
# See https://docs.gunicorn.org/en/stable/settings.html for
|
||||
# explanations of settings
|
||||
|
||||
bind = f'{os.getenv("PAPERLESS_BIND_ADDR", "[::]")}:{os.getenv("PAPERLESS_PORT", 8000)}'
|
||||
bind = f"{os.getenv('PAPERLESS_BIND_ADDR', '[::]')}:{os.getenv('PAPERLESS_PORT', 8000)}"
|
||||
|
||||
workers = int(os.getenv("PAPERLESS_WEBSERVER_WORKERS", 1))
|
||||
worker_class = "paperless.workers.ConfigurableWorker"
|
||||
|
||||
+2
-1
@@ -81,7 +81,8 @@
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"ng2-pdf-viewer",
|
||||
"file-saver"
|
||||
"file-saver",
|
||||
"utif"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
|
||||
+729
-666
File diff suppressed because it is too large
Load Diff
Generated
+1840
-1900
File diff suppressed because it is too large
Load Diff
+30
-29
@@ -11,17 +11,17 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^19.0.2",
|
||||
"@angular/common": "~19.0.3",
|
||||
"@angular/compiler": "~19.0.3",
|
||||
"@angular/core": "~19.0.3",
|
||||
"@angular/forms": "~19.0.3",
|
||||
"@angular/localize": "~19.0.3",
|
||||
"@angular/platform-browser": "~19.0.3",
|
||||
"@angular/platform-browser-dynamic": "~19.0.3",
|
||||
"@angular/router": "~19.0.3",
|
||||
"@angular/cdk": "^19.1.2",
|
||||
"@angular/common": "~19.1.4",
|
||||
"@angular/compiler": "~19.1.4",
|
||||
"@angular/core": "~19.1.4",
|
||||
"@angular/forms": "~19.1.4",
|
||||
"@angular/localize": "~19.1.4",
|
||||
"@angular/platform-browser": "~19.1.4",
|
||||
"@angular/platform-browser-dynamic": "~19.1.4",
|
||||
"@angular/router": "~19.1.4",
|
||||
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
||||
"@ng-select/ng-select": "^14.1.0",
|
||||
"@ng-select/ng-select": "^14.2.0",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
@@ -30,36 +30,37 @@
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-bootstrap-icons": "^1.9.3",
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^19.0.0",
|
||||
"ngx-cookie-service": "^19.1.0",
|
||||
"ngx-device-detector": "^9.0.0",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^16.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.8.1",
|
||||
"utif": "^3.1.0",
|
||||
"uuid": "^11.0.2",
|
||||
"uuid": "^11.0.5",
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "^19.0.0-beta.0",
|
||||
"@angular-builders/jest": "^19.0.0-beta.1",
|
||||
"@angular-builders/custom-webpack": "^19.0.0",
|
||||
"@angular-builders/jest": "^19.0.0",
|
||||
"@angular-devkit/build-angular": "^19.0.4",
|
||||
"@angular-devkit/core": "^19.0.4",
|
||||
"@angular-devkit/schematics": "^19.0.4",
|
||||
"@angular-eslint/builder": "19.0.0",
|
||||
"@angular-eslint/eslint-plugin": "19.0.0",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.0",
|
||||
"@angular-eslint/schematics": "19.0.0",
|
||||
"@angular-eslint/template-parser": "19.0.0",
|
||||
"@angular/cli": "~19.0.4",
|
||||
"@angular/compiler-cli": "~19.0.3",
|
||||
"@codecov/webpack-plugin": "^1.2.1",
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@angular-devkit/core": "^19.1.5",
|
||||
"@angular-devkit/schematics": "^19.1.5",
|
||||
"@angular-eslint/builder": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.2",
|
||||
"@angular-eslint/schematics": "19.0.2",
|
||||
"@angular-eslint/template-parser": "19.0.2",
|
||||
"@angular/cli": "~19.1.5",
|
||||
"@angular/compiler-cli": "~19.1.4",
|
||||
"@codecov/webpack-plugin": "^1.8.0",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.8.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.12.2",
|
||||
"@typescript-eslint/parser": "^8.12.2",
|
||||
"@types/node": "^22.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.22.0",
|
||||
"@typescript-eslint/parser": "^8.22.0",
|
||||
"@typescript-eslint/utils": "^8.0.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint": "^9.19.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^14.4.2",
|
||||
|
||||
@@ -100,6 +100,15 @@ Object.defineProperty(navigator, 'clipboard', {
|
||||
},
|
||||
})
|
||||
Object.defineProperty(navigator, 'canShare', { value: () => true })
|
||||
if (!navigator.share) {
|
||||
Object.defineProperty(navigator, 'share', { value: jest.fn() })
|
||||
}
|
||||
if (!URL.createObjectURL) {
|
||||
Object.defineProperty(window.URL, 'createObjectURL', { value: jest.fn() })
|
||||
}
|
||||
if (!URL.revokeObjectURL) {
|
||||
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
|
||||
}
|
||||
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
|
||||
@@ -18,20 +18,20 @@ import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||
import { PermissionsGuard } from './guards/permissions.guard'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from './services/consumer-status.service'
|
||||
import { HotKeyService } from './services/hot-key.service'
|
||||
import { PermissionsService } from './services/permissions.service'
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { Toast, ToastService } from './services/toast.service'
|
||||
import {
|
||||
FileStatus,
|
||||
WebsocketStatusService,
|
||||
} from './services/websocket-status.service'
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent
|
||||
let fixture: ComponentFixture<AppComponent>
|
||||
let tourService: TourService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let permissionsService: PermissionsService
|
||||
let toastService: ToastService
|
||||
let router: Router
|
||||
@@ -59,7 +59,7 @@ describe('AppComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
tourService = TestBed.inject(TourService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
@@ -90,7 +90,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
const status = new FileStatus()
|
||||
@@ -109,7 +109,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
@@ -122,7 +122,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentDetected')
|
||||
.spyOn(websocketStatusService, 'onDocumentDetected')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
@@ -136,7 +136,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentDetected')
|
||||
.spyOn(websocketStatusService, 'onDocumentDetected')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
@@ -148,7 +148,7 @@ describe('AppComponent', () => {
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFailed')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFailed')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
|
||||
@@ -5,7 +5,7 @@ import { first, Subscription } from 'rxjs'
|
||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||
import { SETTINGS_KEYS } from './data/ui-settings'
|
||||
import { ConsumerStatusService } from './services/consumer-status.service'
|
||||
import { ComponentRouterService } from './services/component-router.service'
|
||||
import { HotKeyService } from './services/hot-key.service'
|
||||
import {
|
||||
PermissionAction,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { SettingsService } from './services/settings.service'
|
||||
import { TasksService } from './services/tasks.service'
|
||||
import { ToastService } from './services/toast.service'
|
||||
import { WebsocketStatusService } from './services/websocket-status.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-root',
|
||||
@@ -34,14 +35,15 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private settings: SettingsService,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
private toastService: ToastService,
|
||||
private router: Router,
|
||||
private tasksService: TasksService,
|
||||
public tourService: TourService,
|
||||
private renderer: Renderer2,
|
||||
private permissionsService: PermissionsService,
|
||||
private hotKeyService: HotKeyService
|
||||
private hotKeyService: HotKeyService,
|
||||
private componentRouterService: ComponentRouterService
|
||||
) {
|
||||
let anyWindow = window as any
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.mjs'
|
||||
@@ -49,7 +51,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.consumerStatusService.disconnect()
|
||||
this.websocketStatusService.disconnect()
|
||||
if (this.successSubscription) {
|
||||
this.successSubscription.unsubscribe()
|
||||
}
|
||||
@@ -74,9 +76,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.consumerStatusService.connect()
|
||||
this.websocketStatusService.connect()
|
||||
|
||||
this.successSubscription = this.consumerStatusService
|
||||
this.successSubscription = this.websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
@@ -106,7 +108,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
})
|
||||
|
||||
this.failedSubscription = this.consumerStatusService
|
||||
this.failedSubscription = this.websocketStatusService
|
||||
.onDocumentConsumptionFailed()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
@@ -119,7 +121,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
})
|
||||
|
||||
this.newDocumentSubscription = this.consumerStatusService
|
||||
this.newDocumentSubscription = this.websocketStatusService
|
||||
.onDocumentDetected()
|
||||
.subscribe((status) => {
|
||||
this.tasksService.reload()
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row">
|
||||
<div class="col-xl-6 pe-xl-5">
|
||||
<h4 i18n>Appearance</h4>
|
||||
<h5 i18n>Appearance</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 col-form-label pt-0">
|
||||
<span i18n>Display language</span>
|
||||
@@ -154,28 +154,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Document editing</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show document thumbnail during loading" formControlName="documentEditingOverlayThumbnail"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-6 ps-xl-5">
|
||||
<h4 class="mt-4 mt-md-0" id="update-checking" i18n>Update checking</h4>
|
||||
<h5 class="mt-3" id="update-checking" i18n>Update checking</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col d-flex flex-row align-items-start">
|
||||
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
|
||||
@@ -193,7 +172,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Bulk editing</h4>
|
||||
<h5 class="mt-3" i18n>Saved Views</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-xl-6 ps-xl-5">
|
||||
<h5 class="mt-3 mt-md-0" i18n>Document editing</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-2">
|
||||
<span i18n>Default zoom:</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<select class="form-select" formControlName="pdfViewerDefaultZoom">
|
||||
<option [ngValue]="ZoomSetting.PageWidth" i18n>Fit width</option>
|
||||
<option [ngValue]="ZoomSetting.PageFit" i18n>Fit page</option>
|
||||
</select>
|
||||
<p class="small text-muted mt-1" i18n>Only applies to the Paperless-ngx PDF viewer.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show document thumbnail during loading" formControlName="documentEditingOverlayThumbnail"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-3" i18n>Notes</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mt-3" i18n>Bulk editing</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs"></pngx-input-check>
|
||||
@@ -201,7 +229,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Global search</h4>
|
||||
<h5 class="mt-3" i18n>Global search</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Do not include advanced search results" formControlName="searchDbOnly"></pngx-input-check>
|
||||
@@ -224,19 +252,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Saved Views</h4>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Notes</h4>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<pngx-input-check i18n-title title="Enable notes" formControlName="notesEnabled"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -247,7 +262,7 @@
|
||||
<a ngbNavLink i18n>Permissions</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Default Permissions</h4>
|
||||
<h5 i18n>Default Permissions</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
@@ -329,7 +344,7 @@
|
||||
<a ngbNavLink i18n>Notifications</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<h4 i18n>Document processing</h4>
|
||||
<h5 i18n>Document processing</h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('SettingsComponent', () => {
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
expect(storeSpy).toHaveBeenCalled()
|
||||
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
||||
expect(setSpy).toHaveBeenCalledTimes(28)
|
||||
expect(setSpy).toHaveBeenCalledTimes(29)
|
||||
|
||||
// succeed
|
||||
storeSpy.mockReturnValueOnce(of(true))
|
||||
|
||||
@@ -63,6 +63,7 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss
|
||||
import { SelectComponent } from '../../common/input/select/select.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||
import { ZoomSetting } from '../../document-detail/document-detail.component'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
|
||||
enum SettingsNavIDs {
|
||||
@@ -125,6 +126,7 @@ export class SettingsComponent
|
||||
defaultPermsEditUsers: new FormControl(null),
|
||||
defaultPermsEditGroups: new FormControl(null),
|
||||
useNativePdfViewer: new FormControl(null),
|
||||
pdfViewerDefaultZoom: new FormControl(null),
|
||||
documentEditingRemoveInboxTags: new FormControl(null),
|
||||
documentEditingOverlayThumbnail: new FormControl(null),
|
||||
searchDbOnly: new FormControl(null),
|
||||
@@ -154,6 +156,8 @@ export class SettingsComponent
|
||||
|
||||
public readonly GlobalSearchType = GlobalSearchType
|
||||
|
||||
public readonly ZoomSetting = ZoomSetting
|
||||
|
||||
get systemStatusHasErrors(): boolean {
|
||||
return (
|
||||
this.systemStatus.database.status === SystemStatusItemStatus.ERROR ||
|
||||
@@ -276,6 +280,9 @@ export class SettingsComponent
|
||||
useNativePdfViewer: this.settings.get(
|
||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER
|
||||
),
|
||||
pdfViewerDefaultZoom: this.settings.get(
|
||||
SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING
|
||||
),
|
||||
displayLanguage: this.settings.getLanguage(),
|
||||
dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE),
|
||||
dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT),
|
||||
@@ -435,6 +442,10 @@ export class SettingsComponent
|
||||
SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER,
|
||||
this.settingsForm.value.useNativePdfViewer
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
||||
this.settingsForm.value.pdfViewerDefaultZoom
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.DATE_LOCALE,
|
||||
this.settingsForm.value.dateLocale
|
||||
|
||||
@@ -86,12 +86,17 @@ export class TrashComponent
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.trashService.emptyTrash([document.id]).subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Document deleted`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document "${document.title}" deleted`
|
||||
)
|
||||
modal.close()
|
||||
this.reload()
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.showError($localize`Error deleting document`, err)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting document "${document.title}"`,
|
||||
err
|
||||
)
|
||||
modal.close()
|
||||
},
|
||||
})
|
||||
@@ -136,7 +141,7 @@ export class TrashComponent
|
||||
this.trashService.restoreDocuments([document.id]).subscribe({
|
||||
next: () => {
|
||||
this.toastService.show({
|
||||
content: $localize`Document restored`,
|
||||
content: $localize`Document "${document.title}" restored`,
|
||||
delay: 5000,
|
||||
actionName: $localize`Open document`,
|
||||
action: () => {
|
||||
@@ -146,7 +151,10 @@ export class TrashComponent
|
||||
this.reload()
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.showError($localize`Error restoring document`, err)
|
||||
this.toastService.showError(
|
||||
$localize`Error restoring document "${document.title}"`,
|
||||
err
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('UsersAndGroupsComponent', () => {
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user "user1"')
|
||||
})
|
||||
|
||||
it('should logout current user if password changed, after delay', fakeAsync(() => {
|
||||
@@ -178,7 +178,7 @@ describe('UsersAndGroupsComponent', () => {
|
||||
completeSetup()
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.deleteGroup(users[0])
|
||||
component.deleteGroup(groups[0])
|
||||
const deleteDialog = modal.componentInstance as ConfirmDialogComponent
|
||||
const deleteSpy = jest.spyOn(groupService, 'delete')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
@@ -192,7 +192,7 @@ describe('UsersAndGroupsComponent', () => {
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group "group1"')
|
||||
})
|
||||
|
||||
it('should get group name', () => {
|
||||
|
||||
@@ -129,13 +129,16 @@ export class UsersAndGroupsComponent
|
||||
this.usersService.delete(user).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted user`)
|
||||
this.toastService.showInfo($localize`Deleted user "${user.username}"`)
|
||||
this.usersService.listAll().subscribe((r) => {
|
||||
this.users = r.results
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting user.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting user "${user.username}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -179,13 +182,16 @@ export class UsersAndGroupsComponent
|
||||
this.groupsService.delete(group).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted group`)
|
||||
this.toastService.showInfo($localize`Deleted group "${group.name}"`)
|
||||
this.groupsService.listAll().subscribe((r) => {
|
||||
this.groups = r.results
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting group.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting group "${group.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
+11
-2
@@ -1,4 +1,8 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
import {
|
||||
CdkDragDrop,
|
||||
DragDropModule,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
@@ -13,7 +17,12 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
selector: 'pngx-merge-confirm-dialog',
|
||||
templateUrl: './merge-confirm-dialog.component.html',
|
||||
styleUrl: './merge-confirm-dialog.component.scss',
|
||||
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
|
||||
imports: [
|
||||
DragDropModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class MergeConfirmDialogComponent
|
||||
extends ConfirmDialogComponent
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common'
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { DisplayField, Document } from 'src/app/data/document'
|
||||
@@ -13,7 +14,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
||||
selector: 'pngx-custom-field-display',
|
||||
templateUrl: './custom-field-display.component.html',
|
||||
styleUrl: './custom-field-display.component.scss',
|
||||
imports: [CustomDatePipe, CurrencyPipe],
|
||||
imports: [CustomDatePipe, CurrencyPipe, NgbTooltipModule],
|
||||
})
|
||||
export class CustomFieldDisplayComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
|
||||
+6
-1
@@ -9,7 +9,11 @@ import {
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import {
|
||||
NgbDatepickerModule,
|
||||
NgbDropdown,
|
||||
NgbDropdownModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { first, Subject, takeUntil } from 'rxjs'
|
||||
@@ -164,6 +168,7 @@ export class CustomFieldQueriesModel {
|
||||
ClearableBadgeComponent,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbDatepickerModule,
|
||||
NgTemplateOutlet,
|
||||
NgSelectModule,
|
||||
NgxBootstrapIconsModule,
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ const CONSUMPTION_SCOPE_OPTIONS = [
|
||||
name: $localize`Only process attachments`,
|
||||
},
|
||||
{
|
||||
id: MailRuleConsumptionScope.Email_Only,
|
||||
id: MailRuleConsumptionScope.EmailOnly,
|
||||
name: $localize`Process message as .eml`,
|
||||
},
|
||||
{
|
||||
|
||||
+19
@@ -160,4 +160,23 @@ describe('UserEditDialogComponent', () => {
|
||||
})
|
||||
expect(component.currentUserIsSuperUser).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should disable superuser option if current user is not superuser', () => {
|
||||
const control: AbstractControl = component.objectForm.get('is_superuser')
|
||||
permissionsService.initialize([], {
|
||||
id: 99,
|
||||
username: 'user99',
|
||||
is_superuser: false,
|
||||
})
|
||||
component.ngOnInit()
|
||||
expect(control.disabled).toBeTruthy()
|
||||
|
||||
permissionsService.initialize([], {
|
||||
id: 99,
|
||||
username: 'user99',
|
||||
is_superuser: true,
|
||||
})
|
||||
component.ngOnInit()
|
||||
expect(control.disabled).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
+5
@@ -60,6 +60,11 @@ export class UserEditDialogComponent
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
this.onToggleSuperUser()
|
||||
if (!this.currentUserIsSuperUser) {
|
||||
this.objectForm.get('is_superuser').disable()
|
||||
} else {
|
||||
this.objectForm.get('is_superuser').enable()
|
||||
}
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
|
||||
+5
-2
@@ -180,7 +180,7 @@
|
||||
<input type="hidden" formControlName="id" />
|
||||
<pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
|
||||
@switch(formGroup.get('type').value) {
|
||||
@case ( WorkflowActionType.Assignment) {
|
||||
@case (WorkflowActionType.Assignment) {
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
|
||||
@@ -338,7 +338,10 @@
|
||||
<input type="hidden" formControlName="id" />
|
||||
<div class="col">
|
||||
<pngx-input-text i18n-title title="Webhook url" formControlName="url" [error]="error?.actions?.[i]?.url"></pngx-input-text>
|
||||
<pngx-input-switch i18n-title title="Use parameters for webhook body" formControlName="use_params"></pngx-input-switch>
|
||||
<div class="d-flex">
|
||||
<pngx-input-switch i18n-title title="Use parameters for webhook body" formControlName="use_params" [horizontal]="true"></pngx-input-switch>
|
||||
<pngx-input-switch i18n-title title="Send webhook payload as JSON" formControlName="as_json" [horizontal]="true" class="ms-5"></pngx-input-switch>
|
||||
</div>
|
||||
@if (formGroup.get('webhook').value['use_params']) {
|
||||
<pngx-input-entries i18n-title title="Webhook params" formControlName="params" [error]="error?.actions?.[i]?.params"></pngx-input-entries>
|
||||
} @else {
|
||||
|
||||
+2
@@ -471,6 +471,7 @@ export class WorkflowEditDialogComponent
|
||||
id: new FormControl(action.webhook?.id),
|
||||
url: new FormControl(action.webhook?.url),
|
||||
use_params: new FormControl(action.webhook?.use_params),
|
||||
as_json: new FormControl(action.webhook?.as_json),
|
||||
params: new FormControl(action.webhook?.params),
|
||||
body: new FormControl(action.webhook?.body),
|
||||
headers: new FormControl(action.webhook?.headers),
|
||||
@@ -588,6 +589,7 @@ export class WorkflowEditDialogComponent
|
||||
id: null,
|
||||
url: null,
|
||||
use_params: true,
|
||||
as_json: false,
|
||||
params: null,
|
||||
body: null,
|
||||
headers: null,
|
||||
|
||||
+13
-6
@@ -1,3 +1,5 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
@@ -51,7 +53,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [FilterPipe],
|
||||
providers: [
|
||||
FilterPipe,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -490,18 +496,19 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
])
|
||||
})
|
||||
|
||||
it('selection model should sort items by state and document counts, if set', () => {
|
||||
component.items = items.concat([{ id: 4, name: 'Item D' }])
|
||||
it('selection model should sort items by state and document counts = 0, if set', () => {
|
||||
const tagA = { id: 4, name: 'Tag A' }
|
||||
component.items = items.concat([tagA])
|
||||
component.selectionModel = selectionModel
|
||||
component.documentCounts = [
|
||||
{ id: 1, document_count: 0 }, // Tag1
|
||||
{ id: 2, document_count: 1 }, // Tag2
|
||||
{ id: 4, document_count: 2 },
|
||||
{ id: 4, document_count: 2 }, // Tag A
|
||||
]
|
||||
component.selectionModel.apply()
|
||||
expect(selectionModel.items).toEqual([
|
||||
nullItem,
|
||||
{ id: 4, name: 'Item D' },
|
||||
tagA,
|
||||
items[1], // Tag2
|
||||
items[0], // Tag1
|
||||
])
|
||||
@@ -516,7 +523,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
expect(selectionModel.items).toEqual([
|
||||
nullItem,
|
||||
items[1], // Tag2
|
||||
{ id: 4, name: 'Item D' },
|
||||
tagA,
|
||||
items[0], // Tag1
|
||||
])
|
||||
})
|
||||
|
||||
@@ -83,11 +83,13 @@ export class FilterableDropdownSelectionModel {
|
||||
return -1
|
||||
} else if (
|
||||
this._documentCounts.length &&
|
||||
this.getDocumentCount(b.id) === 0 &&
|
||||
this.getDocumentCount(a.id) > this.getDocumentCount(b.id)
|
||||
) {
|
||||
return -1
|
||||
} else if (
|
||||
this._documentCounts.length &&
|
||||
this.getDocumentCount(a.id) === 0 &&
|
||||
this.getDocumentCount(a.id) < this.getDocumentCount(b.id)
|
||||
) {
|
||||
return 1
|
||||
|
||||
+6
-1
@@ -1,3 +1,5 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { TagComponent } from '../../tag/tag.component'
|
||||
@@ -12,7 +14,10 @@ describe('ToggleableDropdownButtonComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
imports: [ToggleableDropdownButtonComponent, TagComponent],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@@ -14,7 +18,7 @@ import { AbstractInputComponent } from '../abstract-input'
|
||||
selector: 'pngx-input-entries',
|
||||
templateUrl: './entries.component.html',
|
||||
styleUrl: './entries.component.scss',
|
||||
imports: [NgxBootstrapIconsModule],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
|
||||
})
|
||||
export class EntriesComponent extends AbstractInputComponent<object> {
|
||||
entries = []
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
(change)="onChange(value)">
|
||||
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
<button class="tag-wrap btn p-0" (click)="removeTag($event, item.id)" title="Remove tag" i18n-title>
|
||||
<button class="tag-wrap btn p-0 d-flex align-items-center" (click)="removeTag($event, item.id)" title="Remove tag" i18n-title>
|
||||
<i-bs name="x" style="margin-inline-end: 1px;"></i-bs>
|
||||
@if (item.id && tags) {
|
||||
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
[original-size]="false"
|
||||
[show-borders]="false"
|
||||
[show-all]="true"
|
||||
(error)="onError($event)">
|
||||
(text-layer-rendered)="onPageRendered()"
|
||||
(error)="onError($event)" #pdfViewer>
|
||||
</pdf-viewer>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,4 +158,24 @@ describe('PreviewPopupComponent', () => {
|
||||
jest.advanceTimersByTime(1)
|
||||
expect(component.popover.isOpen()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should dispatch find event on viewer loaded if searchQuery set', () => {
|
||||
documentService.searchQuery = 'test'
|
||||
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
|
||||
component.popover.open()
|
||||
jest.advanceTimersByTime(1000)
|
||||
fixture.detectChanges()
|
||||
// normally setup by pdf-viewer
|
||||
jest.replaceProperty(component.pdfViewer, 'eventBus', {
|
||||
dispatch: jest.fn(),
|
||||
} as any)
|
||||
const dispatchSpy = jest.spyOn(component.pdfViewer.eventBus, 'dispatch')
|
||||
component.onPageRendered()
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('find', {
|
||||
query: 'test',
|
||||
caseSensitive: false,
|
||||
highlightAll: true,
|
||||
phraseSearch: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Component, Input, OnDestroy, ViewChild } from '@angular/core'
|
||||
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { PdfViewerComponent, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { first, Subject, takeUntil } from 'rxjs'
|
||||
import { Document } from 'src/app/data/document'
|
||||
@@ -57,6 +57,8 @@ export class PreviewPopupComponent implements OnDestroy {
|
||||
|
||||
@ViewChild('popover') popover: NgbPopover
|
||||
|
||||
@ViewChild('pdfViewer') pdfViewer: PdfViewerComponent
|
||||
|
||||
mouseOnPreview: boolean = false
|
||||
|
||||
popoverClass: string = 'shadow popover-preview'
|
||||
@@ -114,6 +116,18 @@ export class PreviewPopupComponent implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
onPageRendered() {
|
||||
// Only triggered by the pngx pdf viewer
|
||||
if (this.documentService.searchQuery) {
|
||||
this.pdfViewer.eventBus.dispatch('find', {
|
||||
query: this.documentService.searchQuery,
|
||||
caseSensitive: false,
|
||||
highlightAll: true,
|
||||
phraseSearch: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get previewUrl() {
|
||||
return this.documentService.getPreviewUrl(this.document.id)
|
||||
}
|
||||
|
||||
-1
@@ -48,7 +48,6 @@
|
||||
i18n-title
|
||||
buttonClasses=" btn-outline-secondary"
|
||||
iconName="arrow-repeat"
|
||||
[disabled]="!hasUsablePassword"
|
||||
(confirm)="generateAuthToken()">
|
||||
</pngx-confirm-button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@if (tag !== undefined) {
|
||||
@if (tag) {
|
||||
@if (!clickable) {
|
||||
<span class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
a {
|
||||
a, span {
|
||||
cursor: pointer;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
text-align: end;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.private {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { of } from 'rxjs'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { TagComponent } from './tag.component'
|
||||
|
||||
const tag: Tag = {
|
||||
@@ -12,13 +17,20 @@ const tag: Tag = {
|
||||
describe('TagComponent', () => {
|
||||
let component: TagComponent
|
||||
let fixture: ComponentFixture<TagComponent>
|
||||
let permissionsService: PermissionsService
|
||||
let tagService: TagService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
imports: [TagComponent],
|
||||
}).compileComponents()
|
||||
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
tagService = TestBed.inject(TagService)
|
||||
fixture = TestBed.createComponent(TagComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
@@ -47,4 +59,13 @@ describe('TagComponent', () => {
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('a.badge'))).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should support retrieving tag by ID', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
const getCachedSpy = jest.spyOn(tagService, 'getCached')
|
||||
getCachedSpy.mockReturnValue(of(tag))
|
||||
component.tagID = 1
|
||||
expect(getCachedSpy).toHaveBeenCalledWith(1)
|
||||
expect(component.tag).toEqual(tag)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-tag',
|
||||
@@ -7,10 +13,39 @@ import { Tag } from 'src/app/data/tag'
|
||||
styleUrls: ['./tag.component.scss'],
|
||||
})
|
||||
export class TagComponent {
|
||||
constructor() {}
|
||||
private _tag: Tag
|
||||
private _tagID: number
|
||||
|
||||
constructor(
|
||||
private permissionsService: PermissionsService,
|
||||
private tagService: TagService
|
||||
) {}
|
||||
|
||||
@Input()
|
||||
tag: Tag
|
||||
public set tag(tag: Tag) {
|
||||
this._tag = tag
|
||||
}
|
||||
|
||||
public get tag(): Tag {
|
||||
return this._tag
|
||||
}
|
||||
|
||||
@Input()
|
||||
set tagID(tagID: number) {
|
||||
if (tagID !== this._tagID) {
|
||||
this._tagID = tagID
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.Tag
|
||||
)
|
||||
) {
|
||||
this.tagService.getCached(this._tagID).subscribe((tag) => {
|
||||
this.tag = tag
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
linkTitle: string = ''
|
||||
|
||||
+28
-5
@@ -50,22 +50,45 @@
|
||||
}
|
||||
@case (DisplayField.CORRESPONDENT) {
|
||||
@if (doc.correspondent) {
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickCorrespondent(doc.correspondent, $event)" title="Filter by correspondent" i18n-title>{{(doc.correspondent$ | async)?.name}}</a>
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickCorrespondent(doc.correspondent, $event)" title="Filter by correspondent" i18n-title>{{doc.correspondent | correspondentName | async}}</a>
|
||||
}
|
||||
}
|
||||
@case (DisplayField.TAGS) {
|
||||
@for (t of doc.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t.id, $event)" [clickable]="true" linkTitle="Filter by tag" i18n-title></pngx-tag>
|
||||
@for (tagID of doc.tags; track tagID) {
|
||||
<pngx-tag [tagID]="tagID" class="ms-1" (click)="clickTag(tagID, $event)" [clickable]="true" linkTitle="Filter by tag" i18n-title></pngx-tag>
|
||||
}
|
||||
}
|
||||
@case (DisplayField.DOCUMENT_TYPE) {
|
||||
@if (doc.document_type) {
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickDocType(doc.document_type, $event)" title="Filter by document type" i18n-title>{{(doc.document_type$ | async)?.name}}</a>
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickDocType(doc.document_type, $event)" title="Filter by document type" i18n-title>{{doc.document_type | documentTypeName | async}}</a>
|
||||
}
|
||||
}
|
||||
@case (DisplayField.STORAGE_PATH) {
|
||||
@if (doc.storage_path) {
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickStoragePath(doc.storage_path, $event)" title="Filter by storage path" i18n-title>{{(doc.storage_path$ | async)?.name}}</a>
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickStoragePath(doc.storage_path, $event)" title="Filter by storage path" i18n-title>{{doc.storage_path | storagePathName | async}}</a>
|
||||
}
|
||||
}
|
||||
@case (DisplayField.OWNER) {
|
||||
@if (doc.owner) {
|
||||
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickOwner(doc.owner, $event)" title="Filter by owner" i18n-title>{{doc.owner | username | async}}</a>
|
||||
}
|
||||
}
|
||||
@case (DisplayField.ASN) {
|
||||
{{doc.archive_serial_number}}
|
||||
}
|
||||
@case (DisplayField.PAGE_COUNT) {
|
||||
{{ doc.page_count }}
|
||||
}
|
||||
@case (DisplayField.SHARED) {
|
||||
@if (doc.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }
|
||||
}
|
||||
@case (DisplayField.NOTES) {
|
||||
@if (doc.notes.length) {
|
||||
<a routerLink="/documents/{{doc.id}}/notes" class="btn btn-sm p-0">
|
||||
<span class="badge rounded-pill bg-light border text-primary">
|
||||
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
|
||||
{{doc.notes.length}}</span>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-7
@@ -24,6 +24,7 @@ import {
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_FULLTEXT_MORELIKE,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
FILTER_OWNER_ANY,
|
||||
FILTER_STORAGE_PATH,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
@@ -32,14 +33,14 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import {
|
||||
FileStatus,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { SavedViewWidgetComponent } from './saved-view-widget.component'
|
||||
|
||||
@@ -111,7 +112,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
let component: SavedViewWidgetComponent
|
||||
let fixture: ComponentFixture<SavedViewWidgetComponent>
|
||||
let documentService: DocumentService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let documentListViewService: DocumentListViewService
|
||||
let router: Router
|
||||
|
||||
@@ -175,7 +176,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
router = TestBed.inject(Router)
|
||||
fixture = TestBed.createComponent(SavedViewWidgetComponent)
|
||||
@@ -234,7 +235,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
it('should reload on document consumption finished', () => {
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
const reloadSpy = jest.spyOn(component, 'reload')
|
||||
component.ngOnInit()
|
||||
@@ -295,6 +296,15 @@ describe('SavedViewWidgetComponent', () => {
|
||||
component.clickStoragePath(11) // coverage
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click owner', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickOwner(11, new MouseEvent('click'))
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_OWNER_ANY, value: '11' },
|
||||
])
|
||||
component.clickOwner(11) // coverage
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click more like', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickMoreLike(11)
|
||||
|
||||
+21
-3
@@ -31,13 +31,17 @@ import {
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_FULLTEXT_MORELIKE,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
FILTER_OWNER_ANY,
|
||||
FILTER_STORAGE_PATH,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe'
|
||||
import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import {
|
||||
@@ -48,6 +52,7 @@ import {
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
|
||||
@Component({
|
||||
@@ -62,6 +67,10 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
TagComponent,
|
||||
WidgetFrameComponent,
|
||||
IfPermissionsDirective,
|
||||
UsernamePipe,
|
||||
CorrespondentNamePipe,
|
||||
DocumentTypeNamePipe,
|
||||
StoragePathNamePipe,
|
||||
AsyncPipe,
|
||||
DocumentTitlePipe,
|
||||
CustomDatePipe,
|
||||
@@ -85,7 +94,7 @@ export class SavedViewWidgetComponent
|
||||
private documentService: DocumentService,
|
||||
private router: Router,
|
||||
private list: DocumentListViewService,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
public documentListViewService: DocumentListViewService,
|
||||
public permissionsService: PermissionsService,
|
||||
@@ -115,7 +124,7 @@ export class SavedViewWidgetComponent
|
||||
ngOnInit(): void {
|
||||
this.reload()
|
||||
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
|
||||
this.consumerStatusService
|
||||
this.websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
@@ -229,6 +238,15 @@ export class SavedViewWidgetComponent
|
||||
])
|
||||
}
|
||||
|
||||
clickOwner(ownerID: number, event: MouseEvent = null) {
|
||||
event?.preventDefault()
|
||||
event?.stopImmediatePropagation()
|
||||
|
||||
this.list.quickFilter([
|
||||
{ rule_type: FILTER_OWNER_ANY, value: ownerID.toString() },
|
||||
])
|
||||
}
|
||||
|
||||
openDocumentDetail(document: Document) {
|
||||
this.router.navigate(['documents', document.id])
|
||||
}
|
||||
|
||||
+5
-5
@@ -12,9 +12,9 @@ import { routes } from 'src/app/app-routing.module'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { StatisticsWidgetComponent } from './statistics-widget.component'
|
||||
@@ -23,7 +23,7 @@ describe('StatisticsWidgetComponent', () => {
|
||||
let component: StatisticsWidgetComponent
|
||||
let fixture: ComponentFixture<StatisticsWidgetComponent>
|
||||
let httpTestingController: HttpTestingController
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -44,9 +44,9 @@ describe('StatisticsWidgetComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(StatisticsWidgetComponent)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component = fixture.componentInstance
|
||||
|
||||
|
||||
+3
-3
@@ -8,8 +8,8 @@ import { first, Subject, Subscription, takeUntil } from 'rxjs'
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
import { FILTER_HAS_TAGS_ANY } from 'src/app/data/filter-rule-type'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
|
||||
@@ -51,7 +51,7 @@ export class StatisticsWidgetComponent
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketConnectionService: WebsocketStatusService,
|
||||
private documentListViewService: DocumentListViewService
|
||||
) {
|
||||
super()
|
||||
@@ -109,7 +109,7 @@ export class StatisticsWidgetComponent
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reload()
|
||||
this.subscription = this.consumerStatusService
|
||||
this.subscription = this.websocketConnectionService
|
||||
.onDocumentConsumptionFinished()
|
||||
.subscribe(() => {
|
||||
this.reload()
|
||||
|
||||
+13
-13
@@ -12,13 +12,13 @@ import { NgbAlert, NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
|
||||
import {
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { UploadFileWidgetComponent } from './upload-file-widget.component'
|
||||
|
||||
const FAILED_STATUSES = [new FileStatus()]
|
||||
@@ -42,7 +42,7 @@ const DEFAULT_STATUSES = [
|
||||
describe('UploadFileWidgetComponent', () => {
|
||||
let component: UploadFileWidgetComponent
|
||||
let fixture: ComponentFixture<UploadFileWidgetComponent>
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let uploadDocumentsService: UploadDocumentsService
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -65,7 +65,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
uploadDocumentsService = TestBed.inject(UploadDocumentsService)
|
||||
fixture = TestBed.createComponent(UploadFileWidgetComponent)
|
||||
component = fixture.componentInstance
|
||||
@@ -91,14 +91,14 @@ describe('UploadFileWidgetComponent', () => {
|
||||
})
|
||||
|
||||
it('should generate stats summary', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
mockConsumerStatuses(websocketStatusService)
|
||||
expect(component.getStatusSummary()).toEqual(
|
||||
'Processing: 6, Failed: 1, Added: 4'
|
||||
)
|
||||
})
|
||||
|
||||
it('should report an upload progress summary', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
mockConsumerStatuses(websocketStatusService)
|
||||
expect(component.getTotalUploadProgress()).toEqual(0.75)
|
||||
})
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
})
|
||||
|
||||
it('should enforce a maximum number of alerts', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
mockConsumerStatuses(websocketStatusService)
|
||||
fixture.detectChanges()
|
||||
// 5 total, 1 hidden
|
||||
expect(fixture.debugElement.queryAll(By.directive(NgbAlert))).toHaveLength(
|
||||
@@ -131,19 +131,19 @@ describe('UploadFileWidgetComponent', () => {
|
||||
})
|
||||
|
||||
it('should allow dismissing an alert', () => {
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
|
||||
const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss')
|
||||
component.dismiss(new FileStatus())
|
||||
expect(dismissSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow dismissing completed alerts', fakeAsync(() => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
mockConsumerStatuses(websocketStatusService)
|
||||
component.alertsExpanded = true
|
||||
fixture.detectChanges()
|
||||
jest
|
||||
.spyOn(component, 'getStatusCompleted')
|
||||
.mockImplementation(() => SUCCESS_STATUSES)
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
|
||||
const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss')
|
||||
component.dismissCompleted()
|
||||
tick(1000)
|
||||
fixture.detectChanges()
|
||||
|
||||
+19
-16
@@ -12,13 +12,13 @@ import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
|
||||
import {
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
|
||||
const MAX_ALERTS = 5
|
||||
@@ -46,7 +46,7 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
@ViewChildren(NgbAlert) alerts: QueryList<NgbAlert>
|
||||
|
||||
constructor(
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
private uploadDocumentsService: UploadDocumentsService,
|
||||
public settingsService: SettingsService
|
||||
) {
|
||||
@@ -54,13 +54,13 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.consumerStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
|
||||
return this.websocketStatusService.getConsumerStatus().slice(0, MAX_ALERTS)
|
||||
}
|
||||
|
||||
getStatusSummary() {
|
||||
let strings = []
|
||||
let countUploadingAndProcessing =
|
||||
this.consumerStatusService.getConsumerStatusNotCompleted().length
|
||||
this.websocketStatusService.getConsumerStatusNotCompleted().length
|
||||
let countFailed = this.getStatusFailed().length
|
||||
let countSuccess = this.getStatusSuccess().length
|
||||
if (countUploadingAndProcessing > 0) {
|
||||
@@ -78,27 +78,30 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
getStatusHidden() {
|
||||
if (this.consumerStatusService.getConsumerStatus().length < MAX_ALERTS)
|
||||
if (this.websocketStatusService.getConsumerStatus().length < MAX_ALERTS)
|
||||
return []
|
||||
else return this.consumerStatusService.getConsumerStatus().slice(MAX_ALERTS)
|
||||
else
|
||||
return this.websocketStatusService.getConsumerStatus().slice(MAX_ALERTS)
|
||||
}
|
||||
|
||||
getStatusUploading() {
|
||||
return this.consumerStatusService.getConsumerStatus(
|
||||
return this.websocketStatusService.getConsumerStatus(
|
||||
FileStatusPhase.UPLOADING
|
||||
)
|
||||
}
|
||||
|
||||
getStatusFailed() {
|
||||
return this.consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
return this.websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED)
|
||||
}
|
||||
|
||||
getStatusSuccess() {
|
||||
return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS)
|
||||
return this.websocketStatusService.getConsumerStatus(
|
||||
FileStatusPhase.SUCCESS
|
||||
)
|
||||
}
|
||||
|
||||
getStatusCompleted() {
|
||||
return this.consumerStatusService.getConsumerStatusCompleted()
|
||||
return this.websocketStatusService.getConsumerStatusCompleted()
|
||||
}
|
||||
|
||||
getTotalUploadProgress() {
|
||||
@@ -134,12 +137,12 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
dismiss(status: FileStatus) {
|
||||
this.consumerStatusService.dismiss(status)
|
||||
this.websocketStatusService.dismiss(status)
|
||||
}
|
||||
|
||||
dismissCompleted() {
|
||||
this.getStatusCompleted().forEach((status) =>
|
||||
this.consumerStatusService.dismiss(status)
|
||||
this.websocketStatusService.dismiss(status)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
}
|
||||
<div class="input-group input-group-sm me-md-5 d-none d-md-flex">
|
||||
<button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button>
|
||||
<select class="form-select" (change)="onZoomSelect($event)">
|
||||
<select class="form-select" (change)="setZoom($event.target.value)">
|
||||
@for (setting of zoomSettings; track setting) {
|
||||
<option [value]="setting" [selected]="previewZoomSetting === setting">
|
||||
<option [value]="setting" [attr.selected]="isZoomSelected(setting) ? 'selected' : null">
|
||||
{{ getZoomSettingTitle(setting) }}
|
||||
</option>
|
||||
}
|
||||
@@ -25,15 +25,20 @@
|
||||
</button>
|
||||
|
||||
<div class="btn-group">
|
||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||
<i-bs width="1.2em" height="1.2em" name="download"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||
</a>
|
||||
<button (click)="download()" class="btn btn-sm btn-outline-primary" [disabled]="downloading">
|
||||
@if (downloading) {
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
} @else {
|
||||
<i-bs width="1.2em" height="1.2em" name="download"></i-bs>
|
||||
}
|
||||
<span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||
</button>
|
||||
|
||||
@if (metadata?.has_archive_version) {
|
||||
<div class="btn-group" ngbDropdown role="group">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle></button>
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" [disabled]="downloading" ngbDropdownToggle></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
|
||||
<button ngbDropdownItem (click)="download(true)" [disabled]="downloading" i18n>Download original</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -351,9 +356,9 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #previewContent>
|
||||
<div class="thumb-preview position-absolute pe-none" [class.fade]="previewLoaded">
|
||||
<div class="thumb-preview position-absolute pe-none text-center" [class.fade]="previewLoaded">
|
||||
@if (showThumbnailOverlay) {
|
||||
<img [src]="thumbUrl | safeUrl" class="" width="100%" height="auto" alt="Document loading..." i18n-alt />
|
||||
<img [src]="thumbUrl | safeUrl" class="mx-auto" [attr.width]="previewZoomScale === 'page-fit' ? 'auto' : '100%'" [attr.height]="previewZoomScale === 'page-fit' ? '100%' : 'auto'" alt="Document loading..." i18n-alt />
|
||||
}
|
||||
<div class="position-absolute top-0 start-0 m-2 p-2 d-flex align-items-center justify-content-center">
|
||||
<div>
|
||||
|
||||
@@ -85,5 +85,8 @@ textarea.rtl {
|
||||
|
||||
> img {
|
||||
filter: blur(1px);
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
object-position: top;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
@@ -45,6 +46,7 @@ import { Tag } from 'src/app/data/tag'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { ComponentRouterService } from 'src/app/services/component-router.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
@@ -60,7 +62,10 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||
import { DocumentDetailComponent } from './document-detail.component'
|
||||
import {
|
||||
DocumentDetailComponent,
|
||||
ZoomSetting,
|
||||
} from './document-detail.component'
|
||||
|
||||
const doc: Document = {
|
||||
id: 3,
|
||||
@@ -126,7 +131,9 @@ describe('DocumentDetailComponent', () => {
|
||||
let documentListViewService: DocumentListViewService
|
||||
let settingsService: SettingsService
|
||||
let customFieldsService: CustomFieldsService
|
||||
let deviceDetectorService: DeviceDetectorService
|
||||
let httpTestingController: HttpTestingController
|
||||
let componentRouterService: ComponentRouterService
|
||||
|
||||
let currentUserCan = true
|
||||
let currentUserHasObjectPermissions = true
|
||||
@@ -262,8 +269,10 @@ describe('DocumentDetailComponent', () => {
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 1 }
|
||||
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||
deviceDetectorService = TestBed.inject(DeviceDetectorService)
|
||||
fixture = TestBed.createComponent(DocumentDetailComponent)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
componentRouterService = TestBed.inject(ComponentRouterService)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
@@ -448,7 +457,9 @@ describe('DocumentDetailComponent', () => {
|
||||
component.save(true)
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Document "Doc 3" saved successfully.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should support save without close and show success toast', () => {
|
||||
@@ -461,7 +472,9 @@ describe('DocumentDetailComponent', () => {
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Document "Doc 3" saved successfully.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should show toast error on save if error occurs', () => {
|
||||
@@ -476,7 +489,10 @@ describe('DocumentDetailComponent', () => {
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Error saving document', error)
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Error saving document "Doc 3"',
|
||||
error
|
||||
)
|
||||
})
|
||||
|
||||
it('should show error toast on save but close if user can no longer edit', () => {
|
||||
@@ -492,7 +508,9 @@ describe('DocumentDetailComponent', () => {
|
||||
component.save(true)
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Document "Doc 3" saved successfully.'
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow save and next', () => {
|
||||
@@ -568,6 +586,16 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['documents'])
|
||||
})
|
||||
|
||||
it('should allow close and navigate to the last view if available', () => {
|
||||
initNormally()
|
||||
jest
|
||||
.spyOn(componentRouterService, 'getComponentURLBefore')
|
||||
.mockReturnValue('dashboard')
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.close()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['dashboard'])
|
||||
})
|
||||
|
||||
it('should allow close and navigate to documents by default', () => {
|
||||
initNormally()
|
||||
jest
|
||||
@@ -728,7 +756,7 @@ describe('DocumentDetailComponent', () => {
|
||||
|
||||
it('should support zoom controls', () => {
|
||||
initNormally()
|
||||
component.onZoomSelect({ target: { value: '1' } } as any) // from select
|
||||
component.setZoom(ZoomSetting.One) // from select
|
||||
expect(component.previewZoomSetting).toEqual('1')
|
||||
component.increaseZoom()
|
||||
expect(component.previewZoomSetting).toEqual('1.5')
|
||||
@@ -736,18 +764,18 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(component.previewZoomSetting).toEqual('2')
|
||||
component.decreaseZoom()
|
||||
expect(component.previewZoomSetting).toEqual('1.5')
|
||||
component.onZoomSelect({ target: { value: '1' } } as any) // from select
|
||||
component.setZoom(ZoomSetting.One) // from select
|
||||
component.decreaseZoom()
|
||||
expect(component.previewZoomSetting).toEqual('.75')
|
||||
|
||||
component.onZoomSelect({ target: { value: 'page-fit' } } as any) // from select
|
||||
component.setZoom(ZoomSetting.PageFit) // from select
|
||||
expect(component.previewZoomScale).toEqual('page-fit')
|
||||
expect(component.previewZoomSetting).toEqual('1')
|
||||
component.increaseZoom()
|
||||
expect(component.previewZoomSetting).toEqual('1.5')
|
||||
expect(component.previewZoomScale).toEqual('page-width')
|
||||
|
||||
component.onZoomSelect({ target: { value: 'page-fit' } } as any) // from select
|
||||
component.setZoom(ZoomSetting.PageFit) // from select
|
||||
expect(component.previewZoomScale).toEqual('page-fit')
|
||||
expect(component.previewZoomSetting).toEqual('1')
|
||||
component.decreaseZoom()
|
||||
@@ -755,6 +783,19 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(component.previewZoomScale).toEqual('page-width')
|
||||
})
|
||||
|
||||
it('should select correct zoom setting in dropdown', () => {
|
||||
initNormally()
|
||||
component.setZoom(ZoomSetting.PageFit)
|
||||
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeTruthy()
|
||||
expect(component.isZoomSelected(ZoomSetting.One)).toBeFalsy()
|
||||
component.setZoom(ZoomSetting.PageWidth)
|
||||
expect(component.isZoomSelected(ZoomSetting.One)).toBeTruthy()
|
||||
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeFalsy()
|
||||
component.setZoom(ZoomSetting.Quarter)
|
||||
expect(component.isZoomSelected(ZoomSetting.Quarter)).toBeTruthy()
|
||||
expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support updating notes dynamically', () => {
|
||||
const notes = [
|
||||
{
|
||||
@@ -1255,4 +1296,38 @@ describe('DocumentDetailComponent', () => {
|
||||
.error(new ErrorEvent('failed'))
|
||||
expect(component.tiffError).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should support download using share sheet on mobile, direct download otherwise', () => {
|
||||
const shareSpy = jest.spyOn(navigator, 'share')
|
||||
const createSpy = jest.spyOn(document, 'createElement')
|
||||
const urlRevokeSpy = jest.spyOn(URL, 'revokeObjectURL')
|
||||
initNormally()
|
||||
|
||||
// Mobile
|
||||
jest.spyOn(deviceDetectorService, 'isDesktop').mockReturnValue(false)
|
||||
component.download()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/${doc.id}/download/`)
|
||||
.error(new ProgressEvent('failed'))
|
||||
expect(shareSpy).not.toHaveBeenCalled()
|
||||
|
||||
component.download(true)
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}documents/${doc.id}/download/?original=true`
|
||||
)
|
||||
.flush(new ArrayBuffer(100))
|
||||
expect(shareSpy).toHaveBeenCalled()
|
||||
|
||||
// Desktop
|
||||
shareSpy.mockClear()
|
||||
jest.spyOn(deviceDetectorService, 'isDesktop').mockReturnValue(true)
|
||||
component.download()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/${doc.id}/download/`)
|
||||
.flush(new ArrayBuffer(100))
|
||||
expect(shareSpy).not.toHaveBeenCalled()
|
||||
expect(createSpy).toHaveBeenCalledWith('a')
|
||||
expect(urlRevokeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs'
|
||||
import {
|
||||
debounceTime,
|
||||
@@ -59,6 +60,7 @@ import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
|
||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import { ComponentRouterService } from 'src/app/services/component-router.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -122,7 +124,7 @@ enum ContentRenderType {
|
||||
TIFF = 'tiff',
|
||||
}
|
||||
|
||||
enum ZoomSetting {
|
||||
export enum ZoomSetting {
|
||||
PageFit = 'page-fit',
|
||||
PageWidth = 'page-width',
|
||||
Quarter = '.25',
|
||||
@@ -194,8 +196,6 @@ export class DocumentDetailComponent
|
||||
previewUrl: string
|
||||
thumbUrl: string
|
||||
previewText: string
|
||||
downloadUrl: string
|
||||
downloadOriginalUrl: string
|
||||
previewLoaded: boolean = false
|
||||
tiffURL: string
|
||||
tiffError: string
|
||||
@@ -233,6 +233,9 @@ export class DocumentDetailComponent
|
||||
ogDate: Date
|
||||
|
||||
customFields: CustomField[]
|
||||
|
||||
public downloading: boolean = false
|
||||
|
||||
public readonly CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
public readonly ContentRenderType = ContentRenderType
|
||||
@@ -272,7 +275,9 @@ export class DocumentDetailComponent
|
||||
private userService: UserService,
|
||||
private customFieldsService: CustomFieldsService,
|
||||
private http: HttpClient,
|
||||
private hotKeyService: HotKeyService
|
||||
private hotKeyService: HotKeyService,
|
||||
private componentRouterService: ComponentRouterService,
|
||||
private deviceDetectorService: DeviceDetectorService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -323,6 +328,7 @@ export class DocumentDetailComponent
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING))
|
||||
this.documentForm.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
@@ -415,13 +421,6 @@ export class DocumentDetailComponent
|
||||
.pipe(
|
||||
switchMap((doc) => {
|
||||
this.documentId = doc.id
|
||||
this.downloadUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId
|
||||
)
|
||||
this.downloadOriginalUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId,
|
||||
true
|
||||
)
|
||||
this.suggestions = null
|
||||
const openDocument = this.openDocumentService.getOpenDocument(
|
||||
this.documentId
|
||||
@@ -620,7 +619,10 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.metadata = result
|
||||
if (this.archiveContentRenderType !== ContentRenderType.PDF) {
|
||||
if (
|
||||
this.archiveContentRenderType !== ContentRenderType.PDF ||
|
||||
this.useNativePdfViewer
|
||||
) {
|
||||
this.previewLoaded = true
|
||||
}
|
||||
},
|
||||
@@ -807,7 +809,9 @@ export class DocumentDetailComponent
|
||||
this.store.next(newValues)
|
||||
this.openDocumentService.setDirty(this.document, false)
|
||||
this.openDocumentService.save()
|
||||
this.toastService.showInfo($localize`Document saved successfully.`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document "${newValues.title}" saved successfully.`
|
||||
)
|
||||
this.networkActive = false
|
||||
this.error = null
|
||||
if (close) {
|
||||
@@ -821,11 +825,16 @@ export class DocumentDetailComponent
|
||||
error: (error) => {
|
||||
this.networkActive = false
|
||||
if (!this.userCanEdit) {
|
||||
this.toastService.showInfo($localize`Document saved successfully.`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document "${this.document.title}" saved successfully.`
|
||||
)
|
||||
close && this.close()
|
||||
} else {
|
||||
this.error = error.error
|
||||
this.toastService.showError($localize`Error saving document`, error)
|
||||
this.toastService.showError(
|
||||
$localize`Error saving document "${this.document.title}"`,
|
||||
error
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -845,12 +854,14 @@ export class DocumentDetailComponent
|
||||
)
|
||||
.pipe(
|
||||
switchMap(({ nextDocId, updateResult }) => {
|
||||
if (nextDocId && updateResult)
|
||||
if (nextDocId && updateResult) {
|
||||
this.openDocumentService.setDirty(this.document, false)
|
||||
return this.openDocumentService
|
||||
.closeDocument(this.document)
|
||||
.pipe(
|
||||
map((closeResult) => ({ updateResult, nextDocId, closeResult }))
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(first())
|
||||
@@ -883,6 +894,10 @@ export class DocumentDetailComponent
|
||||
'view',
|
||||
this.documentListViewService.activeSavedViewId,
|
||||
])
|
||||
} else if (this.componentRouterService.getComponentURLBefore()) {
|
||||
this.router.navigate([
|
||||
this.componentRouterService.getComponentURLBefore(),
|
||||
])
|
||||
} else {
|
||||
this.router.navigate(['documents'])
|
||||
}
|
||||
@@ -948,7 +963,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Reprocess operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.`
|
||||
$localize`Reprocess operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.`
|
||||
)
|
||||
if (modal) {
|
||||
modal.close()
|
||||
@@ -967,6 +982,52 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
download(original: boolean = false) {
|
||||
this.downloading = true
|
||||
const downloadUrl = this.documentsService.getDownloadUrl(
|
||||
this.documentId,
|
||||
original
|
||||
)
|
||||
this.http.get(downloadUrl, { responseType: 'blob' }).subscribe({
|
||||
next: (blob) => {
|
||||
this.downloading = false
|
||||
const blobParts = [blob]
|
||||
const file = new File(
|
||||
blobParts,
|
||||
original
|
||||
? this.document.original_file_name
|
||||
: this.document.archived_file_name,
|
||||
{
|
||||
type: original ? this.document.mime_type : 'application/pdf',
|
||||
}
|
||||
)
|
||||
if (
|
||||
!this.deviceDetectorService.isDesktop() &&
|
||||
navigator.canShare &&
|
||||
navigator.canShare({ files: [file] })
|
||||
) {
|
||||
navigator.share({
|
||||
files: [file],
|
||||
})
|
||||
} else {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = this.document.title
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.downloading = false
|
||||
this.toastService.showError(
|
||||
$localize`Error downloading document`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return this.documentListViewService.hasNext(this.documentId)
|
||||
}
|
||||
@@ -1012,14 +1073,13 @@ export class DocumentDetailComponent
|
||||
}
|
||||
}
|
||||
|
||||
onZoomSelect(event: Event) {
|
||||
const setting = (event.target as HTMLSelectElement)?.value as ZoomSetting
|
||||
if (ZoomSetting.PageFit === setting) {
|
||||
this.previewZoomSetting = ZoomSetting.One
|
||||
setZoom(setting: ZoomSetting) {
|
||||
if (ZoomSetting.PageFit === setting || ZoomSetting.PageWidth === setting) {
|
||||
this.previewZoomScale = setting
|
||||
this.previewZoomSetting = ZoomSetting.One
|
||||
} else {
|
||||
this.previewZoomScale = ZoomSetting.PageWidth
|
||||
this.previewZoomSetting = setting
|
||||
this.previewZoomScale = ZoomSetting.PageWidth
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1029,6 +1089,14 @@ export class DocumentDetailComponent
|
||||
)
|
||||
}
|
||||
|
||||
isZoomSelected(setting: ZoomSetting): boolean {
|
||||
if (this.previewZoomScale === ZoomSetting.PageFit) {
|
||||
return setting === ZoomSetting.PageFit
|
||||
}
|
||||
|
||||
return this.previewZoomSetting === setting
|
||||
}
|
||||
|
||||
getZoomSettingTitle(setting: ZoomSetting): string {
|
||||
switch (setting) {
|
||||
case ZoomSetting.PageFit:
|
||||
@@ -1262,7 +1330,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Split operation will begin in the background.`
|
||||
$localize`Split operation for "${this.document.title}" will begin in the background.`
|
||||
)
|
||||
modal.close()
|
||||
},
|
||||
@@ -1301,7 +1369,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.show({
|
||||
content: $localize`Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes.`,
|
||||
content: $localize`Rotation of "${this.document.title}" will begin in the background. Close and re-open the document after the operation has completed to see the changes.`,
|
||||
delay: 8000,
|
||||
action: this.close.bind(this),
|
||||
actionName: $localize`Close`,
|
||||
@@ -1341,7 +1409,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Delete pages operation will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.`
|
||||
$localize`Delete pages operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.`
|
||||
)
|
||||
modal.close()
|
||||
},
|
||||
|
||||
@@ -1039,6 +1039,7 @@ describe('BulkEditorComponent', () => {
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
expect(documentListViewService.selected.size).toEqual(0)
|
||||
})
|
||||
|
||||
it('should support bulk download with archive, originals or both and file formatting', () => {
|
||||
|
||||
@@ -268,6 +268,9 @@ export class BulkEditorComponent
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: () => {
|
||||
if (args['delete_originals']) {
|
||||
this.list.selected.clear()
|
||||
}
|
||||
this.list.reload()
|
||||
this.list.reduceSelectionToFilter()
|
||||
this.list.selected.forEach((id) => {
|
||||
|
||||
+7
-7
@@ -22,9 +22,9 @@
|
||||
@if (document) {
|
||||
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||
@if (clickCorrespondent.observers.length ) {
|
||||
<a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
|
||||
<a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{document.correspondent | correspondentName | async}}</a>
|
||||
} @else {
|
||||
{{(document.correspondent$ | async)?.name}}
|
||||
{{document.correspondent | correspondentName | async}}
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.TITLE)) {:}
|
||||
}
|
||||
@@ -32,8 +32,8 @@
|
||||
{{document.title | documentTitle}}
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.TAGS)) {
|
||||
@for (t of document.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
|
||||
@for (tagID of document.tags; track tagID) {
|
||||
<pngx-tag [tagID]="tagID" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(tagID);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
@@ -95,13 +95,13 @@
|
||||
@if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
|
||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by document type" i18n-title
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{(document.document_type$ | async)?.name}}</small>
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{document.document_type | documentTypeName | async}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
|
||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by storage path" i18n-title
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{document.storage_path | storagePathName | async}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
|
||||
@@ -136,7 +136,7 @@
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
|
||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username}}</small>
|
||||
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username | async}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
|
||||
|
||||
+12
-3
@@ -13,6 +13,7 @@ import {
|
||||
NgbTooltipModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { delay, of } from 'rxjs'
|
||||
import {
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
@@ -20,9 +21,12 @@ import {
|
||||
} from 'src/app/data/document'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe'
|
||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||
import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
@@ -43,6 +47,9 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
||||
CustomFieldDisplayComponent,
|
||||
AsyncPipe,
|
||||
UsernamePipe,
|
||||
CorrespondentNamePipe,
|
||||
DocumentTypeNamePipe,
|
||||
StoragePathNamePipe,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
RouterModule,
|
||||
@@ -104,9 +111,11 @@ export class DocumentCardLargeComponent
|
||||
popoverHidden = true
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
setInterval(() => {
|
||||
this.show = true
|
||||
}, 100)
|
||||
of(true)
|
||||
.pipe(delay(50))
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
})
|
||||
}
|
||||
|
||||
get searchScoreClass() {
|
||||
|
||||
+6
-6
@@ -16,8 +16,8 @@
|
||||
|
||||
@if (document && displayFields?.includes(DisplayField.TAGS)) {
|
||||
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
||||
@for (t of getTagsLimited$() | async; track t) {
|
||||
<pngx-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
|
||||
@for (tagID of tagIDs; track tagID) {
|
||||
<pngx-tag [tagID]="tagID" (click)="clickTag.emit(tagID);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
|
||||
}
|
||||
@if (moreTags) {
|
||||
<div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<p class="card-text">
|
||||
@if (document) {
|
||||
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>
|
||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{document.correspondent | correspondentName | async}}</a>
|
||||
@if (displayFields.includes(DisplayField.TITLE)) {:}
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.TITLE)) {
|
||||
@@ -59,14 +59,14 @@
|
||||
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="file-earmark"></i-bs>
|
||||
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
|
||||
<small>{{document.document_type | documentTypeName | async}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
|
||||
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
|
||||
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
|
||||
<small>{{document.storage_path | storagePathName | async}}</small>
|
||||
</button>
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.CREATED)) {
|
||||
@@ -116,7 +116,7 @@
|
||||
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
|
||||
<div class="ps-0 p-1">
|
||||
<i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
|
||||
<small>{{document.owner | username}}</small>
|
||||
<small>{{document.owner | username | async}}</small>
|
||||
</div>
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
|
||||
|
||||
-13
@@ -5,8 +5,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of } from 'rxjs'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { TagComponent } from '../../common/tag/tag.component'
|
||||
import { DocumentCardSmallComponent } from './document-card-small.component'
|
||||
|
||||
@@ -24,16 +22,6 @@ const doc = {
|
||||
note: 'This is some note content bananas',
|
||||
},
|
||||
],
|
||||
tags$: of([
|
||||
{ id: 1, name: 'Tag1' },
|
||||
{ id: 2, name: 'Tag2' },
|
||||
{ id: 3, name: 'Tag3' },
|
||||
{ id: 4, name: 'Tag4' },
|
||||
{ id: 5, name: 'Tag5' },
|
||||
{ id: 6, name: 'Tag6' },
|
||||
{ id: 7, name: 'Tag7' },
|
||||
{ id: 8, name: 'Tag8' },
|
||||
]),
|
||||
content:
|
||||
'Cupcake ipsum dolor sit amet ice cream. Donut shortbread cheesecake caramels tiramisu pastry caramels chocolate bar. Tart tootsie roll muffin icing cotton candy topping sweet roll. Pie lollipop dragée sesame snaps donut tart pudding. Oat cake apple pie danish danish candy canes. Shortbread candy canes sesame snaps muffin tiramisu marshmallow chocolate bar halvah. Cake lemon drops candy apple pie carrot cake bonbon halvah pastry gummi bears. Sweet roll candy ice cream sesame snaps marzipan cookie ice cream. Cake cheesecake apple pie muffin candy toffee lollipop. Carrot cake oat cake cookie biscuit cupcake cake marshmallow. Sweet roll jujubes carrot cake cheesecake cake candy canes sweet roll gingerbread jelly beans. Apple pie sugar plum oat cake halvah cake. Pie oat cake chocolate cake cookie gingerbread marzipan. Lemon drops cheesecake lollipop danish marzipan candy.',
|
||||
}
|
||||
@@ -80,7 +68,6 @@ describe('DocumentCardSmallComponent', () => {
|
||||
fixture.debugElement.queryAll(By.directive(TagComponent))
|
||||
).toHaveLength(5)
|
||||
component.document.tags = [1, 2]
|
||||
component.document.tags$ = of([{ id: 1 } as Tag, { id: 2 } as Tag])
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(TagComponent))
|
||||
|
||||
+20
-19
@@ -13,7 +13,8 @@ import {
|
||||
NgbTooltipModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { of } from 'rxjs'
|
||||
import { delay } from 'rxjs/operators'
|
||||
import {
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
@@ -21,9 +22,12 @@ import {
|
||||
} from 'src/app/data/document'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe'
|
||||
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||
import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
@@ -44,6 +48,9 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
||||
CustomFieldDisplayComponent,
|
||||
AsyncPipe,
|
||||
UsernamePipe,
|
||||
CorrespondentNamePipe,
|
||||
DocumentTypeNamePipe,
|
||||
StoragePathNamePipe,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
RouterModule,
|
||||
@@ -97,9 +104,11 @@ export class DocumentCardSmallComponent
|
||||
@ViewChild('popupPreview') popupPreview: PreviewPopupComponent
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
setInterval(() => {
|
||||
this.show = true
|
||||
}, 50)
|
||||
of(true)
|
||||
.pipe(delay(50))
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
})
|
||||
}
|
||||
|
||||
getIsThumbInverted() {
|
||||
@@ -114,22 +123,14 @@ export class DocumentCardSmallComponent
|
||||
return this.documentService.getDownloadUrl(this.document.id)
|
||||
}
|
||||
|
||||
get privateName() {
|
||||
return $localize`Private`
|
||||
}
|
||||
|
||||
getTagsLimited$() {
|
||||
get tagIDs() {
|
||||
const limit = this.document.notes.length > 0 ? 6 : 7
|
||||
return this.document.tags$?.pipe(
|
||||
map((tags) => {
|
||||
if (tags.length > limit) {
|
||||
this.moreTags = tags.length - (limit - 1)
|
||||
return tags.slice(0, limit - 1)
|
||||
} else {
|
||||
return tags
|
||||
}
|
||||
})
|
||||
)
|
||||
if (this.document.tags.length > limit) {
|
||||
this.moreTags = this.document.tags.length - (limit - 1)
|
||||
return this.document.tags.slice(0, limit - 1)
|
||||
} else {
|
||||
return this.document.tags
|
||||
}
|
||||
}
|
||||
|
||||
mouseLeaveCard() {
|
||||
|
||||
@@ -295,7 +295,7 @@
|
||||
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<td class="">
|
||||
@if (d.correspondent) {
|
||||
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
|
||||
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{d.correspondent | correspondentName | async}}</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@@ -310,15 +310,15 @@
|
||||
</div>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.TAGS)) {
|
||||
@for (t of d.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
|
||||
@for (tagID of d.tags; track t) {
|
||||
<pngx-tag [tagID]="tagID" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(tagID);$event.stopPropagation()"></pngx-tag>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
|
||||
<td>
|
||||
{{d.owner | username}}
|
||||
{{d.owner | username | async}}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
|
||||
@@ -335,14 +335,14 @@
|
||||
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<td class="">
|
||||
@if (d.document_type) {
|
||||
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
|
||||
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{d.document_type | documentTypeName | async}}</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||
<td class="">
|
||||
@if (d.storage_path) {
|
||||
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
|
||||
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{d.storage_path | storagePathName | async}}</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
@@ -357,10 +357,10 @@
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
|
||||
<td>
|
||||
{{ d.page_count }}
|
||||
</td>
|
||||
}
|
||||
<td>
|
||||
{{ d.page_count }}
|
||||
</td>
|
||||
}
|
||||
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
|
||||
<td>
|
||||
@if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }
|
||||
|
||||
@@ -38,16 +38,16 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import {
|
||||
FileStatus,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component'
|
||||
import { DocumentCardSmallComponent } from './document-card-small/document-card-small.component'
|
||||
import { DocumentListComponent } from './document-list.component'
|
||||
@@ -57,21 +57,21 @@ const docs: Document[] = [
|
||||
id: 1,
|
||||
title: 'Doc1',
|
||||
notes: [],
|
||||
tags$: new Subject(),
|
||||
tags: [],
|
||||
content: 'document content 1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Doc2',
|
||||
notes: [],
|
||||
tags$: new Subject(),
|
||||
tags: [],
|
||||
content: 'document content 2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Doc3',
|
||||
notes: [],
|
||||
tags$: new Subject(),
|
||||
tags: [],
|
||||
content: 'document content 3',
|
||||
},
|
||||
]
|
||||
@@ -81,7 +81,7 @@ describe('DocumentListComponent', () => {
|
||||
let fixture: ComponentFixture<DocumentListComponent>
|
||||
let documentListService: DocumentListViewService
|
||||
let documentService: DocumentService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
let savedViewService: SavedViewService
|
||||
let router: Router
|
||||
let activatedRoute: ActivatedRoute
|
||||
@@ -112,7 +112,7 @@ describe('DocumentListComponent', () => {
|
||||
|
||||
documentListService = TestBed.inject(DocumentListViewService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
savedViewService = TestBed.inject(SavedViewService)
|
||||
router = TestBed.inject(Router)
|
||||
activatedRoute = TestBed.inject(ActivatedRoute)
|
||||
@@ -128,13 +128,24 @@ describe('DocumentListComponent', () => {
|
||||
const reloadSpy = jest.spyOn(documentListService, 'reload')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
fixture.detectChanges()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should reload on document deleted', () => {
|
||||
const reloadSpy = jest.spyOn(documentListService, 'reload')
|
||||
const documentDeletedSubject = new Subject<boolean>()
|
||||
jest
|
||||
.spyOn(websocketStatusService, 'onDocumentDeleted')
|
||||
.mockReturnValue(documentDeletedSubject)
|
||||
fixture.detectChanges()
|
||||
documentDeletedSubject.next(true)
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show score sort fields on fulltext queries', () => {
|
||||
documentListService.filterRules = [
|
||||
{
|
||||
@@ -650,7 +661,6 @@ describe('DocumentListComponent', () => {
|
||||
id: i + 1,
|
||||
title: `Doc${i + 1}`,
|
||||
notes: [],
|
||||
tags$: new Subject(),
|
||||
content: `document content ${i + 1}`,
|
||||
}))
|
||||
jest
|
||||
|
||||
@@ -37,10 +37,12 @@ import {
|
||||
SortableDirective,
|
||||
SortEvent,
|
||||
} from 'src/app/directives/sortable.directive'
|
||||
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe'
|
||||
import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe'
|
||||
import { UsernamePipe } from 'src/app/pipes/username.pipe'
|
||||
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -48,6 +50,7 @@ import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import {
|
||||
filterRulesDiffer,
|
||||
isFullTextFilterRule,
|
||||
@@ -55,6 +58,7 @@ import {
|
||||
import { CustomFieldDisplayComponent } from '../common/custom-field-display/custom-field-display.component'
|
||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||
import { PreviewPopupComponent } from '../common/preview-popup/preview-popup.component'
|
||||
import { TagComponent } from '../common/tag/tag.component'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
import { BulkEditorComponent } from './bulk-editor/bulk-editor.component'
|
||||
import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component'
|
||||
@@ -74,11 +78,15 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
|
||||
DocumentCardSmallComponent,
|
||||
DocumentCardLargeComponent,
|
||||
PreviewPopupComponent,
|
||||
TagComponent,
|
||||
CustomDatePipe,
|
||||
DocumentTitlePipe,
|
||||
IfPermissionsDirective,
|
||||
SortableDirective,
|
||||
UsernamePipe,
|
||||
CorrespondentNamePipe,
|
||||
DocumentTypeNamePipe,
|
||||
StoragePathNamePipe,
|
||||
NgxBootstrapIconsModule,
|
||||
AsyncPipe,
|
||||
FormsModule,
|
||||
@@ -105,7 +113,7 @@ export class DocumentListComponent
|
||||
private router: Router,
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
private websocketStatusService: WebsocketStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
public settingsService: SettingsService,
|
||||
private hotKeyService: HotKeyService,
|
||||
@@ -226,13 +234,17 @@ export class DocumentListComponent
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.consumerStatusService
|
||||
this.websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.list.reload()
|
||||
})
|
||||
|
||||
this.websocketStatusService.onDocumentDeleted().subscribe(() => {
|
||||
this.list.reload()
|
||||
})
|
||||
|
||||
this.route.paramMap
|
||||
.pipe(
|
||||
filter((params) => params.has('id')), // only on saved view e.g. /view/id
|
||||
|
||||
@@ -108,13 +108,16 @@ export class CustomFieldsComponent
|
||||
this.customFieldsService.delete(field).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted field`)
|
||||
this.toastService.showInfo($localize`Deleted field "${field.name}"`)
|
||||
this.customFieldsService.clearCache()
|
||||
this.settingsService.initializeDisplayFields()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting field.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting field "${field.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<li class="list-group-item">
|
||||
<div class="row fade" [class.show]="showAccounts">
|
||||
<div class="col d-flex align-items-center">
|
||||
<button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">
|
||||
<button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount) || !userCanEdit(account)">
|
||||
{{account.name}}@switch (account.account_type) {
|
||||
@case (MailAccountType.IMAP) {<i-bs name="envelope-at-fill" class="ms-2"></i-bs>}
|
||||
@case (MailAccountType.Gmail_OAuth) {<i-bs name="google" class="ms-2"></i-bs>}
|
||||
@@ -62,10 +62,10 @@
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="editMailAccount(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Edit</button>
|
||||
<button (click)="editMailAccount(account)" [disabled]="!userCanEdit(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Edit</button>
|
||||
<button (click)="editPermissions(account)" *pngxIfOwner="account" ngbDropdownItem i18n>Permissions</button>
|
||||
<button (click)="deleteMailAccount(account)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Delete</button>
|
||||
<button (click)="processAccount(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Process Mail</button>
|
||||
<button (click)="deleteMailAccount(account)" [disabled]="!userIsOwner(account)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Delete</button>
|
||||
<button (click)="processAccount(account)" [disabled]="!userIsOwner(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Process Mail</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" class="btn btn-sm btn-outline-secondary" type="button" (click)="processAccount(account)">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userIsOwner(account)" class="btn btn-sm btn-outline-secondary" type="button" (click)="processAccount(account)">
|
||||
<i-bs width="1em" height="1em" name="arrow-clockwise"></i-bs> <ng-container i18n>Process Mail</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
@@ -126,7 +126,7 @@
|
||||
@for (rule of mailRules; track rule) {
|
||||
<li class="list-group-item">
|
||||
<div class="row fade" [class.show]="showRules">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule) || !userCanEdit(rule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center d-none d-sm-flex">{{rule.order}}</div>
|
||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||
<div class="col d-flex align-items-center d-none d-sm-flex">
|
||||
@@ -144,9 +144,9 @@
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="editMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" ngbDropdownItem i18n>Edit</button>
|
||||
<button (click)="editMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" [disabled]="!userCanEdit(rule)" ngbDropdownItem i18n>Edit</button>
|
||||
<button (click)="editPermissions(rule)" *pngxIfOwner="rule" ngbDropdownItem i18n>Permissions</button>
|
||||
<button (click)="deleteMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" ngbDropdownItem i18n>Delete</button>
|
||||
<button (click)="deleteMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" [disabled]="!userIsOwner(rule)" ngbDropdownItem i18n>Delete</button>
|
||||
<button (click)="copyMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }" ngbDropdownItem i18n>Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -214,7 +214,7 @@ describe('MailComponent', () => {
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account "account1"')
|
||||
})
|
||||
|
||||
it('should support process mail account, show error if needed', () => {
|
||||
@@ -231,7 +231,9 @@ describe('MailComponent', () => {
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
processSpy.mockReturnValueOnce(of(true))
|
||||
component.processAccount(mailAccounts[0] as MailAccount)
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Processing mail account')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||
'Processing mail account "account1"'
|
||||
)
|
||||
})
|
||||
|
||||
it('should support edit / create mail rule, show error if needed', () => {
|
||||
@@ -274,14 +276,14 @@ describe('MailComponent', () => {
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const listAllSpy = jest.spyOn(mailRuleService, 'listAll')
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('error deleting mail rule'))
|
||||
throwError(() => new Error('error deleting mail rule "rule1"'))
|
||||
)
|
||||
deleteDialog.confirm()
|
||||
expect(toastErrorSpy).toBeCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
deleteDialog.confirm()
|
||||
expect(listAllSpy).toHaveBeenCalled()
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule')
|
||||
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule "rule1"')
|
||||
})
|
||||
|
||||
it('should support edit permissions on mail rule objects', () => {
|
||||
|
||||
@@ -200,7 +200,9 @@ export class MailComponent
|
||||
this.mailAccountService.delete(account).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted mail account`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Deleted mail account "${account.name}"`
|
||||
)
|
||||
this.mailAccountService.clearCache()
|
||||
this.mailAccountService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
@@ -210,7 +212,7 @@ export class MailComponent
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting mail account.`,
|
||||
$localize`Error deleting mail account "${account.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
@@ -221,10 +223,15 @@ export class MailComponent
|
||||
processAccount(account: MailAccount) {
|
||||
this.mailAccountService.processAccount(account).subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Processing mail account`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Processing mail account "${account.name}"`
|
||||
)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error processing mail account`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error processing mail account "${account.name}")`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -272,7 +279,10 @@ export class MailComponent
|
||||
)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error toggling rule.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error toggling rule "${rule.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -291,7 +301,9 @@ export class MailComponent
|
||||
this.mailRuleService.delete(rule).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted mail rule`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Deleted mail rule "${rule.name}"`
|
||||
)
|
||||
this.mailRuleService.clearCache()
|
||||
this.mailRuleService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
@@ -300,7 +312,10 @@ export class MailComponent
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting mail rule.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting mail rule "${rule.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,35 +73,37 @@
|
||||
</td>
|
||||
}
|
||||
<td scope="row">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
|
||||
<button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
|
||||
@if (object.document_count > 0) {
|
||||
<button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ object.document_count }})</button>
|
||||
}
|
||||
<div class="btn-toolbar gap-2">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
|
||||
<button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
|
||||
@if (object.document_count > 0) {
|
||||
<button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ object.document_count }})</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
@if (object.document_count > 0) {
|
||||
<div class="btn-group d-none d-sm-inline-block ms-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ object.document_count }}</span>
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (object.document_count > 0) {
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ object.document_count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import {
|
||||
HttpErrorResponse,
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
@@ -243,6 +247,21 @@ describe('ManagementListComponent', () => {
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fall back to first page if error is page is out of range', () => {
|
||||
jest.spyOn(tagService, 'listFiltered').mockReturnValueOnce(
|
||||
throwError(
|
||||
() =>
|
||||
new HttpErrorResponse({
|
||||
status: 404,
|
||||
error: { detail: 'Invalid page' },
|
||||
})
|
||||
)
|
||||
)
|
||||
component.page = 2
|
||||
component.reloadData()
|
||||
expect(component.page).toEqual(1)
|
||||
})
|
||||
|
||||
it('should support toggle all items in view', () => {
|
||||
expect(component.selectedObjects.size).toEqual(0)
|
||||
const toggleAllSpy = jest.spyOn(component, 'toggleAll')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import {
|
||||
Directive,
|
||||
OnDestroy,
|
||||
@@ -152,9 +153,17 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
}),
|
||||
delay(100)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
this.loading = false
|
||||
.subscribe({
|
||||
error: (error: HttpErrorResponse) => {
|
||||
if (error.error?.detail?.includes('Invalid page')) {
|
||||
this.page = 1
|
||||
this.reloadData()
|
||||
}
|
||||
},
|
||||
next: () => {
|
||||
this.show = true
|
||||
this.loading = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,57 @@ describe('WorkflowsComponent', () => {
|
||||
expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE)
|
||||
})
|
||||
|
||||
it('should null ids on copy', () => {
|
||||
const workflow = {
|
||||
id: 1,
|
||||
name: 'Workflow 1',
|
||||
order: 1,
|
||||
enabled: true,
|
||||
triggers: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowTriggerType.Consumption,
|
||||
sources: [DocumentSource.ConsumeFolder],
|
||||
filter_filename: '*',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowActionType.Email,
|
||||
email: {
|
||||
id: 1,
|
||||
subject: 'foo',
|
||||
body: 'bar',
|
||||
to: 'baz',
|
||||
include_document: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: WorkflowActionType.Webhook,
|
||||
webhook: {
|
||||
id: 2,
|
||||
url: 'foo',
|
||||
use_params: false,
|
||||
params: {},
|
||||
body: 'bar',
|
||||
headers: {},
|
||||
include_document: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
component.copyWorkflow(workflow)
|
||||
expect(modal).not.toBeUndefined()
|
||||
const editDialog = modal.componentInstance as WorkflowEditDialogComponent
|
||||
expect(editDialog.object.id).toBeNull()
|
||||
expect(editDialog.object.triggers[0].id).toBeNull()
|
||||
expect(editDialog.object.actions[0].id).toBeNull()
|
||||
})
|
||||
|
||||
it('should support delete, show notification on error / success', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
|
||||
@@ -110,6 +110,12 @@ export class WorkflowsComponent
|
||||
clone.actions = [
|
||||
...workflow.actions.map((a) => {
|
||||
a.id = null
|
||||
if (a.webhook) {
|
||||
a.webhook.id = null
|
||||
}
|
||||
if (a.email) {
|
||||
a.email.id = null
|
||||
}
|
||||
return a
|
||||
}),
|
||||
]
|
||||
@@ -136,12 +142,17 @@ export class WorkflowsComponent
|
||||
this.workflowService.delete(workflow).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted workflow`)
|
||||
this.toastService.showInfo(
|
||||
$localize`Deleted workflow "${workflow.name}".`
|
||||
)
|
||||
this.workflowService.clearCache()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting workflow.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error deleting workflow "${workflow.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -152,14 +163,17 @@ export class WorkflowsComponent
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
workflow.enabled
|
||||
? $localize`Enabled workflow`
|
||||
: $localize`Disabled workflow`
|
||||
? $localize`Enabled workflow "${workflow.name}"`
|
||||
: $localize`Disabled workflow "${workflow.name}"`
|
||||
)
|
||||
this.workflowService.clearCache()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error toggling workflow.`, e)
|
||||
this.toastService.showError(
|
||||
$localize`Error toggling workflow "${workflow.name}".`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { LogoComponent } from '../common/logo/logo.component'
|
||||
import { NotFoundComponent } from './not-found.component'
|
||||
|
||||
@@ -16,6 +18,7 @@ describe('NotFoundComponent', () => {
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NotFoundComponent,
|
||||
LogoComponent,
|
||||
RouterModule.forRoot(routes),
|
||||
],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { LogoComponent } from '../common/logo/logo.component'
|
||||
|
||||
@@ -6,7 +7,7 @@ import { LogoComponent } from '../common/logo/logo.component'
|
||||
selector: 'pngx-not-found',
|
||||
templateUrl: './not-found.component.html',
|
||||
styleUrls: ['./not-found.component.scss'],
|
||||
imports: [LogoComponent, NgxBootstrapIconsModule],
|
||||
imports: [LogoComponent, NgxBootstrapIconsModule, RouterModule],
|
||||
})
|
||||
export class NotFoundComponent {
|
||||
constructor() {}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Observable } from 'rxjs'
|
||||
import { Correspondent } from './correspondent'
|
||||
import { CustomFieldInstance } from './custom-field-instance'
|
||||
import { DocumentNote } from './document-note'
|
||||
import { DocumentType } from './document-type'
|
||||
import { ObjectWithPermissions } from './object-with-permissions'
|
||||
import { StoragePath } from './storage-path'
|
||||
import { Tag } from './tag'
|
||||
|
||||
export enum DisplayMode {
|
||||
TABLE = 'table',
|
||||
@@ -118,24 +113,16 @@ export interface SearchHit {
|
||||
}
|
||||
|
||||
export interface Document extends ObjectWithPermissions {
|
||||
correspondent$?: Observable<Correspondent>
|
||||
|
||||
correspondent?: number
|
||||
|
||||
document_type$?: Observable<DocumentType>
|
||||
|
||||
document_type?: number
|
||||
|
||||
storage_path$?: Observable<StoragePath>
|
||||
|
||||
storage_path?: number
|
||||
|
||||
title?: string
|
||||
|
||||
content?: string
|
||||
|
||||
tags$?: Observable<Tag[]>
|
||||
|
||||
tags?: number[]
|
||||
|
||||
checksum?: string
|
||||
|
||||
@@ -7,7 +7,7 @@ export enum MailFilterAttachmentType {
|
||||
|
||||
export enum MailRuleConsumptionScope {
|
||||
Attachments = 1,
|
||||
Email_Only = 2,
|
||||
EmailOnly = 2,
|
||||
Everything = 3,
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export const SETTINGS_KEYS = {
|
||||
DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted',
|
||||
THEME_COLOR: 'general-settings:theme:color',
|
||||
USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer',
|
||||
PDF_VIEWER_ZOOM_SETTING:
|
||||
'general-settings:document-details:pdf-viewer-zoom-setting',
|
||||
DATE_LOCALE: 'general-settings:date-display:date-locale',
|
||||
DATE_FORMAT: 'general-settings:date-display:date-format',
|
||||
NOTIFICATIONS_CONSUMER_NEW_DOCUMENT:
|
||||
@@ -269,4 +271,9 @@ export const SETTINGS: UiSetting[] = [
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING,
|
||||
type: 'string',
|
||||
default: 'page-width', // ZoomSetting from 'document-detail.component'
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface WebsocketDocumentsDeletedMessage {
|
||||
documents: number[]
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export interface WebsocketConsumerStatusMessage {
|
||||
export interface WebsocketProgressMessage {
|
||||
filename?: string
|
||||
task_id?: string
|
||||
current_progress?: number
|
||||
@@ -22,6 +22,8 @@ export interface WorkflowActionWebhook extends ObjectWithId {
|
||||
|
||||
use_params?: boolean
|
||||
|
||||
as_json?: boolean
|
||||
|
||||
params?: object
|
||||
|
||||
body?: string
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { PermissionsService } from '../services/permissions.service'
|
||||
import { CorrespondentService } from '../services/rest/correspondent.service'
|
||||
import { CorrespondentNamePipe } from './correspondent-name.pipe'
|
||||
|
||||
describe('CorrespondentNamePipe', () => {
|
||||
let pipe: CorrespondentNamePipe
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
// The pipe is a simple wrapper around ObjectNamePipe, see ObjectNamePipe for the actual tests.
|
||||
it('should be created', () => {
|
||||
pipe = new CorrespondentNamePipe(
|
||||
TestBed.inject(PermissionsService),
|
||||
TestBed.inject(CorrespondentService)
|
||||
)
|
||||
expect(pipe).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from '../services/permissions.service'
|
||||
import { CorrespondentService } from '../services/rest/correspondent.service'
|
||||
import { ObjectNamePipe } from './object-name.pipe'
|
||||
|
||||
@Pipe({
|
||||
name: 'correspondentName',
|
||||
})
|
||||
export class CorrespondentNamePipe
|
||||
extends ObjectNamePipe
|
||||
implements PipeTransform
|
||||
{
|
||||
constructor(
|
||||
permissionsService: PermissionsService,
|
||||
objectService: CorrespondentService
|
||||
) {
|
||||
super(permissionsService, PermissionType.Correspondent, objectService)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { PermissionsService } from '../services/permissions.service'
|
||||
import { DocumentTypeService } from '../services/rest/document-type.service'
|
||||
import { DocumentTypeNamePipe } from './document-type-name.pipe'
|
||||
|
||||
describe('DocumentTypeNamePipe', () => {
|
||||
let pipe: DocumentTypeNamePipe
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
// The pipe is a simple wrapper around ObjectNamePipe, see ObjectNamePipe for the actual tests.
|
||||
it('should be created', () => {
|
||||
pipe = new DocumentTypeNamePipe(
|
||||
TestBed.inject(PermissionsService),
|
||||
TestBed.inject(DocumentTypeService)
|
||||
)
|
||||
expect(pipe).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from '../services/permissions.service'
|
||||
import { DocumentTypeService } from '../services/rest/document-type.service'
|
||||
import { ObjectNamePipe } from './object-name.pipe'
|
||||
|
||||
@Pipe({
|
||||
name: 'documentTypeName',
|
||||
})
|
||||
export class DocumentTypeNamePipe
|
||||
extends ObjectNamePipe
|
||||
implements PipeTransform
|
||||
{
|
||||
constructor(
|
||||
permissionsService: PermissionsService,
|
||||
objectService: DocumentTypeService
|
||||
) {
|
||||
super(permissionsService, PermissionType.DocumentType, objectService)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { MatchingModel } from '../data/matching-model'
|
||||
import { PermissionsService } from '../services/permissions.service'
|
||||
import { AbstractNameFilterService } from '../services/rest/abstract-name-filter-service'
|
||||
import { CorrespondentService } from '../services/rest/correspondent.service'
|
||||
import { CorrespondentNamePipe } from './correspondent-name.pipe'
|
||||
import { ObjectNamePipe } from './object-name.pipe'
|
||||
|
||||
describe('ObjectNamePipe', () => {
|
||||
/*
|
||||
ObjectNamePipe is an abstract class to prevent instantiation,
|
||||
so we test the concrete implementation CorrespondentNamePipe instead.
|
||||
*/
|
||||
let pipe: CorrespondentNamePipe
|
||||
let permissionsService: PermissionsService
|
||||
let objectService: AbstractNameFilterService<MatchingModel>
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ObjectNamePipe,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
})
|
||||
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
objectService = TestBed.inject(CorrespondentService)
|
||||
pipe = new CorrespondentNamePipe(permissionsService, objectService)
|
||||
})
|
||||
|
||||
it('should return object name if user has permission', (done) => {
|
||||
const mockObjects = {
|
||||
results: [
|
||||
{ id: 1, name: 'Object 1' },
|
||||
{ id: 2, name: 'Object 2' },
|
||||
],
|
||||
count: 2,
|
||||
all: [1, 2],
|
||||
}
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest.spyOn(objectService, 'listAll').mockReturnValue(of(mockObjects))
|
||||
|
||||
pipe.transform(1).subscribe((result) => {
|
||||
expect(result).toBe('Object 1')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return empty string if object not found', (done) => {
|
||||
const mockObjects = {
|
||||
results: [{ id: 2, name: 'Object 2' }],
|
||||
count: 1,
|
||||
all: [2],
|
||||
}
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest.spyOn(objectService, 'listAll').mockReturnValue(of(mockObjects))
|
||||
|
||||
pipe.transform(1).subscribe((result) => {
|
||||
expect(result).toBe('')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return "Private" if user does not have permission', (done) => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
|
||||
|
||||
pipe.transform(1).subscribe((result) => {
|
||||
expect(result).toBe('Private')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle error and return empty string', (done) => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(objectService, 'listAll')
|
||||
.mockReturnValueOnce(throwError(() => new Error('Error getting objects')))
|
||||
|
||||
pipe.transform(1).subscribe((result) => {
|
||||
expect(result).toBe('')
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { catchError, map, Observable, of } from 'rxjs'
|
||||
import { MatchingModel } from '../data/matching-model'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from '../services/permissions.service'
|
||||
import { AbstractNameFilterService } from '../services/rest/abstract-name-filter-service'
|
||||
|
||||
@Pipe({
|
||||
name: 'objectName',
|
||||
})
|
||||
export abstract class ObjectNamePipe implements PipeTransform {
|
||||
/*
|
||||
ObjectNamePipe is an abstract class to prevent instantiation,
|
||||
object-specific pipes extend this class and provide the
|
||||
correct permission type, and object service.
|
||||
*/
|
||||
protected objects: MatchingModel[]
|
||||
|
||||
constructor(
|
||||
protected permissionsService: PermissionsService,
|
||||
protected permissionType: PermissionType,
|
||||
protected objectService: AbstractNameFilterService<MatchingModel>
|
||||
) {}
|
||||
|
||||
transform(obejctId: number): Observable<string> {
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
this.permissionType
|
||||
)
|
||||
) {
|
||||
return this.objectService.listAll().pipe(
|
||||
map((objects) => {
|
||||
this.objects = objects.results
|
||||
return this.objects.find((o) => o.id === obejctId)?.name || ''
|
||||
}),
|
||||
catchError(() => of(''))
|
||||
)
|
||||
} else {
|
||||
return of($localize`Private`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { PermissionsService } from '../services/permissions.service'
|
||||
import { StoragePathService } from '../services/rest/storage-path.service'
|
||||
import { StoragePathNamePipe } from './storage-path-name.pipe'
|
||||
|
||||
describe('StoragePathNamePipe', () => {
|
||||
let pipe: StoragePathNamePipe
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
// The pipe is a simple wrapper around ObjectNamePipe, see ObjectNamePipe for the actual tests.
|
||||
it('should be created', () => {
|
||||
pipe = new StoragePathNamePipe(
|
||||
TestBed.inject(PermissionsService),
|
||||
TestBed.inject(StoragePathService)
|
||||
)
|
||||
expect(pipe).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from '../services/permissions.service'
|
||||
import { StoragePathService } from '../services/rest/storage-path.service'
|
||||
import { ObjectNamePipe } from './object-name.pipe'
|
||||
|
||||
@Pipe({
|
||||
name: 'storagePathName',
|
||||
})
|
||||
export class StoragePathNamePipe
|
||||
extends ObjectNamePipe
|
||||
implements PipeTransform
|
||||
{
|
||||
constructor(
|
||||
permissionsService: PermissionsService,
|
||||
objectService: StoragePathService
|
||||
) {
|
||||
super(permissionsService, PermissionType.StoragePath, objectService)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,11 @@ describe('UsernamePipe', () => {
|
||||
httpTestingController.verify()
|
||||
})
|
||||
|
||||
it('should transform user id to username', () => {
|
||||
it('should transform user id to username', (done) => {
|
||||
pipe.transform(2).subscribe((username) => {
|
||||
expect(username).toEqual('username2')
|
||||
})
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}users/?page=1&page_size=100000`
|
||||
)
|
||||
@@ -55,24 +59,39 @@ describe('UsernamePipe', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
pipe.transform(3).subscribe((username) => {
|
||||
expect(username).toEqual('User Name3')
|
||||
})
|
||||
|
||||
let username = pipe.transform(2)
|
||||
expect(username).toEqual('username2')
|
||||
|
||||
username = pipe.transform(3)
|
||||
expect(username).toEqual('User Name3')
|
||||
|
||||
username = pipe.transform(4)
|
||||
expect(username).toEqual('')
|
||||
pipe.transform(4).subscribe((username) => {
|
||||
expect(username).toEqual('')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show generic label when no users retrieved', () => {
|
||||
it('should show generic label when insufficient permissions', (done) => {
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserCan')
|
||||
.mockImplementation((action, type) => {
|
||||
return false
|
||||
})
|
||||
pipe.transform(4).subscribe((username) => {
|
||||
expect(username).toEqual('Shared')
|
||||
done()
|
||||
})
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}users/?page=1&page_size=100000`
|
||||
)
|
||||
})
|
||||
|
||||
it('should show empty string when no users retrieved due to error', (done) => {
|
||||
pipe.transform(4).subscribe((username) => {
|
||||
expect(username).toEqual('')
|
||||
done()
|
||||
})
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}users/?page=1&page_size=100000`
|
||||
)
|
||||
req.flush(null)
|
||||
|
||||
let username = pipe.transform(4)
|
||||
expect(username).toEqual('Shared')
|
||||
req.error(new ProgressEvent('error'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { catchError, map, Observable, of } from 'rxjs'
|
||||
import { User } from '../data/user'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from '../services/permissions.service'
|
||||
import { UserService } from '../services/rest/user.service'
|
||||
|
||||
@@ -14,25 +15,29 @@ export class UsernamePipe implements PipeTransform {
|
||||
users: User[]
|
||||
|
||||
constructor(
|
||||
permissionsService: PermissionsService,
|
||||
userService: UserService
|
||||
) {
|
||||
private permissionsService: PermissionsService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
transform(userID: number): Observable<string> {
|
||||
if (
|
||||
permissionsService.currentUserCan(
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.User
|
||||
)
|
||||
) {
|
||||
userService.listAll().subscribe((r) => (this.users = r.results))
|
||||
return this.userService.listAll().pipe(
|
||||
map((users) => {
|
||||
this.users = users.results
|
||||
return this.getName(this.users.find((u) => u.id === userID))
|
||||
}),
|
||||
catchError(() => of(''))
|
||||
)
|
||||
} else {
|
||||
return of($localize`Shared`)
|
||||
}
|
||||
}
|
||||
|
||||
transform(userID: number): string {
|
||||
return this.users
|
||||
? (this.getName(this.users.find((u) => u.id === userID)) ?? '')
|
||||
: $localize`Shared`
|
||||
}
|
||||
|
||||
getName(user: User): string {
|
||||
if (!user) return ''
|
||||
const name = [user.first_name, user.last_name].join(' ')
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { ActivationStart, Router } from '@angular/router'
|
||||
import { Subject } from 'rxjs'
|
||||
import { ComponentRouterService } from './component-router.service'
|
||||
|
||||
describe('ComponentRouterService', () => {
|
||||
let service: ComponentRouterService
|
||||
let router: Router
|
||||
let eventsSubject: Subject<any>
|
||||
|
||||
beforeEach(() => {
|
||||
eventsSubject = new Subject<any>()
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ComponentRouterService,
|
||||
{
|
||||
provide: Router,
|
||||
useValue: {
|
||||
events: eventsSubject.asObservable(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
service = TestBed.inject(ComponentRouterService)
|
||||
router = TestBed.inject(Router)
|
||||
})
|
||||
|
||||
it('should add to history and componentHistory on ActivationStart event', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect((service as any).history).toEqual(['test-url'])
|
||||
expect((service as any).componentHistory).toEqual(['TestComponent'])
|
||||
})
|
||||
|
||||
it('should not add duplicate component names to componentHistory', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-1',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-2',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect((service as any).componentHistory.length).toBe(1)
|
||||
expect((service as any).componentHistory).toEqual(['TestComponent'])
|
||||
})
|
||||
|
||||
it('should return the URL of the component before the current one', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-1',
|
||||
component: { name: 'TestComponent1' },
|
||||
} as any)
|
||||
)
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-2',
|
||||
component: { name: 'TestComponent2' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect(service.getComponentURLBefore()).toBe('test-url-1')
|
||||
})
|
||||
|
||||
it('should update the URL of the current component if the same component is loaded via a different URL', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-1',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url-2',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect((service as any).history).toEqual(['test-url-2'])
|
||||
})
|
||||
|
||||
it('should return null if there is no previous component', () => {
|
||||
eventsSubject.next(
|
||||
new ActivationStart({
|
||||
url: 'test-url',
|
||||
component: { name: 'TestComponent' },
|
||||
} as any)
|
||||
)
|
||||
|
||||
expect(service.getComponentURLBefore()).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ActivationStart, Event, Router } from '@angular/router'
|
||||
import { filter } from 'rxjs'
|
||||
|
||||
const EXCLUDE_COMPONENTS = ['AppFrameComponent']
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ComponentRouterService {
|
||||
private history: string[] = []
|
||||
private componentHistory: any[] = []
|
||||
|
||||
constructor(private router: Router) {
|
||||
this.router.events
|
||||
.pipe(filter((event: Event) => event instanceof ActivationStart))
|
||||
.subscribe((event: ActivationStart) => {
|
||||
if (
|
||||
this.componentHistory[this.componentHistory.length - 1] !==
|
||||
event.snapshot.component.name &&
|
||||
!EXCLUDE_COMPONENTS.includes(event.snapshot.component.name)
|
||||
) {
|
||||
this.history.push(event.snapshot.url.toString())
|
||||
this.componentHistory.push(event.snapshot.component.name)
|
||||
} else {
|
||||
// Update the URL of the current component in case the same component was loaded via a different URL
|
||||
this.history[this.history.length - 1] = event.snapshot.url.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getComponentURLBefore(): any {
|
||||
if (this.componentHistory.length > 1) {
|
||||
return this.history[this.history.length - 2]
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
import {
|
||||
HttpEventType,
|
||||
HttpResponse,
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http'
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import WS from 'jest-websocket-mock'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FILE_STATUS_MESSAGES,
|
||||
FileStatusPhase,
|
||||
} from './consumer-status.service'
|
||||
import { DocumentService } from './rest/document.service'
|
||||
import { SettingsService } from './settings.service'
|
||||
|
||||
describe('ConsumerStatusService', () => {
|
||||
let httpTestingController: HttpTestingController
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let documentService: DocumentService
|
||||
let settingsService: SettingsService
|
||||
|
||||
const server = new WS(
|
||||
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`,
|
||||
{ jsonProtocol: true }
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
ConsumerStatusService,
|
||||
DocumentService,
|
||||
SettingsService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
})
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
}
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
httpTestingController.verify()
|
||||
})
|
||||
|
||||
it('should update status on websocket processing progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
expect(status.getProgress()).toEqual(0)
|
||||
|
||||
consumerStatusService.connect()
|
||||
|
||||
consumerStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.SUCCESS)
|
||||
})
|
||||
|
||||
consumerStatusService.onDocumentDetected().subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
|
||||
})
|
||||
|
||||
server.send({
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'WORKING',
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toBeCloseTo(0.6) // (0.8 * 50/100) + .2
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
server.send({
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 100,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'SUCCESS',
|
||||
message: FILE_STATUS_MESSAGES.finished,
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toEqual(1)
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
|
||||
consumerStatusService.disconnect()
|
||||
})
|
||||
|
||||
it('should update status on websocket failed progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
consumerStatusService.connect()
|
||||
|
||||
consumerStatusService
|
||||
.onDocumentConsumptionFailed()
|
||||
.subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.FAILED)
|
||||
})
|
||||
|
||||
server.send({
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
})
|
||||
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
server.send({
|
||||
task_id,
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'FAILED',
|
||||
message: FILE_STATUS_MESSAGES.document_already_exists,
|
||||
})
|
||||
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should update status on upload progress', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
|
||||
documentService.uploadDocument({}).subscribe((event) => {
|
||||
if (event.type === HttpEventType.Response) {
|
||||
status.taskId = event.body['task_id']
|
||||
status.message = $localize`Upload complete, waiting...`
|
||||
} else if (event.type === HttpEventType.UploadProgress) {
|
||||
status.updateProgress(
|
||||
FileStatusPhase.UPLOADING,
|
||||
event.loaded,
|
||||
event.total
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/post_document/`
|
||||
)
|
||||
|
||||
req.event(
|
||||
new HttpResponse({
|
||||
body: {
|
||||
task_id,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
req.event({
|
||||
type: HttpEventType.UploadProgress,
|
||||
loaded: 100,
|
||||
total: 300,
|
||||
})
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toEqual([status])
|
||||
expect(consumerStatusService.getConsumerStatus()).toEqual([status])
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
])
|
||||
|
||||
req.event({
|
||||
type: HttpEventType.UploadProgress,
|
||||
loaded: 300,
|
||||
total: 300,
|
||||
})
|
||||
|
||||
expect(status.getProgress()).toEqual(0.2) // 0.2 * 300/300
|
||||
})
|
||||
|
||||
it('should support dismiss completed', () => {
|
||||
consumerStatusService.connect()
|
||||
server.send({
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 100,
|
||||
max_progress: 100,
|
||||
document_id: 12,
|
||||
status: 'SUCCESS',
|
||||
message: 'finished',
|
||||
})
|
||||
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
consumerStatusService.dismissCompleted()
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0)
|
||||
consumerStatusService.disconnect()
|
||||
})
|
||||
|
||||
it('should support dismiss', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
|
||||
const status2 = consumerStatusService.newFileUpload('file2.pdf')
|
||||
status2.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
|
||||
expect(
|
||||
consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING)
|
||||
).toEqual([status, status2])
|
||||
expect(consumerStatusService.getConsumerStatus()).toEqual([status, status2])
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([
|
||||
status,
|
||||
status2,
|
||||
])
|
||||
|
||||
consumerStatusService.dismiss(status)
|
||||
expect(consumerStatusService.getConsumerStatus()).toEqual([status2])
|
||||
|
||||
consumerStatusService.dismiss(status2)
|
||||
expect(consumerStatusService.getConsumerStatus()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should support fail', () => {
|
||||
const task_id = '1234'
|
||||
const status = consumerStatusService.newFileUpload('file.pdf')
|
||||
status.taskId = task_id
|
||||
status.updateProgress(FileStatusPhase.UPLOADING, 50, 100)
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0)
|
||||
consumerStatusService.fail(status, 'fail')
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
0
|
||||
)
|
||||
expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should notify of document created on status message without upload', () => {
|
||||
let detected = false
|
||||
consumerStatusService.onDocumentDetected().subscribe((filestatus) => {
|
||||
expect(filestatus.phase).toEqual(FileStatusPhase.STARTED)
|
||||
detected = true
|
||||
})
|
||||
|
||||
consumerStatusService.connect()
|
||||
server.send({
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 0,
|
||||
max_progress: 100,
|
||||
message: 'new_file',
|
||||
status: 'STARTED',
|
||||
})
|
||||
|
||||
consumerStatusService.disconnect()
|
||||
expect(detected).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should notify of document in progress without upload', () => {
|
||||
consumerStatusService.connect()
|
||||
server.send({
|
||||
task_id: '1234',
|
||||
filename: 'file.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 12,
|
||||
status: 'WORKING',
|
||||
})
|
||||
|
||||
consumerStatusService.disconnect()
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
})
|
||||
|
||||
it('should not notify current user if document has different expected owner', () => {
|
||||
consumerStatusService.connect()
|
||||
server.send({
|
||||
task_id: '1234',
|
||||
filename: 'file1.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 12,
|
||||
owner_id: 1,
|
||||
status: 'WORKING',
|
||||
})
|
||||
|
||||
server.send({
|
||||
task_id: '5678',
|
||||
filename: 'file2.pdf',
|
||||
current_progress: 50,
|
||||
max_progress: 100,
|
||||
docuement_id: 13,
|
||||
owner_id: 2,
|
||||
status: 'WORKING',
|
||||
})
|
||||
|
||||
consumerStatusService.disconnect()
|
||||
expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength(
|
||||
1
|
||||
)
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user