Compare commits

..

1 Commits

Author SHA1 Message Date
shamoon
ba20dabfa4 Fix: add negation to scheduled wf migration 2025-06-18 07:47:10 -07:00
361 changed files with 13900 additions and 18489 deletions

View File

@@ -20,6 +20,7 @@
# #
# This file is intended only to be used through VSCOde devcontainers. See README.md # This file is intended only to be used through VSCOde devcontainers. See README.md
# in the folder .devcontainer. # in the folder .devcontainer.
services: services:
broker: broker:
image: docker.io/library/redis:7 image: docker.io/library/redis:7

View File

@@ -1,55 +0,0 @@
title: "[Support] "
body:
- type: textarea
id: description
attributes:
label: What's your question or issue?
description: Provide a clear and concise description of what you're trying to do, and what's going wrong.
placeholder: |
I'm trying to...
[Include screenshots if helpful]
validations:
required: true
- type: textarea
id: steps
attributes:
label: What have you tried?
description: Describe any steps you've already taken to troubleshoot or solve the issue.
placeholder: |
- I checked the logs and saw...
- I followed the install guide and tried...
- type: input
id: version
attributes:
label: Paperless-ngx version
placeholder: e.g. 1.14.0
validations:
required: true
- type: input
id: host-os
attributes:
label: Host OS
description: Include architecture if relevant.
placeholder: e.g. Ubuntu 22.04 / Raspberry Pi arm64
- type: dropdown
id: install-method
attributes:
label: Installation method
options:
- Docker - official image
- Docker - linuxserver.io image
- Bare metal
- Other (please describe above)
- type: textarea
id: system-status
attributes:
label: System status
description: If available, copy & paste the system status output from Settings > System Status > Copy
render: json
- type: textarea
id: logs
attributes:
label: Relevant logs or output
description: If you have logs, errors that might help, paste it here.
render: bash

View File

@@ -1,5 +1,6 @@
# Please see the documentation for all configuration options: # Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2 version: 2
# Required for uv support for now # Required for uv support for now
enable-beta-ecosystems: true enable-beta-ecosystems: true

View File

@@ -12,10 +12,9 @@ on:
branches-ignore: branches-ignore:
- 'translations**' - 'translations**'
env: env:
DEFAULT_UV_VERSION: "0.8.x" DEFAULT_UV_VERSION: "0.7.x"
# This is the default version of Python to use in most steps which aren't specific # This is the default version of Python to use in most steps which aren't specific
DEFAULT_PYTHON_VERSION: "3.11" DEFAULT_PYTHON_VERSION: "3.11"
NLTK_DATA: "/usr/share/nltk_data"
jobs: jobs:
pre-commit: pre-commit:
# We want to run on external PRs, but not on our own internal PRs as they'll be run # We want to run on external PRs, but not on our own internal PRs as they'll be run
@@ -122,11 +121,8 @@ jobs:
- name: List installed Python dependencies - name: List installed Python dependencies
run: | run: |
uv pip list uv pip list
- name: Install or update NLTK dependencies
run: uv run python -m nltk.downloader punkt punkt_tab snowball_data stopwords -d ${{ env.NLTK_DATA }}
- name: Tests - name: Tests
env: env:
NLTK_DATA: ${{ env.NLTK_DATA }}
PAPERLESS_CI_TEST: 1 PAPERLESS_CI_TEST: 1
# Enable paperless_mail testing against real server # Enable paperless_mail testing against real server
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }} PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}

View File

@@ -4,6 +4,7 @@
# Requires a PAT with the correct scope set in the secrets. # Requires a PAT with the correct scope set in the secrets.
# #
# This workflow will not trigger runs on forked repos. # This workflow will not trigger runs on forked repos.
name: Cleanup Image Tags name: Cleanup Image Tags
on: on:
delete: delete:

View File

@@ -19,19 +19,12 @@ jobs:
with: with:
days-before-stale: 7 days-before-stale: 7
days-before-close: 14 days-before-close: 14
any-of-issue-labels: 'cant-reproduce,not a bug' any-of-labels: 'stale,cant-reproduce,not a bug'
stale-issue-label: stale stale-issue-label: stale
stale-pr-label: stale
stale-issue-message: > stale-issue-message: >
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details. This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
days-before-pr-stale: 14
days-before-pr-close: 7
stale-pr-message: ""
stale-pr-label: stale
exempt-pr-labels: 'notable'
close-pr-message: >
This pull request has been automatically closed because it has not had recent activity. Thank you for your contributions. Please open a new pull request or discussion if you would like to continue working on this change.
lock-threads: lock-threads:
name: 'Lock Old Threads' name: 'Lock Old Threads'
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'

View File

@@ -61,7 +61,7 @@ jobs:
cd src-ui cd src-ui
pnpm run ng extract-i18n pnpm run ng extract-i18n
- name: Commit changes - name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v6 uses: stefanzweifel/git-auto-commit-action@v5
with: with:
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po' file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
commit_message: "Auto translate strings" commit_message: "Auto translate strings"

View File

@@ -1,6 +1,7 @@
# This file configures pre-commit hooks. # This file configures pre-commit hooks.
# See https://pre-commit.com/ for general information # See https://pre-commit.com/ for general information
# See https://pre-commit.com/hooks.html for a listing of possible hooks # See https://pre-commit.com/hooks.html for a listing of possible hooks
repos: repos:
# General hooks # General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
@@ -28,16 +29,16 @@ repos:
- id: check-case-conflict - id: check-case-conflict
- id: detect-private-key - id: detect-private-key
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.4.1 rev: v2.4.0
hooks: hooks:
- id: codespell - id: codespell
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)|(^src/documents/tests/samples/)" exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
exclude_types: exclude_types:
- pofile - pofile
- json - json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason # See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.6.2' rev: 'v3.3.3'
hooks: hooks:
- id: prettier - id: prettier
types_or: types_or:
@@ -49,17 +50,17 @@ repos:
- 'prettier-plugin-organize-imports@4.1.0' - 'prettier-plugin-organize-imports@4.1.0'
# Python hooks # Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.2 rev: v0.9.9
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.6.0" rev: "v2.5.1"
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
# Dockerfile hooks # Dockerfile hooks
- repo: https://github.com/AleksaC/hadolint-py - repo: https://github.com/AleksaC/hadolint-py
rev: v2.12.1b3 rev: v2.12.0.3
hooks: hooks:
- id: hadolint - id: hadolint
# Shell script hooks # Shell script hooks
@@ -76,7 +77,7 @@ repos:
hooks: hooks:
- id: shellcheck - id: shellcheck
- repo: https://github.com/google/yamlfmt - repo: https://github.com/google/yamlfmt
rev: v0.17.2 rev: v0.14.0
hooks: hooks:
- id: yamlfmt - id: yamlfmt
exclude: "^src-ui/pnpm-lock.yaml" exclude: "^src-ui/pnpm-lock.yaml"

View File

@@ -141,7 +141,7 @@ The admins occasionally invite contributors directly if we believe having them o
# Automatic Repository Maintenance # Automatic Repository Maintenance
The Paperless-ngx team appreciates all effort and interest from the community in filing bug reports, creating feature requests, sharing ideas and helping other The Paperless-ngx team appreciates all effort and interest from the community in filing bug reports, creating feature requests, sharing ideas and helping other
community members. That said, in an effort to keep the repository organized and manageable the project uses automatic handling of certain areas: community members. That said, in an effort to keep the repository organized and managebale the project uses automatic handling of certain areas:
- Issues that cannot be reproduced will be marked 'stale' after 7 days of inactivity and closed after 14 further days of inactivity. - Issues that cannot be reproduced will be marked 'stale' after 7 days of inactivity and closed after 14 further days of inactivity.
- Issues, pull requests and discussions that are closed will be locked after 30 days of inactivity. - Issues, pull requests and discussions that are closed will be locked after 30 days of inactivity.

View File

@@ -5,7 +5,7 @@
# Purpose: Compiles the frontend # Purpose: Compiles the frontend
# Notes: # Notes:
# - Does PNPM stuff with Typescript and such # - Does PNPM stuff with Typescript and such
FROM --platform=$BUILDPLATFORM docker.io/node:20-trixie-slim AS compile-frontend FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim AS compile-frontend
COPY ./src-ui /src/src-ui COPY ./src-ui /src/src-ui
@@ -21,7 +21,7 @@ ARG PNGX_TAG_VERSION=
RUN set -eux && \ RUN set -eux && \
case "${PNGX_TAG_VERSION}" in \ case "${PNGX_TAG_VERSION}" in \
dev|beta|fix*|feature*) \ dev|beta|fix*|feature*) \
sed -i -E "s/tag: '([a-z\.]+)'/tag: '${PNGX_TAG_VERSION}'/g" /src/src-ui/src/environments/environment.prod.ts \ sed -i -E "s/version: '([0-9\.]+)'/version: '\1 #${PNGX_TAG_VERSION}'/g" /src/src-ui/src/environments/environment.prod.ts \
;; \ ;; \
esac esac
@@ -32,7 +32,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs # Purpose: Installs s6-overlay and rootfs
# Comments: # Comments:
# - Don't leave anything extra in here either # - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.8.4-python3.12-trixie-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.7.9-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6
@@ -170,8 +170,20 @@ RUN set -eux \
&& apt-get update \ && apt-get update \
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \ && apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \
&& echo "Installing pre-built updates" \ && echo "Installing pre-built updates" \
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all \ && 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 \ https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
&& echo "Installing qpdf ${QPDF_VERSION}" \
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
&& echo "Installing Ghostscript ${GS_VERSION}" \
&& 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" \ && echo "Installing jbig2enc" \
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \ && dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
&& echo "Configuring imagemagick" \ && echo "Configuring imagemagick" \
@@ -253,4 +265,4 @@ ENTRYPOINT ["/init"]
EXPOSE 8000 EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=5 CMD [ "curl", "-fs", "-S", "-L", "--max-time", "2", "http://localhost:8000" ] HEALTHCHECK --interval=30s --timeout=10s --retries=5 CMD [ "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000" ]

319
dev.txt
View File

@@ -1,319 +0,0 @@
adduser 3.134
apt 2.6.1
base-files 12.4+deb12u11
base-passwd 3.6.1
bash 5.2.15-2+b8
bsdutils 1:2.38.1-5+deb12u3
ca-certificates 20230311+deb12u1
coreutils 9.1-1
curl 7.88.1-10+deb12u12
dash 0.5.12-2
debconf 1.5.82
debian-archive-keyring 2023.3+deb12u2
debianutils 5.7-0.5~deb12u1
diffutils 1:3.8-4
dirmngr 2.2.40-1.1
dpkg 1.21.22
e2fsprogs 1.47.0-2
file 1:5.44-3
findutils 4.9.0-4
fontconfig 2.14.1-4
fontconfig-config 2.14.1-4
fonts-liberation 1:1.07.4-11
fonts-urw-base35 20200910-7
gcc-12-base 12.2.0-14+deb12u1
gettext 0.21-12
gettext-base 0.21-12
ghostscript 10.03.1~dfsg-1
gnupg 2.2.40-1.1
gnupg-l10n 2.2.40-1.1
gnupg-utils 2.2.40-1.1
gosu 1.14-1+b10
gpg 2.2.40-1.1
gpg-agent 2.2.40-1.1
gpg-wks-client 2.2.40-1.1
gpg-wks-server 2.2.40-1.1
gpgconf 2.2.40-1.1
gpgsm 2.2.40-1.1
gpgv 2.2.40-1.1
grep 3.8-5
gzip 1.12-1
hicolor-icon-theme 0.17-2
hostname 3.23+nmu1
icc-profiles-free 2.0.1+dfsg-1.1
imagemagick 8:6.9.11.60+dfsg-1.6+deb12u3
imagemagick-6-common 8:6.9.11.60+dfsg-1.6+deb12u3
imagemagick-6.q16 8:6.9.11.60+dfsg-1.6+deb12u3
init-system-helpers 1.65.2
jbig2dec 0.19-3
jbig2enc 0.30-1
libacl1 2.3.1-3
libaom3 3.6.0-1+deb12u1
libapt-pkg6.0 2.6.1
libarchive13 3.6.2-1+deb12u2
libassuan0 2.5.5-5
libattr1 1:2.5.1-4
libaudit-common 1:3.0.9-1
libaudit1 1:3.0.9-1
libavahi-client3 0.8-10+deb12u1
libavahi-common-data 0.8-10+deb12u1
libavahi-common3 0.8-10+deb12u1
libavcodec59 7:5.1.6-0+deb12u1
libavformat59 7:5.1.6-0+deb12u1
libavutil57 7:5.1.6-0+deb12u1
libblkid1 2.38.1-5+deb12u3
libbluray2 1:1.3.4-1
libbrotli1 1.0.9-2+b6
libbsd0 0.11.7-2
libbz2-1.0 1.0.8-5+b1
libc-bin 2.36-9+deb12u10
libc6 2.36-9+deb12u10
libcairo-gobject2 1.16.0-7
libcairo2 1.16.0-7
libcap-ng0 0.8.3-1+b3
libcap2 1:2.66-4+deb12u1
libchromaprint1 1.5.1-2+b1
libcjson1 1.7.15-1+deb12u2
libcodec2-1.0 1.0.5-1
libcom-err2 1.47.0-2
libconfig-inifiles-perl 3.000003-2
libcrypt1 1:4.4.33-2
libcups2 2.4.2-3+deb12u8
libcurl4 7.88.1-10+deb12u12
libdatrie1 0.2.13-2+b1
libdav1d6 1.0.0-2+deb12u1
libdb5.3 5.3.28+dfsg2-1
libdbus-1-3 1.14.10-1~deb12u1
libde265-0 1.0.11-1+deb12u2
libdebconfclient0 0.270
libdeflate0 1.14-1
libdrm-common 2.4.114-1
libdrm2 2.4.114-1+b1
libedit2 3.1-20221030-2
libexpat1 2.5.0-1+deb12u1
libext2fs2 1.47.0-2
libffi8 3.4.4-1
libfftw3-double3 3.3.10-1
libfontconfig1 2.14.1-4
libfontenc1 1:1.1.4-1
libfreetype6 2.12.1+dfsg-5+deb12u4
libfribidi0 1.0.8-2.1
libgcc-s1 12.2.0-14+deb12u1
libgcrypt20 1.10.1-3
libgdbm-compat4 1.23-3
libgdbm6 1.23-3
libgdk-pixbuf-2.0-0 2.42.10+dfsg-1+deb12u2
libgdk-pixbuf2.0-common 2.42.10+dfsg-1+deb12u2
libgif7 5.2.1-2.5
libglib2.0-0 2.74.6-2+deb12u6
libgme0 0.6.3-6
libgmp10 2:6.2.1+dfsg1-1.1
libgnutls30 3.7.9-2+deb12u5
libgomp1 12.2.0-14+deb12u1
libgpg-error0 1.46-1
libgraphite2-3 1.3.14-1
libgs-common 10.0.0~dfsg-11+deb12u7
libgs10 10.03.1~dfsg-1
libgs10-common 10.03.1~dfsg-1
libgsm1 1.0.22-1
libgssapi-krb5-2 1.20.1-2+deb12u3
libharfbuzz0b 6.0.0+dfsg-3
libheif1 1.15.1-1+deb12u1
libhogweed6 3.8.1-2
libhwy1 1.0.3-3+deb12u1
libice6 2:1.0.10-1
libicu72 72.1-3+deb12u1
libidn12 1.41-1
libidn2-0 2.3.3-1+b1
libijs-0.35 0.35-15
libimagequant0 2.17.0-1
libjbig0 2.1-6.1
libjbig2dec0 0.19-3
libjpeg62-turbo 1:2.1.5-2
libjxl0.7 0.7.0-10+deb12u1
libk5crypto3 1.20.1-2+deb12u3
libkeyutils1 1.6.3-2
libkrb5-3 1.20.1-2+deb12u3
libkrb5support0 1.20.1-2+deb12u3
libksba8 1.6.3-2
liblcms2-2 2.14-2
libldap-2.5-0 2.5.13+dfsg-5
liblept5 1.82.0-3+b3
liblerc4 4.0.0+ds-2
liblqr-1-0 0.4.2-2.1
libltdl7 2.4.7-7~deb12u1
liblz4-1 1.9.4-1
liblzma5 5.4.1-1
libmagic-mgc 1:5.44-3
libmagic1 1:5.44-3
libmagickcore-6.q16-6 8:6.9.11.60+dfsg-1.6+deb12u3
libmagickwand-6.q16-6 8:6.9.11.60+dfsg-1.6+deb12u3
libmariadb3 1:10.11.11-0+deb12u1
libmbedcrypto7 2.28.3-1
libmd0 1.0.4-2
libmfx1 22.5.4-1
libmount1 2.38.1-5+deb12u3
libmp3lame0 3.100-6
libmpg123-0 1.31.2-1+deb12u1
libncurses6 6.4-4
libncursesw6 6.4-4
libnettle8 3.8.1-2
libnghttp2-14 1.52.0-1+deb12u2
libnorm1 1.5.9+dfsg-2
libnpth0 1.6-3
libnsl2 1.3.0-2
libnspr4 2:4.35-1
libnss3 2:3.87.1-1+deb12u1
libnuma1 2.0.16-1
libogg0 1.3.5-3
libopenjp2-7 2.5.0-2+deb12u1
libopenmpt0 0.6.9-1
libopus0 1.3.1-3
libp11-kit0 0.24.1-2
libpam-modules 1.5.2-6+deb12u1
libpam-modules-bin 1.5.2-6+deb12u1
libpam-runtime 1.5.2-6+deb12u1
libpam0g 1.5.2-6+deb12u1
libpango-1.0-0 1.50.12+ds-1
libpangocairo-1.0-0 1.50.12+ds-1
libpangoft2-1.0-0 1.50.12+ds-1
libpaper1 1.1.29
libpcre2-8-0 10.42-1
libperl5.36 5.36.0-7+deb12u2
libpgm-5.3-0 5.3.128~dfsg-2
libpixman-1-0 0.42.2-1
libpng16-16 1.6.39-2
libpoppler126 22.12.0-2+deb12u1
libpq5 15.13-0+deb12u1
libpsl5 0.21.2-1
libqpdf29 11.9.0-1
librabbitmq4 0.11.0-1+deb12u1
librav1e0 0.5.1-6
libreadline8 8.2-1.3
librist4 0.2.7+dfsg-1
librsvg2-2 2.54.7+dfsg-1~deb12u1
librtmp1 2.4+20151223.gitfa8646d.1-2+b2
libsasl2-2 2.1.28+dfsg-10
libsasl2-modules-db 2.1.28+dfsg-10
libseccomp2 2.5.4-1+deb12u1
libselinux1 3.4-1+b6
libsemanage-common 3.4-1
libsemanage2 3.4-1+b5
libsepol2 3.4-2.1
libshine3 3.1.1-2
libsm6 2:1.2.3-1
libsmartcols1 2.38.1-5+deb12u3
libsnappy1v5 1.1.9-3
libsodium23 1.0.18-1
libsoxr0 0.1.3-4
libspeex1 1.2.1-2
libsqlite3-0 3.40.1-2+deb12u1
libsrt1.5-gnutls 1.5.1-1+deb12u1
libss2 1.47.0-2
libssh-gcrypt-4 0.10.6-0+deb12u1
libssh2-1 1.10.0-3+b1
libssl3 3.0.17-1~deb12u1
libstdc++6 12.2.0-14+deb12u1
libsvtav1enc1 1.4.1+dfsg-1
libswresample4 7:5.1.6-0+deb12u1
libsystemd0 252.38-1~deb12u1
libtasn1-6 4.19.0-2+deb12u1
libtesseract5 5.3.0-2
libthai-data 0.1.29-1
libthai0 0.1.29-1
libtheora0 1.1.1+dfsg.1-16.1+b1
libtiff6 4.5.0-6+deb12u2
libtinfo6 6.4-4
libtirpc-common 1.3.3+ds-1
libtirpc3 1.3.3+ds-1
libtwolame0 0.4.0-2
libudev1 252.38-1~deb12u1
libudfread0 1.1.2-1
libunistring2 1.0-2
libuuid1 2.38.1-5+deb12u3
libv4l-0 1.22.1-5+b2
libv4lconvert0 1.22.1-5+b2
libva-drm2 2.17.0-1
libva-x11-2 2.17.0-1
libva2 2.17.0-1
libvdpau1 1.5-2
libvorbis0a 1.3.7-1
libvorbisenc2 1.3.7-1
libvorbisfile3 1.3.7-1
libvpx7 1.12.0-1+deb12u4
libwebp7 1.2.4-0.2+deb12u1
libwebpdemux2 1.2.4-0.2+deb12u1
libwebpmux3 1.2.4-0.2+deb12u1
libx11-6 2:1.8.4-2+deb12u2
libx11-data 2:1.8.4-2+deb12u2
libx11-xcb1 2:1.8.4-2+deb12u2
libx264-164 2:0.164.3095+gitbaee400-3
libx265-199 3.5-2+b1
libxau6 1:1.0.9-1
libxcb-dri3-0 1.15-1
libxcb-render0 1.15-1
libxcb-shm0 1.15-1
libxcb1 1.15-1
libxdmcp6 1:1.1.2-3
libxext6 2:1.3.4-1+b1
libxfixes3 1:6.0.0-2
libxml2 2.9.14+dfsg-1.3~deb12u2
libxrender1 1:0.9.10-1.1
libxslt1.1 1.1.35-1+deb12u1
libxt6 1:1.2.1-1.1
libxvidcore4 2:1.3.7-1
libxxhash0 0.8.1-1
libzbar0 0.23.92-7+deb12u1
libzmq5 4.3.4-6
libzstd1 1.5.4+dfsg2-5
libzvbi-common 0.2.41-1
libzvbi0 0.2.41-1
login 1:4.13+dfsg1-1+deb12u1
logsave 1.47.0-2
mariadb-client 1:10.11.11-0+deb12u1
mariadb-client-core 1:10.11.11-0+deb12u1
mariadb-common 1:10.11.11-0+deb12u1
mawk 1.3.4.20200120-3.1
media-types 10.0.0
mount 2.38.1-5+deb12u3
mysql-common 5.8+1.1.0
ncurses-base 6.4-4
ncurses-bin 6.4-4
netbase 6.4
ocl-icd-libopencl1 2.3.1-1
openssl 3.0.17-1~deb12u1
passwd 1:4.13+dfsg1-1+deb12u1
perl 5.36.0-7+deb12u2
perl-base 5.36.0-7+deb12u2
perl-modules-5.36 5.36.0-7+deb12u2
pinentry-curses 1.2.1-1
pngquant 2.17.0-1
poppler-data 0.4.12-1
poppler-utils 22.12.0-2+deb12u1
postgresql-client 15+248
postgresql-client-15 15.13-0+deb12u1
postgresql-client-common 248
qpdf 11.9.0-1
readline-common 8.2-1.3
sed 4.9-1
sensible-utils 0.0.17+nmu1
shared-mime-info 2.2-1
sysvinit-utils 3.06-4
tar 1.34+dfsg-1.2+deb12u1
tesseract-ocr 5.3.0-2
tesseract-ocr-deu 1:4.1.0-2
tesseract-ocr-eng 1:4.1.0-2
tesseract-ocr-fra 1:4.1.0-2
tesseract-ocr-ita 1:4.1.0-2
tesseract-ocr-osd 1:4.1.0-2
tesseract-ocr-spa 1:4.1.0-2
tzdata 2025b-0+deb12u1
ucf 3.0043+nmu1+deb12u1
unpaper 7.0.0-0.1
usr-is-merged 37~deb12u1
util-linux 2.38.1-5+deb12u3
util-linux-extra 2.38.1-5+deb12u3
x11-common 1:7.7+23
xfonts-encodings 1:1.0.4-2.2
xfonts-utils 1:7.7+6
zlib1g 1:1.2.13.dfsg-1

View File

@@ -1,7 +1,8 @@
# Docker Compose file for running paperless testing with actual Gotenberg # Docker Compose file for running paperless testing with actual gotenberg
# and Tika containers for a more end to end test of the Tika related functionality # and Tika containers for a more end to end test of the Tika related functionality
# Can be used locally or by the CI to start the necessary containers with the # Can be used locally or by the CI to start the necessary containers with the
# correct networking for the tests # correct networking for the tests
services: services:
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:8.20 image: docker.io/gotenberg/gotenberg:8.20

View File

@@ -32,6 +32,6 @@
# Note that this is different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines # Note that this is different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines
# the language used for OCR. # the language used for OCR.
# The container installs English, German, Italian, Spanish and French by default. # The container installs English, German, Italian, Spanish and French by default.
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names # See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
# for available languages. # for available languages.
#PAPERLESS_OCR_LANGUAGES=tur ces #PAPERLESS_OCR_LANGUAGES=tur ces

View File

@@ -16,8 +16,8 @@
# - Instead of SQLite (default), MariaDB is used as the database server. # - Instead of SQLite (default), MariaDB is used as the database server.
# - Apache Tika and Gotenberg servers are started with paperless and paperless # - Apache Tika and Gotenberg servers are started with paperless and paperless
# is configured to use these services. These provide support for consuming # is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, PowerPoint and their LibreOffice counter- # Office documents (Word, Excel, Power Point and their LibreOffice counter-
# parts). # parts.
# #
# To install and update paperless with this file, do the following: # To install and update paperless with this file, do the following:
# #
@@ -25,9 +25,11 @@
# and '.env' into a folder. # and '.env' into a folder.
# - Run 'docker compose pull'. # - Run 'docker compose pull'.
# - Run 'docker compose up -d'. # - Run 'docker compose up -d'.
# #
# For more extensive installation and update instructions, refer to the # For more extensive installation and update instructions, refer to the
# documentation. # documentation.
services: services:
broker: broker:
image: docker.io/library/redis:8 image: docker.io/library/redis:8

View File

@@ -24,6 +24,7 @@
# #
# For more extensive installation and update instructions, refer to the # For more extensive installation and update instructions, refer to the
# documentation. # documentation.
services: services:
broker: broker:
image: docker.io/library/redis:8 image: docker.io/library/redis:8

View File

@@ -25,6 +25,7 @@
# #
# For more extensive installation and update instructions, refer to the # For more extensive installation and update instructions, refer to the
# documentation. # documentation.
services: services:
broker: broker:
image: docker.io/library/redis:8 image: docker.io/library/redis:8

View File

@@ -16,8 +16,8 @@
# - Instead of SQLite (default), PostgreSQL is used as the database server. # - Instead of SQLite (default), PostgreSQL is used as the database server.
# - Apache Tika and Gotenberg servers are started with paperless and paperless # - Apache Tika and Gotenberg servers are started with paperless and paperless
# is configured to use these services. These provide support for consuming # is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, PowerPoint and their LibreOffice counter- # Office documents (Word, Excel, Power Point and their LibreOffice counter-
# parts). # parts.
# #
# To install and update paperless with this file, do the following: # To install and update paperless with this file, do the following:
# #
@@ -28,6 +28,7 @@
# #
# For more extensive installation and update instructions, refer to the # For more extensive installation and update instructions, refer to the
# documentation. # documentation.
services: services:
broker: broker:
image: docker.io/library/redis:8 image: docker.io/library/redis:8

View File

@@ -24,6 +24,7 @@
# #
# For more extensive installation and update instructions, refer to the # For more extensive installation and update instructions, refer to the
# documentation. # documentation.
services: services:
broker: broker:
image: docker.io/library/redis:8 image: docker.io/library/redis:8

View File

@@ -16,8 +16,8 @@
# #
# - Apache Tika and Gotenberg servers are started with paperless and paperless # - Apache Tika and Gotenberg servers are started with paperless and paperless
# is configured to use these services. These provide support for consuming # is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, PowerPoint and their LibreOffice counter- # Office documents (Word, Excel, Power Point and their LibreOffice counter-
# parts). # parts.
# #
# To install and update paperless with this file, do the following: # To install and update paperless with this file, do the following:
# #
@@ -28,6 +28,7 @@
# #
# For more extensive installation and update instructions, refer to the # For more extensive installation and update instructions, refer to the
# documentation. # documentation.
services: services:
broker: broker:
image: docker.io/library/redis:8 image: docker.io/library/redis:8

View File

@@ -21,6 +21,7 @@
# #
# For more extensive installation and update instructions, refer to the # For more extensive installation and update instructions, refer to the
# documentation. # documentation.
services: services:
broker: broker:
image: docker.io/library/redis:8 image: docker.io/library/redis:8

View File

@@ -306,7 +306,7 @@ in dedicated folders according to their nature: `archive`, `originals`,
If `-sm` or `--split-manifest` is provided, information about document If `-sm` or `--split-manifest` is provided, information about document
will be placed in individual json files, instead of a single JSON file. The main will be placed in individual json files, instead of a single JSON file. The main
manifest.json will still contain application wide information (e.g. tags, correspondent, manifest.json will still contain application wide information (e.g. tags, correspondent,
document type, etc) documenttype, etc)
If `-z` or `--zip` is provided, the export will be a zip file If `-z` or `--zip` is provided, the export will be a zip file
in the target directory, named according to the current local date or the in the target directory, named according to the current local date or the
@@ -457,22 +457,6 @@ of the index and usually makes queries faster and also ensures that the
autocompletion works properly. This command is regularly invoked by the autocompletion works properly. This command is regularly invoked by the
task scheduler. task scheduler.
### Clearing the database read cache
If the database read cache is enabled, **you must run this command** after making any changes to the database outside the application context.
This includes operations such as restoring a database backup or executing SQL statements like UPDATE, INSERT, DELETE, ALTER, CREATE, or DROP.
Failing to invalidate the cache after such modifications can lead to stale data being served from the cache, and **may cause data corruption** or inconsistent behavior in the application.
Use the following management command to clear the cache:
```
invalidate_cachalot
```
!!! info
The database read cache is based on Django-Cachalot. You can refer to their [documentation](https://django-cachalot.readthedocs.io/en/latest/quickstart.html#manage-py-command).
### Managing filenames {#renamer} ### Managing filenames {#renamer}
If you use paperless' feature to If you use paperless' feature to

View File

@@ -282,18 +282,6 @@ The following methods are supported:
- `"merge": true or false` (defaults to false) - `"merge": true or false` (defaults to false)
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including - The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
removing them) or be merged with existing permissions. removing them) or be merged with existing permissions.
- `edit_pdf`
- Requires `parameters`:
- `"doc_ids": [DOCUMENT_ID]` A list of a single document ID to edit.
- `"operations": [OPERATION, ...]` A list of operations to perform on the documents. Each operation is a dictionary
with the following keys:
- `"page": PAGE_NUMBER` The page number to edit (1-based).
- `"rotate": DEGREES` Optional rotation in degrees (90, 180, 270).
- `"doc": OUTPUT_DOCUMENT_INDEX` Optional index of the output document for split operations.
- Optional `parameters`:
- `"delete_original": true` to delete the original documents after editing.
- `"update_document": true` to update the existing document with the edited PDF.
- `"include_metadata": true` to copy metadata from the original document to the edited document.
- `merge` - `merge`
- No additional `parameters` required. - No additional `parameters` required.
- The ordering of the merged document is determined by the list of IDs. - The ordering of the merged document is determined by the list of IDs.

View File

@@ -1,64 +1,5 @@
# Changelog # Changelog
## paperless-ngx 2.17.1
### Bug Fixes
- Fix: correct PAPERLESS_EMPTY_TRASH_DIR to Path [@shamoon](https://github.com/shamoon) ([#10227](https://github.com/paperless-ngx/paperless-ngx/pull/10227))
### All App Changes
- Fix: correct PAPERLESS_EMPTY_TRASH_DIR to Path [@shamoon](https://github.com/shamoon) ([#10227](https://github.com/paperless-ngx/paperless-ngx/pull/10227))
## paperless-ngx 2.17.0
### Breaking Changes
- Fix: restore expected pre-2.16 scheduled workflow offset behavior [@shamoon](https://github.com/shamoon) ([#10218](https://github.com/paperless-ngx/paperless-ngx/pull/10218))
### Features / Enhancements
- QoL: log version at startup, show backend vs frontend mismatch in system status [@shamoon](https://github.com/shamoon) ([#10214](https://github.com/paperless-ngx/paperless-ngx/pull/10214))
- Feature: add Persian translation [@shamoon](https://github.com/shamoon) ([#10183](https://github.com/paperless-ngx/paperless-ngx/pull/10183))
- Enhancement: support import of zipped export [@kaerbr](https://github.com/kaerbr) ([#10073](https://github.com/paperless-ngx/paperless-ngx/pull/10073))
### Bug Fixes
- Fix: more api fixes [@shamoon](https://github.com/shamoon) ([#10204](https://github.com/paperless-ngx/paperless-ngx/pull/10204))
- Fix: restore expected pre-2.16 scheduled workflow offset behavior [@shamoon](https://github.com/shamoon) ([#10218](https://github.com/paperless-ngx/paperless-ngx/pull/10218))
- Fix: fix some API crashes [@shamoon](https://github.com/shamoon) ([#10196](https://github.com/paperless-ngx/paperless-ngx/pull/10196))
- Fix: remove duplicate base path in websocket urls [@robertmx](https://github.com/robertmx) ([#10194](https://github.com/paperless-ngx/paperless-ngx/pull/10194))
- Fix: use hard delete for custom fields with workflow removal [@shamoon](https://github.com/shamoon) ([#10191](https://github.com/paperless-ngx/paperless-ngx/pull/10191))
- Fix: fix mail account test api schema [@shamoon](https://github.com/shamoon) ([#10164](https://github.com/paperless-ngx/paperless-ngx/pull/10164))
- Fix: correct api schema for mail_account process [@shamoon](https://github.com/shamoon) ([#10157](https://github.com/paperless-ngx/paperless-ngx/pull/10157))
- Fix: correct api schema for next_asn [@shamoon](https://github.com/shamoon) ([#10151](https://github.com/paperless-ngx/paperless-ngx/pull/10151))
- Fix: fix email and notes endpoints api spec [@shamoon](https://github.com/shamoon) ([#10148](https://github.com/paperless-ngx/paperless-ngx/pull/10148))
### Dependencies
- Chore: bump angular/common to 19.12.14 [@shamoon](https://github.com/shamoon) ([#10212](https://github.com/paperless-ngx/paperless-ngx/pull/10212))
### All App Changes
<details>
<summary>14 changes</summary>
- QoL: log version at startup, show backend vs frontend mismatch in system status [@shamoon](https://github.com/shamoon) ([#10214](https://github.com/paperless-ngx/paperless-ngx/pull/10214))
- Fix: more api fixes [@shamoon](https://github.com/shamoon) ([#10204](https://github.com/paperless-ngx/paperless-ngx/pull/10204))
- Fix: restore expected pre-2.16 scheduled workflow offset behavior [@shamoon](https://github.com/shamoon) ([#10218](https://github.com/paperless-ngx/paperless-ngx/pull/10218))
- Chore: switch from os.path to pathlib.Path [@gothicVI](https://github.com/gothicVI) ([#9933](https://github.com/paperless-ngx/paperless-ngx/pull/9933))
- Chore: bump angular/common to 19.12.14 [@shamoon](https://github.com/shamoon) ([#10212](https://github.com/paperless-ngx/paperless-ngx/pull/10212))
- Fix: fix some API crashes [@shamoon](https://github.com/shamoon) ([#10196](https://github.com/paperless-ngx/paperless-ngx/pull/10196))
- Fix: remove duplicate base path in websocket urls [@robertmx](https://github.com/robertmx) ([#10194](https://github.com/paperless-ngx/paperless-ngx/pull/10194))
- Fix: use hard delete for custom fields with workflow removal [@shamoon](https://github.com/shamoon) ([#10191](https://github.com/paperless-ngx/paperless-ngx/pull/10191))
- Feature: add Persian translation [@shamoon](https://github.com/shamoon) ([#10183](https://github.com/paperless-ngx/paperless-ngx/pull/10183))
- Enhancement: support import of zipped export [@kaerbr](https://github.com/kaerbr) ([#10073](https://github.com/paperless-ngx/paperless-ngx/pull/10073))
- Fix: fix mail account test api schema [@shamoon](https://github.com/shamoon) ([#10164](https://github.com/paperless-ngx/paperless-ngx/pull/10164))
- Fix: correct api schema for mail_account process [@shamoon](https://github.com/shamoon) ([#10157](https://github.com/paperless-ngx/paperless-ngx/pull/10157))
- Fix: correct api schema for next_asn [@shamoon](https://github.com/shamoon) ([#10151](https://github.com/paperless-ngx/paperless-ngx/pull/10151))
- Fix: fix email and notes endpoints api spec [@shamoon](https://github.com/shamoon) ([#10148](https://github.com/paperless-ngx/paperless-ngx/pull/10148))
</details>
## paperless-ngx 2.16.3 ## paperless-ngx 2.16.3
### Features / Enhancements ### Features / Enhancements
@@ -6007,6 +5948,7 @@ primarily.
a very good job at ocr'ing a document with the default a very good job at ocr'ing a document with the default
language. Certain language specifics such as umlauts may not get language. Certain language specifics such as umlauts may not get
picked up properly. picked up properly.
- `PAPERLESS_DEBUG` defaults to `false`.
- The presence of `PAPERLESS_DBHOST` now determines whether to use - The presence of `PAPERLESS_DBHOST` now determines whether to use
PostgreSQL or SQLite. PostgreSQL or SQLite.
- `PAPERLESS_OCR_THREADS` is gone and replaced with - `PAPERLESS_OCR_THREADS` is gone and replaced with

View File

@@ -159,58 +159,6 @@ Available options are `postgresql` and `mariadb`.
Defaults to unset, which uses Djangos built-in defaults. Defaults to unset, which uses Djangos built-in defaults.
#### [`PAPERLESS_DB_POOLSIZE=<int>`](#PAPERLESS_DB_POOLSIZE) {#PAPERLESS_DB_POOLSIZE}
: Defines the maximum number of database connections to keep in the pool.
Only applies to PostgreSQL. This setting is ignored for other database engines.
The value must be greater than or equal to 1 to be used.
Defaults to unset, which disables connection pooling.
!!! note
A small pool is typically sufficient — for example, a size of 4.
Make sure your PostgreSQL server's max_connections setting is large enough to handle:
```(Paperless workers + Celery workers) × pool size + safety margin```
For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4:
(4 + 2) × 4 + 10 = 34 connections required.
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
: Caches the database read query results into Redis. This can significantly improve application response times by caching database queries, at the cost of slightly increased memory usage.
Defaults to `false`.
!!! danger
**Do not modify the database outside the application while it is running.**
This includes actions such as restoring a backup, upgrading the database, or performing manual inserts. All external modifications must be done **only when the application is stopped**.
After making any such changes, you **must invalidate the DB read cache** using the `invalidate_cachalot` management command.
#### [`PAPERLESS_READ_CACHE_TTL=<int>`](#PAPERLESS_READ_CACHE_TTL) {#PAPERLESS_READ_CACHE_TTL}
: Specifies how long (in seconds) read data should be cached.
Allowed values are between `1` (one second) and `31536000` (one year). Defaults to `3600` (one hour).
!!! warning
A high TTL increases memory usage over time. Memory may be used until end of TTL, even if the cache is invalidated with the `invalidate_cachalot` command.
In case of an out-of-memory (OOM) situation, Redis may stop accepting new data — including cache entries, scheduled tasks, and documents to consume.
If your system has limited RAM, consider configuring a dedicated Redis instance for the read cache, with a memory limit and the eviction policy set to `allkeys-lru`.
For more details, refer to the [Redis eviction policy documentation](https://redis.io/docs/latest/develop/reference/eviction/), and see the `PAPERLESS_READ_CACHE_REDIS_URL` setting to specify a separate Redis broker.
#### [`PAPERLESS_READ_CACHE_REDIS_URL=<url>`](#PAPERLESS_READ_CACHE_REDIS_URL) {#PAPERLESS_READ_CACHE_REDIS_URL}
: Defines the Redis instance used for the read cache.
Defaults to `None`.
!!! Note
If this value is not set, the same Redis instance used for scheduled tasks will be used for caching as well.
## Optional Services ## Optional Services
### Tika {#tika} ### Tika {#tika}
@@ -1020,22 +968,6 @@ still perform some basic text pre-processing before matching.
Defaults to 1. Defaults to 1.
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
Specifies which language Paperless should use when parsing dates from documents.
This should be a language code supported by the dateparser library,
for example: "en", or a combination such as "en+de".
Locales are also supported (e.g., "en-AU").
Multiple languages can be combined using "+", for example: "en+de" or "en-AU+de".
For valid values, refer to the list of supported languages and locales in the [dateparser documentation](https://dateparser.readthedocs.io/en/latest/supported_locales.html).
Set this to match the languages in which most of your documents are written.
If not set, Paperless will attempt to infer the language(s) from the OCR configuration (`PAPERLESS_OCR_LANGUAGE`).
!!! note
This format differs from the `PAPERLESS_OCR_LANGUAGE` setting, which uses ISO 639-2 codes (3 letters, e.g., "eng+deu" for Tesseract OCR).
#### [`PAPERLESS_EMAIL_TASK_CRON=<cron expression>`](#PAPERLESS_EMAIL_TASK_CRON) {#PAPERLESS_EMAIL_TASK_CRON} #### [`PAPERLESS_EMAIL_TASK_CRON=<cron expression>`](#PAPERLESS_EMAIL_TASK_CRON) {#PAPERLESS_EMAIL_TASK_CRON}
: Configures the scheduled email fetching frequency. The value : Configures the scheduled email fetching frequency. The value
@@ -1282,30 +1214,6 @@ within your documents.
Defaults to false. Defaults to false.
## Workflow webhooks
#### [`PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES=<str>`](#PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES) {#PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES}
: A comma-separated list of allowed schemes for webhooks. This setting
controls which URL schemes are permitted for webhook URLs.
Defaults to `http,https`.
#### [`PAPERLESS_WEBHOOKS_ALLOWED_PORTS=<str>`](#PAPERLESS_WEBHOOKS_ALLOWED_PORTS) {#PAPERLESS_WEBHOOKS_ALLOWED_PORTS}
: A comma-separated list of allowed ports for webhooks. This setting
controls which ports are permitted for webhook URLs. For example, if you
set this to `80,443`, webhooks will only be sent to URLs that use these
ports.
Defaults to empty list, which allows all ports.
#### [`PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS=<bool>`](#PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS) {#PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS}
: If set to false, webhooks cannot be sent to internal URLs (e.g., localhost).
Defaults to true, which allows internal requests.
### Polling {#polling} ### Polling {#polling}
#### [`PAPERLESS_CONSUMER_POLLING=<num>`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING} #### [`PAPERLESS_CONSUMER_POLLING=<num>`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING}

View File

@@ -95,13 +95,13 @@ first-time setup.
7. You can now either ... 7. You can now either ...
- install Redis or - install redis or
- use the included `scripts/start_services.sh` to use Docker to fire - use the included `scripts/start_services.sh` to use docker to fire
up a Redis instance (and some other services such as Tika, up a redis instance (and some other services such as tika,
Gotenberg and a database server) or gotenberg and a database server) or
- spin up a bare Redis container - spin up a bare redis container
``` ```
docker run -d -p 6379:6379 --restart unless-stopped redis:latest docker run -d -p 6379:6379 --restart unless-stopped redis:latest
@@ -147,7 +147,7 @@ $ ng build --configuration production
### Testing ### Testing
- Run `pytest` in the `src/` directory to execute all tests. This also - Run `pytest` in the `src/` directory to execute all tests. This also
generates a HTML coverage report. When running tests, `paperless.conf` generates a HTML coverage report. When runnings test, `paperless.conf`
is loaded as well. However, the tests rely on the default is loaded as well. However, the tests rely on the default
configuration. This is not ideal. But for now, make sure no settings configuration. This is not ideal. But for now, make sure no settings
except for DEBUG are overridden when testing. except for DEBUG are overridden when testing.

View File

@@ -30,7 +30,7 @@ physical documents into a searchable online archive so you can keep, well, _less
- Utilizes the open-source Tesseract engine to recognize more than 100 languages. - Utilizes the open-source Tesseract engine to recognize more than 100 languages.
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals. - Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
- Uses machine-learning to automatically add tags, correspondents and document types to your documents. - Uses machine-learning to automatically add tags, correspondents and document types to your documents.
- Supports PDF documents, images, plain text files, Office documents (Word, Excel, PowerPoint, and LibreOffice equivalents)[^1] and more. - Supports PDF documents, images, plain text files, Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents)[^1] and more.
- Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents. - Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents.
- **Beautiful, modern web application** that features: - **Beautiful, modern web application** that features:
- Customizable dashboard with statistics. - Customizable dashboard with statistics.

View File

@@ -445,7 +445,7 @@ are released, dependency support is confirmed, etc.
13. Configure ImageMagick to allow processing of PDF documents. Most 13. Configure ImageMagick to allow processing of PDF documents. Most
distributions have this disabled by default, since PDF documents can distributions have this disabled by default, since PDF documents can
contain malware. If you don't do this, paperless will fall back to contain malware. If you don't do this, paperless will fall back to
Ghostscript for certain steps such as thumbnail generation. ghostscript for certain steps such as thumbnail generation.
Edit `/etc/ImageMagick-6/policy.xml` and adjust Edit `/etc/ImageMagick-6/policy.xml` and adjust

View File

@@ -335,7 +335,7 @@ You may see errors when deleting documents like:
Data too long for column 'transaction_id' at row 1 Data too long for column 'transaction_id' at row 1
``` ```
This error can occur in installations which have upgraded from a version of Paperless-ngx that used Django 4 (Paperless-ngx versions prior to v2.13.0) with a MariaDB/MySQL database. Due to the backwards-incompatible change in Django 5, the column "documents_document.transaction_id" will need to be re-created, which can be done with a one-time run of the following management command: This error can occur in installations which have upgraded from a version of Paperless-ngx that used Django 4 (Paperless-ngx versions prior to v2.13.0) with a MariaDB/MySQL database. Due to the backawards-incompatible change in Django 5, the column "documents_document.transaction_id" will need to be re-created, which can be done with a one-time run of the following management command:
```shell-session ```shell-session
$ python3 manage.py convert_mariadb_uuid $ python3 manage.py convert_mariadb_uuid

View File

@@ -30,9 +30,6 @@ Each document has data fields that you can assign to them:
- A _document type_ is used to demarcate the type of a document such - A _document type_ is used to demarcate the type of a document such
as letter, bank statement, invoice, contract, etc. It is used to as letter, bank statement, invoice, contract, etc. It is used to
identify what a document is about. identify what a document is about.
- The document _storage path_ is the location where the document files
are stored. See [Storage Paths](advanced_usage.md#storage-paths) for
more information.
- The _date added_ of a document is the date the document was scanned - The _date added_ of a document is the date the document was scanned
into paperless. You cannot and should not change this date. into paperless. You cannot and should not change this date.
- The _date created_ of a document is the date the document was - The _date created_ of a document is the date the document was
@@ -411,7 +408,7 @@ Currently, there are three events that correspond to workflow trigger 'types':
tags, doc type, or correspondent. tags, doc type, or correspondent.
4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document 4. **Scheduled**: a scheduled trigger that can be used to run workflows at a specific time. The date used can be either the document
added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive
offsets will trigger after the date, negative offsets will trigger before). offsets will trigger before the date, negative offsets will trigger after).
The following flow diagram illustrates the three document trigger types: The following flow diagram illustrates the three document trigger types:
@@ -499,10 +496,6 @@ The following workflow action types are available:
- Encoding for the request body, either JSON or form data - Encoding for the request body, either JSON or form data
- The request headers as key-value pairs - The request headers as key-value pairs
For security reasons, webhooks can be limited to specific ports and disallowed from connecting to local URLs. See the relevant
[configuration settings](configuration.md#workflow-webhooks) to change this behavior. If you are allowing non-admins to create workflows,
you may want to adjust these settings to prevent abuse.
#### Workflow placeholders #### Workflow placeholders
Some workflow text can include placeholders but the available options differ depending on the type of Some workflow text can include placeholders but the available options differ depending on the type of
@@ -580,14 +573,12 @@ The following custom field types are supported:
## PDF Actions ## PDF Actions
Paperless-ngx supports basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files). When viewing an individual document you can Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):
open the 'PDF Editor' to use a simple UI for re-arranging, rotating, deleting pages and splitting documents.
- Merging documents: available when selecting multiple documents for 'bulk editing'. - Merging documents: available when selecting multiple documents for 'bulk editing'.
- Rotating documents: available when selecting multiple documents for 'bulk editing' and via the pdf editor on an individual document's details page. - Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
- Splitting documents: via the pdf editor on an individual document's details page. - Splitting documents: available from an individual document's details page.
- Deleting pages: via the pdf editor on an individual document's details page. - Deleting pages: available from an individual document's details page.
- Re-arranging pages: via the pdf editor on an individual document's details page.
!!! important !!! important

View File

@@ -52,12 +52,12 @@ if ! command -v wget &> /dev/null ; then
fi fi
if ! command -v docker &> /dev/null ; then if ! command -v docker &> /dev/null ; then
echo "docker executable not found. Is Docker installed?" echo "docker executable not found. Is docker installed?"
exit 1 exit 1
fi fi
if ! docker compose &> /dev/null ; then if ! docker compose &> /dev/null ; then
echo "docker compose plugin not found. Is Docker Compose installed?" echo "docker compose plugin not found. Is docker compose installed?"
exit 1 exit 1
fi fi
@@ -66,7 +66,7 @@ fi
if ! docker stats --no-stream &> /dev/null ; then if ! docker stats --no-stream &> /dev/null ; then
echo "" echo ""
echo "WARN: It look like the current user does not have Docker permissions." echo "WARN: It look like the current user does not have Docker permissions."
echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user (may require restarting the shell)." echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user (may require restarting shell)."
echo "" echo ""
sleep 3 sleep 3
fi fi
@@ -135,7 +135,7 @@ DATABASE_BACKEND=$ask_result
echo "" echo ""
echo "Paperless is able to use Apache Tika to support Office documents such as" echo "Paperless is able to use Apache Tika to support Office documents such as"
echo "Word, Excel, PowerPoint, and LibreOffice equivalents. This feature" echo "Word, Excel, Powerpoint, and Libreoffice equivalents. This feature"
echo "requires more resources due to the required services." echo "requires more resources due to the required services."
echo "" echo ""
@@ -157,7 +157,7 @@ echo ""
echo "Specify the user id and group id you wish to run paperless as." echo "Specify the user id and group id you wish to run paperless as."
echo "Paperless will also change ownership on the data, media and consume" echo "Paperless will also change ownership on the data, media and consume"
echo "folder to the specified values, so it's a good idea to supply the user id" echo "folder to the specified values, so it's a good idea to supply the user id"
echo "and group id of your Unix user account." echo "and group id of your unix user account."
echo "If unsure, leave default." echo "If unsure, leave default."
echo "" echo ""
@@ -212,7 +212,7 @@ if [[ "$DATABASE_BACKEND" == "sqlite" ]] ; then
echo -n "SQLite database, the " echo -n "SQLite database, the "
fi fi
echo "search index and other data." echo "search index and other data."
echo "As with the media folder, leave empty to have this managed by Docker." echo "As with the media folder, leave empty to have this managed by docker."
echo "" echo ""
echo "CAUTION: If specified, you must specify an absolute path starting with /" echo "CAUTION: If specified, you must specify an absolute path starting with /"
echo "or a relative path starting with ./ here." echo "or a relative path starting with ./ here."
@@ -224,7 +224,7 @@ DATA_FOLDER=$ask_result
if [[ "$DATABASE_BACKEND" == "postgres" || "$DATABASE_BACKEND" == "mariadb" ]] ; then if [[ "$DATABASE_BACKEND" == "postgres" || "$DATABASE_BACKEND" == "mariadb" ]] ; then
echo "" echo ""
echo "The database folder, where your database stores its data." echo "The database folder, where your database stores its data."
echo "Leave empty to have this managed by Docker." echo "Leave empty to have this managed by docker."
echo "" echo ""
echo "CAUTION: If specified, you must specify an absolute path starting with /" echo "CAUTION: If specified, you must specify an absolute path starting with /"
echo "or a relative path starting with ./ here." echo "or a relative path starting with ./ here."
@@ -276,18 +276,18 @@ echo ""
echo "Target folder: $TARGET_FOLDER" echo "Target folder: $TARGET_FOLDER"
echo "Consume folder: $CONSUME_FOLDER" echo "Consume folder: $CONSUME_FOLDER"
if [[ -z $MEDIA_FOLDER ]] ; then if [[ -z $MEDIA_FOLDER ]] ; then
echo "Media folder: Managed by Docker" echo "Media folder: Managed by docker"
else else
echo "Media folder: $MEDIA_FOLDER" echo "Media folder: $MEDIA_FOLDER"
fi fi
if [[ -z $DATA_FOLDER ]] ; then if [[ -z $DATA_FOLDER ]] ; then
echo "Data folder: Managed by Docker" echo "Data folder: Managed by docker"
else else
echo "Data folder: $DATA_FOLDER" echo "Data folder: $DATA_FOLDER"
fi fi
if [[ "$DATABASE_BACKEND" == "postgres" || "$DATABASE_BACKEND" == "mariadb" ]] ; then if [[ "$DATABASE_BACKEND" == "postgres" || "$DATABASE_BACKEND" == "mariadb" ]] ; then
if [[ -z $DATABASE_FOLDER ]] ; then if [[ -z $DATABASE_FOLDER ]] ; then
echo "Database folder: Managed by Docker" echo "Database folder: Managed by docker"
else else
echo "Database folder: $DATABASE_FOLDER" echo "Database folder: $DATABASE_FOLDER"
fi fi

View File

@@ -1,6 +1,10 @@
# Have a look at the docs for documentation. # Have a look at the docs for documentation.
# https://docs.paperless-ngx.com/configuration/ # https://docs.paperless-ngx.com/configuration/
# Debug. Only enable this for development.
#PAPERLESS_DEBUG=false
# Required services # Required services
#PAPERLESS_REDIS=redis://localhost:6379 #PAPERLESS_REDIS=redis://localhost:6379

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "paperless-ngx" name = "paperless-ngx"
version = "2.17.1" version = "2.16.3"
description = "A community-supported supercharged version of paperless: scan, index and archive all your physical documents" description = "A community-supported supercharged version of paperless: scan, index and archive all your physical documents"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -26,7 +26,6 @@ dependencies = [
"django~=5.1.7", "django~=5.1.7",
"django-allauth[socialaccount,mfa]~=65.4.0", "django-allauth[socialaccount,mfa]~=65.4.0",
"django-auditlog~=3.1.2", "django-auditlog~=3.1.2",
"django-cachalot~=2.8.0",
"django-celery-results~=2.6.0", "django-celery-results~=2.6.0",
"django-compression-middleware~=0.5.0", "django-compression-middleware~=0.5.0",
"django-cors-headers~=4.7.0", "django-cors-headers~=4.7.0",
@@ -44,15 +43,14 @@ dependencies = [
"flower~=2.0.1", "flower~=2.0.1",
"gotenberg-client~=0.10.0", "gotenberg-client~=0.10.0",
"httpx-oauth~=0.16", "httpx-oauth~=0.16",
"imap-tools~=1.11.0", "imap-tools~=1.10.0",
"inotifyrecursive~=0.3", "inotifyrecursive~=0.3",
"jinja2~=3.1.5", "jinja2~=3.1.5",
"langdetect~=1.0.9", "langdetect~=1.0.9",
"nltk~=3.9.1", "nltk~=3.9.1",
"ocrmypdf~=16.10.0", "ocrmypdf~=16.10.0",
"pathvalidate~=3.3.1", "pathvalidate~=3.2.3",
"pdf2image~=1.17.0", "pdf2image~=1.17.0",
"psycopg-pool",
"python-dateutil~=2.9.0", "python-dateutil~=2.9.0",
"python-dotenv~=1.1.0", "python-dotenv~=1.1.0",
"python-gnupg~=0.5.4", "python-gnupg~=0.5.4",
@@ -61,9 +59,9 @@ dependencies = [
"pyzbar~=0.1.9", "pyzbar~=0.1.9",
"rapidfuzz~=3.13.0", "rapidfuzz~=3.13.0",
"redis[hiredis]~=5.2.1", "redis[hiredis]~=5.2.1",
"scikit-learn~=1.7.0", "scikit-learn~=1.6.1",
"setproctitle~=1.3.4", "setproctitle~=1.3.4",
"tika-client~=0.10.0", "tika-client~=0.9.0",
"tqdm~=4.67.1", "tqdm~=4.67.1",
"watchdog~=6.0", "watchdog~=6.0",
"whitenoise~=6.9", "whitenoise~=6.9",
@@ -75,13 +73,12 @@ optional-dependencies.mariadb = [
"mysqlclient~=2.2.7", "mysqlclient~=2.2.7",
] ]
optional-dependencies.postgres = [ optional-dependencies.postgres = [
"psycopg[c,pool]==3.2.9", "psycopg[c]==3.2.5",
# Direct dependency for proper resolution of the pre-built wheels # Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.2.9", "psycopg-c==3.2.5",
"psycopg-pool==3.2.6",
] ]
optional-dependencies.webserver = [ optional-dependencies.webserver = [
"granian[uvloop]~=2.4.1", "granian[uvloop]~=2.3.2",
] ]
[dependency-groups] [dependency-groups]
@@ -101,8 +98,8 @@ testing = [
"daphne", "daphne",
"factory-boy~=3.3.1", "factory-boy~=3.3.1",
"imagehash", "imagehash",
"pytest~=8.4.1", "pytest~=8.3.3",
"pytest-cov~=6.2.1", "pytest-cov~=6.0.0",
"pytest-django~=4.10.0", "pytest-django~=4.10.0",
"pytest-env", "pytest-env",
"pytest-httpx", "pytest-httpx",
@@ -113,9 +110,9 @@ testing = [
] ]
lint = [ lint = [
"pre-commit~=4.2.0", "pre-commit~=4.1.0",
"pre-commit-uv~=4.1.3", "pre-commit-uv~=4.1.3",
"ruff~=0.12.2", "ruff~=0.9.9",
] ]
typing = [ typing = [
@@ -175,7 +172,6 @@ lint.extend-select = [
] ]
lint.ignore = [ lint.ignore = [
"DJ001", "DJ001",
"PLC0415",
"RUF012", "RUF012",
"SIM105", "SIM105",
] ]
@@ -204,9 +200,15 @@ lint.per-file-ignores."docker/wait-for-redis.py" = [
"INP001", "INP001",
"T201", "T201",
] ]
lint.per-file-ignores."src/documents/file_handling.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/management/commands/document_consumer.py" = [ lint.per-file-ignores."src/documents/management/commands/document_consumer.py" = [
"PTH", "PTH",
] # TODO Enable & remove ] # TODO Enable & remove
lint.per-file-ignores."src/documents/management/commands/document_exporter.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/migrations/1012_fix_archive_files.py" = [ lint.per-file-ignores."src/documents/migrations/1012_fix_archive_files.py" = [
"PTH", "PTH",
] # TODO Enable & remove ] # TODO Enable & remove
@@ -216,9 +218,25 @@ lint.per-file-ignores."src/documents/models.py" = [
lint.per-file-ignores."src/documents/parsers.py" = [ lint.per-file-ignores."src/documents/parsers.py" = [
"PTH", "PTH",
] # TODO Enable & remove ] # TODO Enable & remove
lint.per-file-ignores."src/documents/signals/handlers.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/views.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/paperless/checks.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/paperless/settings.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/paperless_mail/mail.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [ lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
"PTH",
"RUF001", "RUF001",
] ] # TODO PTH Enable & remove
lint.isort.force-single-line = true lint.isort.force-single-line = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
@@ -232,7 +250,6 @@ testpaths = [
"src/paperless_mail/tests/", "src/paperless_mail/tests/",
"src/paperless_tesseract/tests/", "src/paperless_tesseract/tests/",
"src/paperless_tika/tests", "src/paperless_tika/tests",
"src/paperless_text/tests/",
] ]
addopts = [ addopts = [
"--pythonwarnings=all", "--pythonwarnings=all",
@@ -296,8 +313,8 @@ environments = [
[tool.uv.sources] [tool.uv.sources]
# Markers are chosen to select these almost exclusively when building the Docker image # Markers are chosen to select these almost exclusively when building the Docker image
psycopg-c = [ psycopg-c = [
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" }, { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.5/psycopg_c-3.2.5-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" }, { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.5/psycopg_c-3.2.5-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
] ]
zxing-cpp = [ zxing-cpp = [
{ url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" }, { url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },

View File

@@ -48,7 +48,6 @@
"sv-SE": "src/locale/messages.sv_SE.xlf", "sv-SE": "src/locale/messages.sv_SE.xlf",
"tr-TR": "src/locale/messages.tr_TR.xlf", "tr-TR": "src/locale/messages.tr_TR.xlf",
"uk-UA": "src/locale/messages.uk_UA.xlf", "uk-UA": "src/locale/messages.uk_UA.xlf",
"vi-VN": "src/locale/messages.vi_VN.xlf",
"zh-CN": "src/locale/messages.zh_CN.xlf", "zh-CN": "src/locale/messages.zh_CN.xlf",
"zh-TW": "src/locale/messages.zh_TW.xlf" "zh-TW": "src/locale/messages.zh_TW.xlf"
} }
@@ -61,12 +60,10 @@
"path": "./extra-webpack.config.ts" "path": "./extra-webpack.config.ts"
}, },
"outputPath": "dist/paperless-ui", "outputPath": "dist/paperless-ui",
"main": "src/main.ts",
"outputHashing": "none", "outputHashing": "none",
"index": "src/index.html", "index": "src/index.html",
"polyfills": [ "main": "src/main.ts",
"src/polyfills.ts" "polyfills": "src/polyfills.ts",
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"localize": true, "localize": true,
"assets": [ "assets": [
@@ -89,15 +86,12 @@
"file-saver", "file-saver",
"utif" "utif"
], ],
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true, "sourceMap": true,
"optimization": false, "optimization": false,
"namedChunks": true, "namedChunks": true
"stylePreprocessorOptions": {
"includePaths": [
"."
]
}
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -113,6 +107,8 @@
"sourceMap": false, "sourceMap": false,
"namedChunks": false, "namedChunks": false,
"extractLicenses": true, "extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
@@ -192,30 +188,6 @@
}, },
"@angular-eslint/schematics:library": { "@angular-eslint/schematics:library": {
"setParserOptionsProject": true "setParserOptionsProject": true
},
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "paperless-ngx-ui", "name": "paperless-ngx-ui",
"version": "2.17.1", "version": "2.16.3",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"ng": "ng", "ng": "ng",
@@ -11,28 +11,28 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^20.1.4", "@angular/cdk": "^19.2.14",
"@angular/common": "~20.1.4", "@angular/common": "~19.2.14",
"@angular/compiler": "~20.1.4", "@angular/compiler": "~19.2.14",
"@angular/core": "~20.1.4", "@angular/core": "~19.2.14",
"@angular/forms": "~20.1.4", "@angular/forms": "~19.2.14",
"@angular/localize": "~20.1.4", "@angular/localize": "~19.2.14",
"@angular/platform-browser": "~20.1.4", "@angular/platform-browser": "~19.2.14",
"@angular/platform-browser-dynamic": "~20.1.4", "@angular/platform-browser-dynamic": "~19.2.14",
"@angular/router": "~20.1.4", "@angular/router": "~19.2.14",
"@ng-bootstrap/ng-bootstrap": "^19.0.1", "@ng-bootstrap/ng-bootstrap": "^18.0.0",
"@ng-select/ng-select": "^20.0.1", "@ng-select/ng-select": "^14.9.0",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.7", "bootstrap": "^5.3.6",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"mime-names": "^1.0.0", "mime-names": "^1.0.0",
"ng2-pdf-viewer": "^10.4.0", "ng2-pdf-viewer": "^10.4.0",
"ngx-bootstrap-icons": "^1.9.3", "ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^10.0.0", "ngx-color": "^10.0.0",
"ngx-cookie-service": "^20.0.1", "ngx-cookie-service": "^19.1.2",
"ngx-device-detector": "^10.0.2", "ngx-device-detector": "^9.0.0",
"ngx-ui-tour-ng-bootstrap": "^17.0.1", "ngx-ui-tour-ng-bootstrap": "^16.0.0",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"utif": "^3.1.0", "utif": "^3.1.0",
@@ -40,35 +40,34 @@
"zone.js": "^0.15.1" "zone.js": "^0.15.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "^20.0.0", "@angular-builders/custom-webpack": "^19.0.1",
"@angular-builders/jest": "^20.0.0", "@angular-builders/jest": "^19.0.1",
"@angular-devkit/core": "^20.1.4", "@angular-devkit/build-angular": "^19.2.14",
"@angular-devkit/schematics": "^20.1.4", "@angular-devkit/core": "^19.2.14",
"@angular-eslint/builder": "20.1.1", "@angular-devkit/schematics": "^19.2.14",
"@angular-eslint/eslint-plugin": "20.1.1", "@angular-eslint/builder": "19.7.0",
"@angular-eslint/eslint-plugin-template": "20.1.1", "@angular-eslint/eslint-plugin": "19.7.0",
"@angular-eslint/schematics": "20.1.1", "@angular-eslint/eslint-plugin-template": "19.7.0",
"@angular-eslint/template-parser": "20.1.1", "@angular-eslint/schematics": "19.7.0",
"@angular/build": "^20.1.4", "@angular-eslint/template-parser": "19.7.0",
"@angular/cli": "~20.1.4", "@angular/cli": "~19.2.14",
"@angular/compiler-cli": "~20.1.4", "@angular/compiler-cli": "~19.2.14",
"@codecov/webpack-plugin": "^1.9.1", "@codecov/webpack-plugin": "^1.9.1",
"@playwright/test": "^1.54.2", "@playwright/test": "^1.51.1",
"@types/jest": "^30.0.0", "@types/jest": "^29.5.14",
"@types/node": "^24.1.0", "@types/node": "^22.15.29",
"@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/eslint-plugin": "^8.33.0",
"@typescript-eslint/parser": "^8.38.0", "@typescript-eslint/parser": "^8.33.0",
"@typescript-eslint/utils": "^8.38.0", "@typescript-eslint/utils": "^8.33.0",
"eslint": "^9.32.0", "eslint": "^9.28.0",
"jest": "30.0.5", "jest": "29.7.0",
"jest-environment-jsdom": "^30.0.5", "jest-environment-jsdom": "^29.7.0",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"jest-preset-angular": "^15.0.0", "jest-preset-angular": "^14.5.5",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.2.0", "prettier-plugin-organize-imports": "^4.1.0",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",
"typescript": "^5.8.3", "typescript": "^5.5.4"
"webpack": "^5.101.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@@ -78,5 +77,6 @@
"lmdb", "lmdb",
"msgpackr-extract" "msgpackr-extract"
] ]
} },
"typings": "./src/typings.d.ts"
} }

7498
src-ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,12 @@
import '@angular/localize/init' import '@angular/localize/init'
import { jest } from '@jest/globals' import { jest } from '@jest/globals'
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone' import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'
import { TextDecoder, TextEncoder } from 'node:util' import { TextDecoder, TextEncoder } from 'util'
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
setupZoneTestEnv() setupZoneTestEnv()
} }
;(globalThis as any).TextEncoder = TextEncoder as unknown as { global.TextEncoder = TextEncoder
new (): TextEncoder global.TextDecoder = TextDecoder
}
;(globalThis as any).TextDecoder = TextDecoder as unknown as {
new (): TextDecoder
}
import { registerLocaleData } from '@angular/common' import { registerLocaleData } from '@angular/common'
import localeAf from '@angular/common/locales/af' import localeAf from '@angular/common/locales/af'
@@ -44,7 +40,6 @@ import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv' import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr' import localeTr from '@angular/common/locales/tr'
import localeUk from '@angular/common/locales/uk' import localeUk from '@angular/common/locales/uk'
import localeVi from '@angular/common/locales/vi'
import localeZh from '@angular/common/locales/zh' import localeZh from '@angular/common/locales/zh'
import localeZhHant from '@angular/common/locales/zh-Hant' import localeZhHant from '@angular/common/locales/zh-Hant'
@@ -80,7 +75,6 @@ registerLocaleData(localeSr)
registerLocaleData(localeSv) registerLocaleData(localeSv)
registerLocaleData(localeTr) registerLocaleData(localeTr)
registerLocaleData(localeUk) registerLocaleData(localeUk)
registerLocaleData(localeVi)
registerLocaleData(localeZh) registerLocaleData(localeZh)
registerLocaleData(localeZhHant) registerLocaleData(localeZhHant)
@@ -120,26 +114,10 @@ if (!URL.revokeObjectURL) {
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() }) Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
} }
Object.defineProperty(window, 'ResizeObserver', { value: mock() }) Object.defineProperty(window, 'ResizeObserver', { value: mock() })
Object.defineProperty(window, 'location', {
if (typeof IntersectionObserver === 'undefined') {
class MockIntersectionObserver {
constructor(
public callback: IntersectionObserverCallback,
public options?: IntersectionObserverInit
) {}
observe = jest.fn()
unobserve = jest.fn()
disconnect = jest.fn()
takeRecords = jest.fn()
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
configurable: true, configurable: true,
value: MockIntersectionObserver, value: { reload: jest.fn() },
}) })
}
HTMLCanvasElement.prototype.getContext = < HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext typeof HTMLCanvasElement.prototype.getContext

View File

@@ -1,4 +1,4 @@
import { Component, inject, OnDestroy, OnInit, Renderer2 } from '@angular/core' import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'
import { Router, RouterOutlet } from '@angular/router' import { Router, RouterOutlet } from '@angular/router'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { first, Subscription } from 'rxjs' import { first, Subscription } from 'rxjs'
@@ -29,22 +29,22 @@ import { WebsocketStatusService } from './services/websocket-status.service'
], ],
}) })
export class AppComponent implements OnInit, OnDestroy { export class AppComponent implements OnInit, OnDestroy {
private settings = inject(SettingsService)
private websocketStatusService = inject(WebsocketStatusService)
private toastService = inject(ToastService)
private router = inject(Router)
private tasksService = inject(TasksService)
tourService = inject(TourService)
private renderer = inject(Renderer2)
private permissionsService = inject(PermissionsService)
private hotKeyService = inject(HotKeyService)
private componentRouterService = inject(ComponentRouterService)
newDocumentSubscription: Subscription newDocumentSubscription: Subscription
successSubscription: Subscription successSubscription: Subscription
failedSubscription: Subscription failedSubscription: Subscription
constructor() { constructor(
private settings: SettingsService,
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 componentRouterService: ComponentRouterService
) {
let anyWindow = window as any let anyWindow = window as any
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.mjs' anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.mjs'
this.settings.updateAppearanceSettings() this.settings.updateAppearanceSettings()

View File

@@ -50,7 +50,7 @@
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div> <div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group me-2"> <div class="btn-group me-2">
<button type="button" (click)="discardChanges()" class="btn btn-outline-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button> <button type="button" (click)="discardChanges()" class="btn btn-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button> <button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>

View File

@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common' import { AsyncPipe } from '@angular/common'
import { Component, OnDestroy, OnInit, inject } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { import {
AbstractControl, AbstractControl,
FormControl, FormControl,
@@ -57,10 +57,6 @@ export class ConfigComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnInit, OnDestroy, DirtyComponent implements OnInit, OnDestroy, DirtyComponent
{ {
private configService = inject(ConfigService)
private toastService = inject(ToastService)
private settingsService = inject(SettingsService)
public readonly ConfigOptionType = ConfigOptionType public readonly ConfigOptionType = ConfigOptionType
// generated dynamically // generated dynamically
@@ -81,7 +77,11 @@ export class ConfigComponent
storeSub: Subscription storeSub: Subscription
isDirty$: Observable<boolean> isDirty$: Observable<boolean>
constructor() { constructor(
private configService: ConfigService,
private toastService: ToastService,
private settingsService: SettingsService
) {
super() super()
this.configForm.addControl('id', new FormControl()) this.configForm.addControl('id', new FormControl())
PaperlessConfigOptions.forEach((option) => { PaperlessConfigOptions.forEach((option) => {

View File

@@ -5,7 +5,6 @@ import {
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewChild, ViewChild,
inject,
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap' import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
@@ -29,8 +28,12 @@ export class LogsComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
private logService = inject(LogService) constructor(
private changedetectorRef = inject(ChangeDetectorRef) private logService: LogService,
private changedetectorRef: ChangeDetectorRef
) {
super()
}
public logs: string[] = [] public logs: string[] = []

View File

@@ -176,7 +176,6 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check> <pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
<pngx-input-check i18n-title title="Show document counts in sidebar saved views" formControlName="sidebarViewsShowCount"></pngx-input-check>
</div> </div>
</div> </div>
@@ -358,6 +357,6 @@
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div> <div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button> <button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button> <button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
</form> </form>

View File

@@ -31,12 +31,10 @@ import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { PermissionsService } from 'src/app/services/permissions.service' import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service' import { GroupService } from 'src/app/services/rest/group.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.service' import { SystemStatusService } from 'src/app/services/system-status.service'
import { Toast, ToastService } from 'src/app/services/toast.service' import { Toast, ToastService } from 'src/app/services/toast.service'
import * as navUtils from 'src/app/utils/navigation'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CheckComponent } from '../../common/input/check/check.component' import { CheckComponent } from '../../common/input/check/check.component'
@@ -74,7 +72,6 @@ describe('SettingsComponent', () => {
let groupService: GroupService let groupService: GroupService
let modalService: NgbModal let modalService: NgbModal
let systemStatusService: SystemStatusService let systemStatusService: SystemStatusService
let savedViewsService: SavedViewService
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -125,7 +122,6 @@ describe('SettingsComponent', () => {
permissionsService = TestBed.inject(PermissionsService) permissionsService = TestBed.inject(PermissionsService)
modalService = TestBed.inject(NgbModal) modalService = TestBed.inject(NgbModal)
systemStatusService = TestBed.inject(SystemStatusService) systemStatusService = TestBed.inject(SystemStatusService)
savedViewsService = TestBed.inject(SavedViewService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions') .spyOn(permissionsService, 'currentUserHasObjectPermissions')
@@ -216,7 +212,7 @@ describe('SettingsComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(30) expect(setSpy).toHaveBeenCalledTimes(29)
// succeed // succeed
storeSpy.mockReturnValueOnce(of(true)) storeSpy.mockReturnValueOnce(of(true))
@@ -226,9 +222,6 @@ describe('SettingsComponent', () => {
}) })
it('should offer reload if settings changes require', () => { it('should offer reload if settings changes require', () => {
const reloadSpy = jest
.spyOn(navUtils, 'locationReload')
.mockImplementation(() => {})
completeSetup() completeSetup()
let toast: Toast let toast: Toast
toastService.getToasts().subscribe((t) => (toast = t[0])) toastService.getToasts().subscribe((t) => (toast = t[0]))
@@ -245,7 +238,6 @@ describe('SettingsComponent', () => {
expect(toast.actionName).toEqual('Reload now') expect(toast.actionName).toEqual('Reload now')
toast.action() toast.action()
expect(reloadSpy).toHaveBeenCalled()
}) })
it('should allow setting theme color, visually apply change immediately but not save', () => { it('should allow setting theme color, visually apply change immediately but not save', () => {
@@ -274,7 +266,7 @@ describe('SettingsComponent', () => {
) )
completeSetup(userService) completeSetup(userService)
fixture.detectChanges() fixture.detectChanges()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toBeCalled()
}) })
it('should show errors on load if load groups failure', () => { it('should show errors on load if load groups failure', () => {
@@ -286,7 +278,7 @@ describe('SettingsComponent', () => {
) )
completeSetup(groupService) completeSetup(groupService)
fixture.detectChanges() fixture.detectChanges()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toBeCalled()
}) })
it('should load system status on initialize, show errors if needed', () => { it('should load system status on initialize, show errors if needed', () => {
@@ -353,14 +345,4 @@ describe('SettingsComponent', () => {
component.reset() component.reset()
expect(component.settingsForm.get('themeColor').value).toEqual('') expect(component.settingsForm.get('themeColor').value).toEqual('')
}) })
it('should trigger maybeRefreshDocumentCounts on settings save', () => {
completeSetup()
const maybeRefreshSpy = jest.spyOn(
savedViewsService,
'maybeRefreshDocumentCounts'
)
settingsService.settingsSaved.emit(true)
expect(maybeRefreshSpy).toHaveBeenCalled()
})
}) })

View File

@@ -2,10 +2,10 @@ import { AsyncPipe, ViewportScroller } from '@angular/common'
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
Inject,
LOCALE_ID, LOCALE_ID,
OnDestroy, OnDestroy,
OnInit, OnInit,
inject,
} from '@angular/core' } from '@angular/core'
import { import {
FormControl, FormControl,
@@ -49,7 +49,6 @@ import {
PermissionsService, PermissionsService,
} from 'src/app/services/permissions.service' } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service' import { GroupService } from 'src/app/services/rest/group.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { import {
LanguageOption, LanguageOption,
@@ -57,7 +56,6 @@ import {
} from 'src/app/services/settings.service' } from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.service' import { SystemStatusService } from 'src/app/services/system-status.service'
import { Toast, ToastService } from 'src/app/services/toast.service' import { Toast, ToastService } from 'src/app/services/toast.service'
import { locationReload } from 'src/app/utils/navigation'
import { CheckComponent } from '../../common/input/check/check.component' import { CheckComponent } from '../../common/input/check/check.component'
import { ColorComponent } from '../../common/input/color/color.component' import { ColorComponent } from '../../common/input/color/color.component'
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component' import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
@@ -106,21 +104,6 @@ export class SettingsComponent
extends ComponentWithPermissions extends ComponentWithPermissions
implements OnInit, AfterViewInit, OnDestroy, DirtyComponent implements OnInit, AfterViewInit, OnDestroy, DirtyComponent
{ {
private documentListViewService = inject(DocumentListViewService)
private toastService = inject(ToastService)
private settings = inject(SettingsService)
currentLocale = inject(LOCALE_ID)
private viewportScroller = inject(ViewportScroller)
private activatedRoute = inject(ActivatedRoute)
readonly tourService = inject(TourService)
private usersService = inject(UserService)
private groupsService = inject(GroupService)
private router = inject(Router)
permissionsService = inject(PermissionsService)
private modalService = inject(NgbModal)
private systemStatusService = inject(SystemStatusService)
private savedViewsService = inject(SavedViewService)
activeNavID: number activeNavID: number
settingsForm = new FormGroup({ settingsForm = new FormGroup({
@@ -155,7 +138,6 @@ export class SettingsComponent
notificationsConsumerSuppressOnDashboard: new FormControl(null), notificationsConsumerSuppressOnDashboard: new FormControl(null),
savedViewsWarnOnUnsavedChange: new FormControl(null), savedViewsWarnOnUnsavedChange: new FormControl(null),
sidebarViewsShowCount: new FormControl(null),
}) })
SettingsNavIDs = SettingsNavIDs SettingsNavIDs = SettingsNavIDs
@@ -197,11 +179,24 @@ export class SettingsComponent
) )
} }
constructor() { constructor(
private documentListViewService: DocumentListViewService,
private toastService: ToastService,
private settings: SettingsService,
@Inject(LOCALE_ID) public currentLocale: string,
private viewportScroller: ViewportScroller,
private activatedRoute: ActivatedRoute,
public readonly tourService: TourService,
private usersService: UserService,
private groupsService: GroupService,
private router: Router,
public permissionsService: PermissionsService,
private modalService: NgbModal,
private systemStatusService: SystemStatusService
) {
super() super()
this.settings.settingsSaved.subscribe(() => { this.settings.settingsSaved.subscribe(() => {
if (!this.savePending) this.initialize() if (!this.savePending) this.initialize()
this.savedViewsService.maybeRefreshDocumentCounts()
}) })
} }
@@ -313,9 +308,6 @@ export class SettingsComponent
savedViewsWarnOnUnsavedChange: this.settings.get( savedViewsWarnOnUnsavedChange: this.settings.get(
SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE
), ),
sidebarViewsShowCount: this.settings.get(
SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT
),
defaultPermsOwner: this.settings.get(SETTINGS_KEYS.DEFAULT_PERMS_OWNER), defaultPermsOwner: this.settings.get(SETTINGS_KEYS.DEFAULT_PERMS_OWNER),
defaultPermsViewUsers: this.settings.get( defaultPermsViewUsers: this.settings.get(
SETTINGS_KEYS.DEFAULT_PERMS_VIEW_USERS SETTINGS_KEYS.DEFAULT_PERMS_VIEW_USERS
@@ -493,10 +485,6 @@ export class SettingsComponent
SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE, SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE,
this.settingsForm.value.savedViewsWarnOnUnsavedChange this.settingsForm.value.savedViewsWarnOnUnsavedChange
) )
this.settings.set(
SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT,
this.settingsForm.value.sidebarViewsShowCount
)
this.settings.set( this.settings.set(
SETTINGS_KEYS.DEFAULT_PERMS_OWNER, SETTINGS_KEYS.DEFAULT_PERMS_OWNER,
this.settingsForm.value.defaultPermsOwner this.settingsForm.value.defaultPermsOwner
@@ -551,7 +539,7 @@ export class SettingsComponent
savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.` savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`
savedToast.actionName = $localize`Reload now` savedToast.actionName = $localize`Reload now`
savedToast.action = () => { savedToast.action = () => {
locationReload() location.reload()
} }
} }

View File

@@ -1,5 +1,5 @@
import { NgTemplateOutlet, SlicePipe } from '@angular/common' import { NgTemplateOutlet, SlicePipe } from '@angular/common'
import { Component, inject, OnDestroy, OnInit } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { import {
@@ -69,10 +69,6 @@ export class TasksComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
tasksService = inject(TasksService)
private modalService = inject(NgbModal)
private readonly router = inject(Router)
public activeTab: TaskTab public activeTab: TaskTab
public selectedTasks: Set<number> = new Set() public selectedTasks: Set<number> = new Set()
public togggleAll: boolean = false public togggleAll: boolean = false
@@ -109,6 +105,14 @@ export class TasksComponent
: $localize`Dismiss all` : $localize`Dismiss all`
} }
constructor(
public tasksService: TasksService,
private modalService: NgbModal,
private readonly router: Router
) {
super()
}
ngOnInit() { ngOnInit() {
this.tasksService.reload() this.tasksService.reload()
timer(5000, 5000) timer(5000, 5000)

View File

@@ -1,4 +1,4 @@
import { Component, OnDestroy, inject } from '@angular/core' import { Component, OnDestroy } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { import {
@@ -36,19 +36,19 @@ export class TrashComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnDestroy implements OnDestroy
{ {
private trashService = inject(TrashService)
private toastService = inject(ToastService)
private modalService = inject(NgbModal)
private settingsService = inject(SettingsService)
private router = inject(Router)
public documentsInTrash: Document[] = [] public documentsInTrash: Document[] = []
public selectedDocuments: Set<number> = new Set() public selectedDocuments: Set<number> = new Set()
public allToggled: boolean = false public allToggled: boolean = false
public page: number = 1 public page: number = 1
public totalDocuments: number public totalDocuments: number
constructor() { constructor(
private trashService: TrashService,
private toastService: ToastService,
private modalService: NgbModal,
private settingsService: SettingsService,
private router: Router
) {
super() super()
this.reload() this.reload()
} }

View File

@@ -19,7 +19,6 @@ import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import * as navUtils from 'src/app/utils/navigation'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component' import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component' import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
@@ -108,7 +107,7 @@ describe('UsersAndGroupsComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit() editDialog.failed.emit()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toBeCalled()
settingsService.currentUser = users[1] // simulate logged in as different user settingsService.currentUser = users[1] // simulate logged in as different user
editDialog.succeeded.emit(users[0]) editDialog.succeeded.emit(users[0])
expect(toastInfoSpy).toHaveBeenCalledWith( expect(toastInfoSpy).toHaveBeenCalledWith(
@@ -131,7 +130,7 @@ describe('UsersAndGroupsComponent', () => {
throwError(() => new Error('error deleting user')) throwError(() => new Error('error deleting user'))
) )
deleteDialog.confirm() deleteDialog.confirm()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true)) deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm() deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled() expect(listAllSpy).toHaveBeenCalled()
@@ -143,18 +142,19 @@ describe('UsersAndGroupsComponent', () => {
let modal: NgbModalRef let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0])) modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editUser(users[0]) component.editUser(users[0])
const navSpy = jest
.spyOn(navUtils, 'setLocationHref')
.mockImplementation(() => {})
const editDialog = modal.componentInstance as UserEditDialogComponent const editDialog = modal.componentInstance as UserEditDialogComponent
editDialog.passwordIsSet = true editDialog.passwordIsSet = true
settingsService.currentUser = users[0] // simulate logged in as same user settingsService.currentUser = users[0] // simulate logged in as same user
editDialog.succeeded.emit(users[0]) editDialog.succeeded.emit(users[0])
fixture.detectChanges() fixture.detectChanges()
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/',
},
writable: true, // possibility to override
})
tick(2600) tick(2600)
expect(navSpy).toHaveBeenCalledWith( expect(window.location.href).toContain('logout')
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
})) }))
it('should support edit / create group, show error if needed', () => { it('should support edit / create group, show error if needed', () => {
@@ -166,7 +166,7 @@ describe('UsersAndGroupsComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit() editDialog.failed.emit()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toBeCalled()
editDialog.succeeded.emit(groups[0]) editDialog.succeeded.emit(groups[0])
expect(toastInfoSpy).toHaveBeenCalledWith( expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved group "${groups[0].name}".` `Saved group "${groups[0].name}".`
@@ -188,7 +188,7 @@ describe('UsersAndGroupsComponent', () => {
throwError(() => new Error('error deleting group')) throwError(() => new Error('error deleting group'))
) )
deleteDialog.confirm() deleteDialog.confirm()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toBeCalled()
deleteSpy.mockReturnValueOnce(of(true)) deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm() deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled() expect(listAllSpy).toHaveBeenCalled()
@@ -210,7 +210,7 @@ describe('UsersAndGroupsComponent', () => {
) )
completeSetup(userService) completeSetup(userService)
fixture.detectChanges() fixture.detectChanges()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toBeCalled()
}) })
it('should show errors on load if load groups failure', () => { it('should show errors on load if load groups failure', () => {
@@ -222,6 +222,6 @@ describe('UsersAndGroupsComponent', () => {
) )
completeSetup(groupService) completeSetup(groupService)
fixture.detectChanges() fixture.detectChanges()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toBeCalled()
}) })
}) })

View File

@@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, first, takeUntil } from 'rxjs' import { Subject, first, takeUntil } from 'rxjs'
@@ -10,7 +10,6 @@ import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { setLocationHref } from 'src/app/utils/navigation'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component' import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
@@ -32,18 +31,22 @@ export class UsersAndGroupsComponent
extends ComponentWithPermissions extends ComponentWithPermissions
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
private usersService = inject(UserService)
private groupsService = inject(GroupService)
private toastService = inject(ToastService)
private modalService = inject(NgbModal)
permissionsService = inject(PermissionsService)
private settings = inject(SettingsService)
users: User[] users: User[]
groups: Group[] groups: Group[]
unsubscribeNotifier: Subject<any> = new Subject() unsubscribeNotifier: Subject<any> = new Subject()
constructor(
private usersService: UserService,
private groupsService: GroupService,
private toastService: ToastService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
private settings: SettingsService
) {
super()
}
ngOnInit(): void { ngOnInit(): void {
this.usersService this.usersService
.listAll(null, null, { full_perms: true }) .listAll(null, null, { full_perms: true })
@@ -94,9 +97,7 @@ export class UsersAndGroupsComponent
$localize`Password has been changed, you will be logged out momentarily.` $localize`Password has been changed, you will be logged out momentarily.`
) )
setTimeout(() => { setTimeout(() => {
setLocationHref( window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
}, 2500) }, 2500)
} else { } else {
this.toastService.showInfo( this.toastService.showInfo(

View File

@@ -112,14 +112,7 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim"> popoverClass="popover-slim">
<i-bs class="me-1" name="funnel"></i-bs><span>&nbsp;{{view.name}} <i-bs class="me-1" name="funnel"></i-bs><span>&nbsp;{{view.name}}</span>
@if (showSidebarCounts && !slimSidebarEnabled) {
<span><span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span></span>
}
</span>
@if (showSidebarCounts && slimSidebarEnabled) {
<span class="badge bg-info text-dark position-absolute top-0 end-0 d-none d-md-block">{{ savedViewService.getDocumentCount(view) }}</span>
}
</a> </a>
@if (settingsService.organizingSidebarSavedViews) { @if (settingsService.organizingSidebarSavedViews) {
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle> <div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>

View File

@@ -92,7 +92,6 @@ describe('AppFrameComponent', () => {
let router: Router let router: Router
let savedViewSpy let savedViewSpy
let modalService: NgbModal let modalService: NgbModal
let maybeRefreshSpy
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -114,11 +113,7 @@ describe('AppFrameComponent', () => {
{ {
provide: SavedViewService, provide: SavedViewService,
useValue: { useValue: {
reload: (fn: any) => { reload: () => {},
if (fn) {
fn()
}
},
listAll: () => listAll: () =>
of({ of({
all: [saved_views.map((v) => v.id)], all: [saved_views.map((v) => v.id)],
@@ -126,8 +121,6 @@ describe('AppFrameComponent', () => {
results: saved_views, results: saved_views,
}), }),
sidebarViews: saved_views.filter((v) => v.show_in_sidebar), sidebarViews: saved_views.filter((v) => v.show_in_sidebar),
getDocumentCount: (view: SavedView) => 5,
maybeRefreshDocumentCounts: () => {},
}, },
}, },
PermissionsService, PermissionsService,
@@ -176,7 +169,6 @@ describe('AppFrameComponent', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
savedViewSpy = jest.spyOn(savedViewService, 'reload') savedViewSpy = jest.spyOn(savedViewService, 'reload')
maybeRefreshSpy = jest.spyOn(savedViewService, 'maybeRefreshDocumentCounts')
fixture = TestBed.createComponent(AppFrameComponent) fixture = TestBed.createComponent(AppFrameComponent)
component = fixture.componentInstance component = fixture.componentInstance
@@ -367,8 +359,4 @@ describe('AppFrameComponent', () => {
expect(toastErrorSpy).toHaveBeenCalledTimes(2) expect(toastErrorSpy).toHaveBeenCalledTimes(2)
expect(toastInfoSpy).toHaveBeenCalledTimes(3) expect(toastInfoSpy).toHaveBeenCalledTimes(3)
}) })
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
expect(maybeRefreshSpy).toHaveBeenCalled()
})
}) })

View File

@@ -6,7 +6,7 @@ import {
moveItemInArray, moveItemInArray,
} from '@angular/cdk/drag-drop' } from '@angular/cdk/drag-drop'
import { NgClass } from '@angular/common' import { NgClass } from '@angular/common'
import { Component, HostListener, inject, OnInit } from '@angular/core' import { Component, HostListener, OnInit } from '@angular/core'
import { ActivatedRoute, Router, RouterModule } from '@angular/router' import { ActivatedRoute, Router, RouterModule } from '@angular/router'
import { import {
NgbCollapseModule, NgbCollapseModule,
@@ -74,27 +74,27 @@ export class AppFrameComponent
extends ComponentWithPermissions extends ComponentWithPermissions
implements OnInit, ComponentCanDeactivate implements OnInit, ComponentCanDeactivate
{ {
router = inject(Router) versionString = `${environment.appTitle} ${environment.version}`
private activatedRoute = inject(ActivatedRoute)
private openDocumentsService = inject(OpenDocumentsService)
savedViewService = inject(SavedViewService)
private remoteVersionService = inject(RemoteVersionService)
settingsService = inject(SettingsService)
tasksService = inject(TasksService)
private readonly toastService = inject(ToastService)
private modalService = inject(NgbModal)
permissionsService = inject(PermissionsService)
private djangoMessagesService = inject(DjangoMessagesService)
appRemoteVersion: AppRemoteVersion appRemoteVersion: AppRemoteVersion
isMenuCollapsed: boolean = true isMenuCollapsed: boolean = true
slimSidebarAnimating: boolean = false slimSidebarAnimating: boolean = false
constructor() { constructor(
public router: Router,
private activatedRoute: ActivatedRoute,
private openDocumentsService: OpenDocumentsService,
public savedViewService: SavedViewService,
private remoteVersionService: RemoteVersionService,
public settingsService: SettingsService,
public tasksService: TasksService,
private readonly toastService: ToastService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
private djangoMessagesService: DjangoMessagesService
) {
super() super()
const permissionsService = this.permissionsService
if ( if (
permissionsService.currentUserCan( permissionsService.currentUserCan(
@@ -102,9 +102,7 @@ export class AppFrameComponent
PermissionType.SavedView PermissionType.SavedView
) )
) { ) {
this.savedViewService.reload(() => { this.savedViewService.reload()
this.savedViewService.maybeRefreshDocumentCounts()
})
} }
} }
@@ -144,10 +142,6 @@ export class AppFrameComponent
}, 200) // slightly longer than css animation for slim sidebar }, 200) // slightly longer than css animation for slim sidebar
} }
get versionString(): string {
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.production ? '' : ` #${environment.tag}`}`
}
get customAppTitle(): string { get customAppTitle(): string {
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE) return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
} }
@@ -285,8 +279,4 @@ export class AppFrameComponent
onLogout() { onLogout() {
this.openDocumentsService.closeAll() this.openDocumentsService.closeAll()
} }
get showSidebarCounts(): boolean {
return this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT)
}
} }

View File

@@ -6,7 +6,6 @@ import {
QueryList, QueryList,
ViewChild, ViewChild,
ViewChildren, ViewChildren,
inject,
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router } from '@angular/router' import { Router } from '@angular/router'
@@ -70,17 +69,6 @@ import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-e
], ],
}) })
export class GlobalSearchComponent implements OnInit { export class GlobalSearchComponent implements OnInit {
searchService = inject(SearchService)
private router = inject(Router)
private modalService = inject(NgbModal)
private documentService = inject(DocumentService)
private documentListViewService = inject(DocumentListViewService)
private permissionsService = inject(PermissionsService)
private toastService = inject(ToastService)
private hotkeyService = inject(HotKeyService)
private settingsService = inject(SettingsService)
private locationStrategy = inject(LocationStrategy)
public DataType = DataType public DataType = DataType
public query: string public query: string
public queryDebounce: Subject<string> public queryDebounce: Subject<string>
@@ -102,7 +90,18 @@ export class GlobalSearchComponent implements OnInit {
) )
} }
constructor() { constructor(
public searchService: SearchService,
private router: Router,
private modalService: NgbModal,
private documentService: DocumentService,
private documentListViewService: DocumentListViewService,
private permissionsService: PermissionsService,
private toastService: ToastService,
private hotkeyService: HotKeyService,
private settingsService: SettingsService,
private locationStrategy: LocationStrategy
) {
this.queryDebounce = new Subject<string>() this.queryDebounce = new Subject<string>()
this.queryDebounce this.queryDebounce

View File

@@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { import {
NgbDropdownModule, NgbDropdownModule,
NgbProgressbarModule, NgbProgressbarModule,
@@ -20,7 +20,7 @@ import { ToastComponent } from '../../common/toast/toast.component'
], ],
}) })
export class ToastsDropdownComponent implements OnInit, OnDestroy { export class ToastsDropdownComponent implements OnInit, OnDestroy {
toastService = inject(ToastService) constructor(public toastService: ToastService) {}
private subscription: Subscription private subscription: Subscription

View File

@@ -1,5 +1,5 @@
import { DecimalPipe } from '@angular/common' import { DecimalPipe } from '@angular/common'
import { Component, EventEmitter, Input, Output, inject } from '@angular/core' import { Component, EventEmitter, Input, Output } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
@@ -12,7 +12,9 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
imports: [DecimalPipe, SafeHtmlPipe], imports: [DecimalPipe, SafeHtmlPipe],
}) })
export class ConfirmDialogComponent extends LoadingComponentWithPermissions { export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
activeModal = inject(NgbActiveModal) constructor(public activeModal: NgbActiveModal) {
super()
}
@Output() @Output()
public confirmClicked = new EventEmitter() public confirmClicked = new EventEmitter()

View File

@@ -0,0 +1,54 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col">
<div class="btn-toolbar flex-nowrap">
<div class="input-group input-group-sm">
<div class="input-group-text" i18n>Page</div>
<input class="form-control mw-60" type="number" min="1" [(ngModel)]="currentPage" />
<div class="input-group-text" i18n>of {{totalPages}}</div>
</div>
<div class="input-group input-group-sm ms-auto">
<span class="input-group-text" i18n>Pages to remove</span>
<input [ngModel]="pagesString" class="form-control" disabled />
</div>
</div>
<div class="pdf-viewer-container w-100 mt-3">
<pdf-viewer #pdfViewer [src]="pdfSrc" [(page)]="currentPage"
[original-size]="false"
[zoom]="1"
zoom-scale="page-fit"
[render-text]="false"
(pagerendered)="pageRendered($event)"
(after-load-complete)="pdfPreviewLoaded($event)">
</pdf-viewer>
</div>
</div>
</div>
</div>
<div class="modal-footer flex-nowrap">
<div>
@if (message) {
<p [innerHTML]="message | safeHtml"></p>
}
@if (messageBold) {
<p class="mb-0 small"><b [innerHTML]="messageBold | safeHtml"></b></p>
}
</div>
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}}
</button>
</div>
<ng-template #pageCheckOverlay let-page="page" let-pages="pages">
<div class="position-absolute top-0 start-0 w-100 h-100 p-2" (click)="pageCheckChanged(page)">
<input type="checkbox" class="form-check-input" />
</div>
</ng-template>

View File

@@ -0,0 +1,28 @@
.pdf-viewer-container {
background-color: gray;
height: 550px;
pdf-viewer {
width: 100%;
height: 100%;
}
}
.mw-60 {
max-width: 60px;
}
div.position-absolute:has(.form-check-input:checked) {
background-color: rgba(var(--bs-dark-rgb), 0.4);
}
.form-check-input {
&:checked {
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
&:focus {
box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), var(--pngx-focus-alpha));
border-color: var(--bs-danger);
}
}

View File

@@ -0,0 +1,60 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
describe('DeletePagesConfirmDialogComponent', () => {
let component: DeletePagesConfirmDialogComponent
let fixture: ComponentFixture<DeletePagesConfirmDialogComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [],
imports: [
NgxBootstrapIconsModule.pick(allIcons),
FormsModule,
ReactiveFormsModule,
DeletePagesConfirmDialogComponent,
],
providers: [
NgbActiveModal,
SafeHtmlPipe,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents()
fixture = TestBed.createComponent(DeletePagesConfirmDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should return a string with comma-separated pages', () => {
component.pages = [1, 2, 3, 4]
expect(component.pagesString).toEqual('1, 2, 3, 4')
})
it('should update totalPages when pdf is loaded', () => {
component.pdfPreviewLoaded({ numPages: 5 } as any)
expect(component.totalPages).toEqual(5)
})
it('should update checks when page is rendered', () => {
const event = {
target: document.createElement('div'),
detail: { pageNumber: 1 },
} as any
component.pageRendered(event)
expect(component['checks'].length).toEqual(1)
})
it('should update pages when page check is changed', () => {
component.pageCheckChanged(1)
expect(component.pages).toEqual([1])
component.pageCheckChanged(1)
expect(component.pages).toEqual([])
})
})

View File

@@ -0,0 +1,71 @@
import { Component, TemplateRef, ViewChild } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import {
PDFDocumentProxy,
PdfViewerComponent,
PdfViewerModule,
} from 'ng2-pdf-viewer'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-delete-pages-confirm-dialog',
templateUrl: './delete-pages-confirm-dialog.component.html',
styleUrl: './delete-pages-confirm-dialog.component.scss',
imports: [PdfViewerModule, FormsModule, ReactiveFormsModule, SafeHtmlPipe],
})
export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent {
public documentID: number
public pages: number[] = []
public currentPage: number = 1
public totalPages: number
@ViewChild('pdfViewer') pdfViewer: PdfViewerComponent
@ViewChild('pageCheckOverlay') pageCheckOverlay!: TemplateRef<any>
private checks: HTMLElement[] = []
public get pagesString(): string {
return this.pages.join(', ')
}
public get pdfSrc(): string {
return this.documentService.getPreviewUrl(this.documentID)
}
constructor(
activeModal: NgbActiveModal,
private documentService: DocumentService
) {
super(activeModal)
}
public pdfPreviewLoaded(pdf: PDFDocumentProxy) {
this.totalPages = pdf.numPages
}
pageRendered(event: CustomEvent) {
const pageDiv = event.target as HTMLDivElement
const check = this.pageCheckOverlay.createEmbeddedView({
page: event.detail.pageNumber,
})
this.checks[event.detail.pageNumber - 1] = check.rootNodes[0]
pageDiv?.insertBefore(check.rootNodes[0], pageDiv.firstChild)
this.updateChecks()
}
pageCheckChanged(pageNumber: number) {
if (!this.pages.includes(pageNumber)) this.pages.push(pageNumber)
else if (this.pages.includes(pageNumber))
this.pages.splice(this.pages.indexOf(pageNumber), 1)
this.updateChecks()
}
private updateChecks() {
this.checks.forEach((check, i) => {
const input = check.getElementsByTagName('input')[0]
input.checked = this.pages.includes(i + 1)
})
}
}

View File

@@ -3,8 +3,9 @@ import {
DragDropModule, DragDropModule,
moveItemInArray, moveItemInArray,
} from '@angular/cdk/drag-drop' } from '@angular/cdk/drag-drop'
import { Component, OnInit, inject } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
@@ -27,9 +28,6 @@ export class MergeConfirmDialogComponent
extends ConfirmDialogComponent extends ConfirmDialogComponent
implements OnInit implements OnInit
{ {
private documentService = inject(DocumentService)
private permissionService = inject(PermissionsService)
public documentIDs: number[] = [] public documentIDs: number[] = []
public archiveFallback: boolean = false public archiveFallback: boolean = false
public deleteOriginals: boolean = false public deleteOriginals: boolean = false
@@ -40,8 +38,12 @@ export class MergeConfirmDialogComponent
public metadataDocumentID: number = -1 public metadataDocumentID: number = -1
constructor() { constructor(
super() activeModal: NgbActiveModal,
private documentService: DocumentService,
private permissionService: PermissionsService
) {
super(activeModal)
} }
ngOnInit() { ngOnInit() {

View File

@@ -1,5 +1,6 @@
import { NgStyle } from '@angular/common' import { NgStyle } from '@angular/common'
import { Component, inject } from '@angular/core' import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
@@ -12,8 +13,6 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
imports: [NgStyle, NgxBootstrapIconsModule, SafeHtmlPipe], imports: [NgStyle, NgxBootstrapIconsModule, SafeHtmlPipe],
}) })
export class RotateConfirmDialogComponent extends ConfirmDialogComponent { export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
documentService = inject(DocumentService)
public documentID: number public documentID: number
public showPDFNote: boolean = true public showPDFNote: boolean = true
@@ -26,8 +25,11 @@ export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
return degrees return degrees
} }
constructor() { constructor(
super() activeModal: NgbActiveModal,
public documentService: DocumentService
) {
super(activeModal)
} }
rotate(clockwise: boolean = true) { rotate(clockwise: boolean = true) {

View File

@@ -0,0 +1,59 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<p>{{message}}</p>
<div class="row mb-2">
<div class="col-7">
<div class="input-group input-group-sm">
<div class="input-group-text" i18n>Page</div>
<input class="form-control" type="number" min="1" [(ngModel)]="page" />
<div class="input-group-text" i18n>of {{totalPages}}</div>
</div>
<div class="pdf-viewer-container w-100 mt-3">
<pdf-viewer [src]="pdfSrc" [(page)]="page"
[original-size]="false"
[zoom]="1"
zoom-scale="page-fit"
(after-load-complete)="pdfPreviewLoaded($event)">
</pdf-viewer>
</div>
</div>
<div class="col-5">
<div class="d-grid">
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit">
<i-bs name="plus-circle"></i-bs>&nbsp;
<span i18n>Add Split</span>
</button>
</div>
<ul class="list-group mt-3">
@for (pageStr of pagesString.split(','); track pageStr; let i = $index) {
<li class="list-group-item d-flex align-items-center">
{{pageStr}}
@if (pagesString.split(',').length > 1) {
&nbsp;
<button class="btn btn-sm btn-danger ms-auto" (click)="removeSplit(i)">
<i-bs name="trash"></i-bs>
</button>
}
</li>
}
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<div class="form-check form-switch me-auto">
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalSwitch" [(ngModel)]="deleteOriginal" [disabled]="!userOwnsDocument">
<label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label>
</div>
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}}
</button>
</div>

View File

@@ -0,0 +1,9 @@
.pdf-viewer-container {
background-color: gray;
height: 500px;
pdf-viewer {
width: 100%;
height: 100%;
}
}

View File

@@ -0,0 +1,107 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SplitConfirmDialogComponent } from './split-confirm-dialog.component'
describe('SplitConfirmDialogComponent', () => {
let component: SplitConfirmDialogComponent
let fixture: ComponentFixture<SplitConfirmDialogComponent>
let documentService: DocumentService
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
NgxBootstrapIconsModule.pick(allIcons),
ReactiveFormsModule,
FormsModule,
PdfViewerModule,
SplitConfirmDialogComponent,
],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents()
fixture = TestBed.createComponent(SplitConfirmDialogComponent)
documentService = TestBed.inject(DocumentService)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should load document on init', () => {
const getSpy = jest.spyOn(documentService, 'get')
component.documentID = 1
getSpy.mockReturnValue(of({ id: 1 } as any))
component.ngOnInit()
expect(documentService.get).toHaveBeenCalledWith(1)
})
it('should update pagesString when pages are added', () => {
component.totalPages = 5
component.page = 2
component.addSplit()
expect(component.pagesString).toEqual('1-2,3-5')
component.page = 4
component.addSplit()
expect(component.pagesString).toEqual('1-2,3-4,5')
})
it('should update pagesString when pages are removed', () => {
component.totalPages = 5
component.page = 2
component.addSplit()
component.page = 4
component.addSplit()
expect(component.pagesString).toEqual('1-2,3-4,5')
component.removeSplit(0)
expect(component.pagesString).toEqual('1-4,5')
})
it('should enable confirm button when pages are added', () => {
component.totalPages = 5
component.page = 2
component.addSplit()
expect(component.confirmButtonEnabled).toBeTruthy()
})
it('should disable confirm button when all pages are removed', () => {
component.totalPages = 5
component.page = 2
component.addSplit()
component.removeSplit(0)
expect(component.confirmButtonEnabled).toBeFalsy()
})
it('should not add split if page is the last page', () => {
component.totalPages = 5
component.page = 5
component.addSplit()
expect(component.pagesString).toEqual('1-5')
})
it('should update totalPages when pdf is loaded', () => {
component.pdfPreviewLoaded({ numPages: 5 } as any)
expect(component.totalPages).toEqual(5)
})
it('should correctly disable split button', () => {
component.totalPages = 5
component.page = 1
expect(component.canSplit).toBeTruthy()
component.page = 5
expect(component.canSplit).toBeFalsy()
component.page = 4
expect(component.canSplit).toBeTruthy()
component['pages'] = new Set([1, 2, 3, 4])
expect(component.canSplit).toBeFalsy()
})
})

View File

@@ -0,0 +1,100 @@
import { Component, OnInit } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Document } from 'src/app/data/document'
import { PermissionsService } from 'src/app/services/permissions.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-split-confirm-dialog',
templateUrl: './split-confirm-dialog.component.html',
styleUrl: './split-confirm-dialog.component.scss',
imports: [
FormsModule,
ReactiveFormsModule,
NgxBootstrapIconsModule,
PdfViewerModule,
],
})
export class SplitConfirmDialogComponent
extends ConfirmDialogComponent
implements OnInit
{
public get pagesString(): string {
let pagesStr = ''
let lastPage = 1
for (let i = 1; i <= this.totalPages; i++) {
if (this.pages.has(i) || i === this.totalPages) {
if (lastPage === i) {
pagesStr += `${i},`
lastPage = Math.min(i + 1, this.totalPages)
} else {
pagesStr += `${lastPage}-${i},`
lastPage = Math.min(i + 1, this.totalPages)
}
}
}
return pagesStr.replace(/,$/, '')
}
private pages: Set<number> = new Set()
public documentID: number
private document: Document
public page: number = 1
public totalPages: number
public deleteOriginal: boolean = false
public get canSplit(): boolean {
return (
this.page < this.totalPages &&
this.pages.size < this.totalPages - 1 &&
!this.pages.has(this.page)
)
}
public get pdfSrc(): string {
return this.documentService.getPreviewUrl(this.documentID)
}
constructor(
activeModal: NgbActiveModal,
private documentService: DocumentService,
private permissionService: PermissionsService
) {
super(activeModal)
this.confirmButtonEnabled = this.pages.size > 0
}
ngOnInit(): void {
this.documentService.get(this.documentID).subscribe((r) => {
this.document = r
})
}
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
this.totalPages = pdf.numPages
}
addSplit() {
if (this.page === this.totalPages) return
this.pages.add(this.page)
this.pages = new Set(Array.from(this.pages).sort((a, b) => a - b))
this.confirmButtonEnabled = this.pages.size > 0
}
removeSplit(i: number) {
let page = Array.from(this.pages)[Math.min(i, this.pages.size - 1)]
this.pages.delete(page)
this.confirmButtonEnabled = this.pages.size > 0
}
get userOwnsDocument(): boolean {
return this.permissionService.currentUserOwnsObject(this.document)
}
}

View File

@@ -1,5 +1,5 @@
import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common' import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common'
import { Component, Input, LOCALE_ID, OnInit, inject } from '@angular/core' import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
@@ -20,9 +20,6 @@ export class CustomFieldDisplayComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnInit implements OnInit
{ {
private customFieldService = inject(CustomFieldsService)
private documentService = inject(DocumentService)
CustomFieldDataType = CustomFieldDataType CustomFieldDataType = CustomFieldDataType
private _document: Document private _document: Document
@@ -66,9 +63,11 @@ export class CustomFieldDisplayComponent
private defaultCurrencyCode: any private defaultCurrencyCode: any
constructor() { constructor(
const currentLocale = inject(LOCALE_ID) private customFieldService: CustomFieldsService,
private documentService: DocumentService,
@Inject(LOCALE_ID) currentLocale: string
) {
super() super()
this.defaultCurrencyCode = getLocaleCurrencyCode(currentLocale) this.defaultCurrencyCode = getLocaleCurrencyCode(currentLocale)
this.customFieldService.listAll().subscribe((r) => { this.customFieldService.listAll().subscribe((r) => {

View File

@@ -7,7 +7,6 @@ import {
QueryList, QueryList,
ViewChild, ViewChild,
ViewChildren, ViewChildren,
inject,
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -38,11 +37,6 @@ import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit
], ],
}) })
export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions { export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissions {
private customFieldsService = inject(CustomFieldsService)
private modalService = inject(NgbModal)
private toastService = inject(ToastService)
private permissionsService = inject(PermissionsService)
public popperOptions = pngxPopperOptions public popperOptions = pngxPopperOptions
@Input() @Input()
@@ -84,7 +78,12 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
) )
} }
constructor() { constructor(
private customFieldsService: CustomFieldsService,
private modalService: NgbModal,
private toastService: ToastService,
private permissionsService: PermissionsService
) {
super() super()
this.getFields() this.getFields()
} }

View File

@@ -2,7 +2,6 @@ import { NgTemplateOutlet } from '@angular/common'
import { import {
Component, Component,
EventEmitter, EventEmitter,
inject,
Input, Input,
Output, Output,
QueryList, QueryList,
@@ -179,8 +178,6 @@ export class CustomFieldQueriesModel {
], ],
}) })
export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions { export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions {
protected customFieldsService = inject(CustomFieldsService)
public CustomFieldQueryComponentType = CustomFieldQueryElementType public CustomFieldQueryComponentType = CustomFieldQueryElementType
public CustomFieldQueryOperator = CustomFieldQueryOperator public CustomFieldQueryOperator = CustomFieldQueryOperator
public CustomFieldDataType = CustomFieldDataType public CustomFieldDataType = CustomFieldDataType
@@ -246,9 +243,9 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
customFields: CustomField[] = [] customFields: CustomField[] = []
public readonly today: string = new Date().toLocaleDateString('en-CA') public readonly today: string = new Date().toISOString().split('T')[0]
constructor() { constructor(protected customFieldsService: CustomFieldsService) {
super() super()
this.selectionModel = new CustomFieldQueriesModel() this.selectionModel = new CustomFieldQueriesModel()
this.getFields() this.getFields()

View File

@@ -6,7 +6,6 @@ import {
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
inject,
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { import {
@@ -64,9 +63,7 @@ export enum RelativeDate {
export class DatesDropdownComponent implements OnInit, OnDestroy { export class DatesDropdownComponent implements OnInit, OnDestroy {
public popperOptions = pngxPopperOptions public popperOptions = pngxPopperOptions
constructor() { constructor(settings: SettingsService) {
const settings = inject(SettingsService)
this.datePlaceHolder = settings.getLocalizedDateInputFormat() this.datePlaceHolder = settings.getLocalizedDateInputFormat()
} }
@@ -165,7 +162,7 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
@Input() @Input()
placement: string = 'bottom-start' placement: string = 'bottom-start'
public readonly today: string = new Date().toLocaleDateString('en-CA') public readonly today: string = new Date().toISOString().split('T')[0]
get isActive(): boolean { get isActive(): boolean {
return ( return (

View File

@@ -13,6 +13,8 @@
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) { @if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check> <pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check>
} }

View File

@@ -1,10 +1,11 @@
import { Component, inject } from '@angular/core' import { Component } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Correspondent } from 'src/app/data/correspondent' import { Correspondent } from 'src/app/data/correspondent'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
@@ -12,7 +13,6 @@ import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { CheckComponent } from '../../input/check/check.component'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component' import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { SelectComponent } from '../../input/select/select.component' import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
@@ -22,7 +22,6 @@ import { TextComponent } from '../../input/text/text.component'
templateUrl: './correspondent-edit-dialog.component.html', templateUrl: './correspondent-edit-dialog.component.html',
styleUrls: ['./correspondent-edit-dialog.component.scss'], styleUrls: ['./correspondent-edit-dialog.component.scss'],
imports: [ imports: [
CheckComponent,
SelectComponent, SelectComponent,
PermissionsFormComponent, PermissionsFormComponent,
TextComponent, TextComponent,
@@ -32,11 +31,13 @@ import { TextComponent } from '../../input/text/text.component'
], ],
}) })
export class CorrespondentEditDialogComponent extends EditDialogComponent<Correspondent> { export class CorrespondentEditDialogComponent extends EditDialogComponent<Correspondent> {
constructor() { constructor(
super() service: CorrespondentService,
this.service = inject(CorrespondentService) activeModal: NgbActiveModal,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
} }
getCreateTitle() { getCreateTitle() {

View File

@@ -5,7 +5,6 @@ import {
OnInit, OnInit,
QueryList, QueryList,
ViewChildren, ViewChildren,
inject,
} from '@angular/core' } from '@angular/core'
import { import {
FormArray, FormArray,
@@ -14,6 +13,7 @@ import {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil } from 'rxjs' import { takeUntil } from 'rxjs'
import { import {
@@ -54,11 +54,13 @@ export class CustomFieldEditDialogComponent
.select_options as FormArray .select_options as FormArray
} }
constructor() { constructor(
super() service: CustomFieldsService,
this.service = inject(CustomFieldsService) activeModal: NgbActiveModal,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
} }
ngOnInit(): void { ngOnInit(): void {

View File

@@ -14,6 +14,8 @@
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) { @if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
} }
</div> </div>

View File

@@ -1,10 +1,11 @@
import { Component, inject } from '@angular/core' import { Component } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { DocumentType } from 'src/app/data/document-type' import { DocumentType } from 'src/app/data/document-type'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
@@ -12,7 +13,6 @@ import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { CheckComponent } from '../../input/check/check.component'
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component' import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
import { SelectComponent } from '../../input/select/select.component' import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
@@ -22,7 +22,6 @@ import { TextComponent } from '../../input/text/text.component'
templateUrl: './document-type-edit-dialog.component.html', templateUrl: './document-type-edit-dialog.component.html',
styleUrls: ['./document-type-edit-dialog.component.scss'], styleUrls: ['./document-type-edit-dialog.component.scss'],
imports: [ imports: [
CheckComponent,
SelectComponent, SelectComponent,
PermissionsFormComponent, PermissionsFormComponent,
TextComponent, TextComponent,
@@ -32,11 +31,13 @@ import { TextComponent } from '../../input/text/text.component'
], ],
}) })
export class DocumentTypeEditDialogComponent extends EditDialogComponent<DocumentType> { export class DocumentTypeEditDialogComponent extends EditDialogComponent<DocumentType> {
constructor() { constructor(
super() service: DocumentTypeService,
this.service = inject(DocumentTypeService) activeModal: NgbActiveModal,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
} }
getCreateTitle() { getCreateTitle() {

View File

@@ -41,9 +41,13 @@ import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
imports: [FormsModule, ReactiveFormsModule], imports: [FormsModule, ReactiveFormsModule],
}) })
class TestComponent extends EditDialogComponent<Tag> { class TestComponent extends EditDialogComponent<Tag> {
constructor() { constructor(
super() service: TagService,
this.service = TestBed.inject(TagService) activeModal: NgbActiveModal,
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
} }
getForm(): FormGroup<any> { getForm(): FormGroup<any> {

View File

@@ -1,11 +1,4 @@
import { import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
Directive,
EventEmitter,
Input,
OnInit,
Output,
inject,
} from '@angular/core'
import { FormGroup } from '@angular/forms' import { FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
@@ -36,12 +29,14 @@ export abstract class EditDialogComponent<
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnInit implements OnInit
{ {
protected service = inject<AbstractPaperlessService<T>>( constructor(
AbstractPaperlessService protected service: AbstractPaperlessService<T>,
) private activeModal: NgbActiveModal,
protected activeModal = inject(NgbActiveModal) private userService: UserService,
protected userService = inject(UserService) protected settingsService: SettingsService
protected settingsService = inject(SettingsService) ) {
super()
}
users: User[] users: User[]

View File

@@ -1,10 +1,11 @@
import { Component, inject } from '@angular/core' import { Component } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Group } from 'src/app/data/group' import { Group } from 'src/app/data/group'
import { GroupService } from 'src/app/services/rest/group.service' import { GroupService } from 'src/app/services/rest/group.service'
@@ -25,11 +26,13 @@ import { PermissionsSelectComponent } from '../../permissions-select/permissions
], ],
}) })
export class GroupEditDialogComponent extends EditDialogComponent<Group> { export class GroupEditDialogComponent extends EditDialogComponent<Group> {
constructor() { constructor(
super() service: GroupService,
this.service = inject(GroupService) activeModal: NgbActiveModal,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
} }
getCreateTitle() { getCreateTitle() {
@@ -43,7 +46,7 @@ export class GroupEditDialogComponent extends EditDialogComponent<Group> {
getForm(): FormGroup { getForm(): FormGroup {
return new FormGroup({ return new FormGroup({
name: new FormControl(''), name: new FormControl(''),
permissions: new FormControl([]), permissions: new FormControl(null),
}) })
} }
} }

View File

@@ -1,11 +1,15 @@
import { Component, ViewChild, inject } from '@angular/core' import { Component, ViewChild } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbAlert, NgbAlertModule } from '@ng-bootstrap/ng-bootstrap' import {
NgbActiveModal,
NgbAlert,
NgbAlertModule,
} from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { IMAPSecurity, MailAccount } from 'src/app/data/mail-account' import { IMAPSecurity, MailAccount } from 'src/app/data/mail-account'
import { MailAccountService } from 'src/app/services/rest/mail-account.service' import { MailAccountService } from 'src/app/services/rest/mail-account.service'
@@ -43,11 +47,13 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<MailAcco
@ViewChild('testResultAlert', { static: false }) testResultAlert: NgbAlert @ViewChild('testResultAlert', { static: false }) testResultAlert: NgbAlert
constructor() { constructor(
super() service: MailAccountService,
this.service = inject(MailAccountService) activeModal: NgbActiveModal,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
} }
getCreateTitle() { getCreateTitle() {

View File

@@ -1,10 +1,11 @@
import { Component, inject } from '@angular/core' import { Component } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs' import { first } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Correspondent } from 'src/app/data/correspondent' import { Correspondent } from 'src/app/data/correspondent'
@@ -154,34 +155,32 @@ const METADATA_CORRESPONDENT_OPTIONS = [
], ],
}) })
export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> { export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
private accountService: MailAccountService
private correspondentService: CorrespondentService
private documentTypeService: DocumentTypeService
accounts: MailAccount[] accounts: MailAccount[]
correspondents: Correspondent[] correspondents: Correspondent[]
documentTypes: DocumentType[] documentTypes: DocumentType[]
constructor() { constructor(
super() service: MailRuleService,
this.service = inject(MailRuleService) activeModal: NgbActiveModal,
this.accountService = inject(MailAccountService) accountService: MailAccountService,
this.correspondentService = inject(CorrespondentService) correspondentService: CorrespondentService,
this.documentTypeService = inject(DocumentTypeService) documentTypeService: DocumentTypeService,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
this.accountService accountService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.accounts = result.results)) .subscribe((result) => (this.accounts = result.results))
this.correspondentService correspondentService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.correspondents = result.results)) .subscribe((result) => (this.correspondents = result.results))
this.documentTypeService documentTypeService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.documentTypes = result.results)) .subscribe((result) => (this.documentTypes = result.results))

View File

@@ -64,6 +64,8 @@
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) { @if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
} }

View File

@@ -1,12 +1,12 @@
import { AsyncPipe, NgTemplateOutlet } from '@angular/common' import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
import { Component, OnDestroy, inject } from '@angular/core' import { Component, OnDestroy } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap' import { NgbAccordionModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectComponent } from '@ng-select/ng-select' import { NgSelectComponent } from '@ng-select/ng-select'
import { import {
Observable, Observable,
@@ -60,8 +60,6 @@ export class StoragePathEditDialogComponent
extends EditDialogComponent<StoragePath> extends EditDialogComponent<StoragePath>
implements OnDestroy implements OnDestroy
{ {
private documentsService = inject(DocumentService)
public documentsInput$ = new Subject<string>() public documentsInput$ = new Subject<string>()
public foundDocuments$: Observable<Document[]> public foundDocuments$: Observable<Document[]>
private testDocument: Document private testDocument: Document
@@ -70,11 +68,14 @@ export class StoragePathEditDialogComponent
public loading = false public loading = false
public testLoading = false public testLoading = false
constructor() { constructor(
super() service: StoragePathService,
this.service = inject(StoragePathService) activeModal: NgbActiveModal,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService,
private documentsService: DocumentService
) {
super(service, activeModal, userService, settingsService)
this.initPathObservables() this.initPathObservables()
} }

View File

@@ -16,6 +16,8 @@
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) { @if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
}
@if (patternRequired) {
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check> <pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
} }

View File

@@ -1,10 +1,11 @@
import { Component, inject } from '@angular/core' import { Component } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
@@ -35,11 +36,13 @@ import { TextComponent } from '../../input/text/text.component'
], ],
}) })
export class TagEditDialogComponent extends EditDialogComponent<Tag> { export class TagEditDialogComponent extends EditDialogComponent<Tag> {
constructor() { constructor(
super() service: TagService,
this.service = inject(TagService) activeModal: NgbActiveModal,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
} }
getCreateTitle() { getCreateTitle() {

View File

@@ -1,10 +1,11 @@
import { Component, OnInit, inject } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs' import { first } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Group } from 'src/app/data/group' import { Group } from 'src/app/data/group'
@@ -36,21 +37,21 @@ export class UserEditDialogComponent
extends EditDialogComponent<User> extends EditDialogComponent<User>
implements OnInit implements OnInit
{ {
private toastService = inject(ToastService)
private permissionsService = inject(PermissionsService)
private groupsService: GroupService
groups: Group[] groups: Group[]
passwordIsSet: boolean = false passwordIsSet: boolean = false
public totpLoading: boolean = false public totpLoading: boolean = false
constructor() { constructor(
super() service: UserService,
this.service = inject(UserService) activeModal: NgbActiveModal,
this.groupsService = inject(GroupService) groupsService: GroupService,
this.settingsService = inject(SettingsService) settingsService: SettingsService,
private toastService: ToastService,
private permissionsService: PermissionsService
) {
super(service, activeModal, service, settingsService)
this.groupsService groupsService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.groups = result.results)) .subscribe((result) => (this.groups = result.results))

View File

@@ -129,7 +129,7 @@
formControlName="schedule_offset_days" formControlName="schedule_offset_days"
[showAdd]="false" [showAdd]="false"
[error]="error?.schedule_offset_days" [error]="error?.schedule_offset_days"
hint="Positive values will trigger after the date, negative values before." hint="Positive values will trigger the workflow before the date, negative values after."
i18n-hint i18n-hint
></pngx-input-number> ></pngx-input-number>
</div> </div>

View File

@@ -4,7 +4,7 @@ import {
moveItemInArray, moveItemInArray,
} from '@angular/cdk/drag-drop' } from '@angular/cdk/drag-drop'
import { NgTemplateOutlet } from '@angular/common' import { NgTemplateOutlet } from '@angular/common'
import { Component, OnInit, inject } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { import {
FormArray, FormArray,
FormControl, FormControl,
@@ -12,7 +12,7 @@ import {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap' import { NgbAccordionModule, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first } from 'rxjs' import { first } from 'rxjs'
import { Correspondent } from 'src/app/data/correspondent' import { Correspondent } from 'src/app/data/correspondent'
@@ -171,12 +171,6 @@ export class WorkflowEditDialogComponent
public WorkflowTriggerType = WorkflowTriggerType public WorkflowTriggerType = WorkflowTriggerType
public WorkflowActionType = WorkflowActionType public WorkflowActionType = WorkflowActionType
private correspondentService: CorrespondentService
private documentTypeService: DocumentTypeService
private storagePathService: StoragePathService
private mailRuleService: MailRuleService
private customFieldsService: CustomFieldsService
templates: Workflow[] templates: Workflow[]
correspondents: Correspondent[] correspondents: Correspondent[]
documentTypes: DocumentType[] documentTypes: DocumentType[]
@@ -189,38 +183,40 @@ export class WorkflowEditDialogComponent
private allowedActionTypes = [] private allowedActionTypes = []
constructor() { constructor(
super() service: WorkflowService,
this.service = inject(WorkflowService) activeModal: NgbActiveModal,
this.correspondentService = inject(CorrespondentService) correspondentService: CorrespondentService,
this.documentTypeService = inject(DocumentTypeService) documentTypeService: DocumentTypeService,
this.storagePathService = inject(StoragePathService) storagePathService: StoragePathService,
this.mailRuleService = inject(MailRuleService) mailRuleService: MailRuleService,
this.userService = inject(UserService) userService: UserService,
this.settingsService = inject(SettingsService) settingsService: SettingsService,
this.customFieldsService = inject(CustomFieldsService) customFieldsService: CustomFieldsService
) {
super(service, activeModal, userService, settingsService)
this.correspondentService correspondentService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.correspondents = result.results)) .subscribe((result) => (this.correspondents = result.results))
this.documentTypeService documentTypeService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.documentTypes = result.results)) .subscribe((result) => (this.documentTypes = result.results))
this.storagePathService storagePathService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.storagePaths = result.results)) .subscribe((result) => (this.storagePaths = result.results))
this.mailRuleService mailRuleService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.mailRules = result.results)) .subscribe((result) => (this.mailRules = result.results))
this.customFieldsService customFieldsService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => { .subscribe((result) => {

View File

@@ -1,4 +1,4 @@
import { Component, Input, inject } from '@angular/core' import { Component, Input } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@@ -13,10 +13,6 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
imports: [FormsModule, NgxBootstrapIconsModule], imports: [FormsModule, NgxBootstrapIconsModule],
}) })
export class EmailDocumentDialogComponent extends LoadingComponentWithPermissions { export class EmailDocumentDialogComponent extends LoadingComponentWithPermissions {
private activeModal = inject(NgbActiveModal)
private documentService = inject(DocumentService)
private toastService = inject(ToastService)
@Input() @Input()
title = $localize`Email Document` title = $localize`Email Document`
@@ -41,7 +37,11 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
public emailSubject: string = '' public emailSubject: string = ''
public emailMessage: string = '' public emailMessage: string = ''
constructor() { constructor(
private activeModal: NgbActiveModal,
private documentService: DocumentService,
private toastService: ToastService
) {
super() super()
this.loading = false this.loading = false
} }

View File

@@ -30,7 +30,7 @@
} }
<div class="list-group-item"> <div class="list-group-item">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input class="form-control" type="text" spellcheck="false" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput> <input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div> </div>
</div> </div>
@if (selectionModel.items) { @if (selectionModel.items) {

View File

@@ -7,7 +7,6 @@ import {
OnInit, OnInit,
Output, Output,
ViewChild, ViewChild,
inject,
} from '@angular/core' } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
@@ -435,9 +434,6 @@ export class FilterableDropdownComponent
extends LoadingComponentWithPermissions extends LoadingComponentWithPermissions
implements OnInit implements OnInit
{ {
private filterPipe = inject(FilterPipe)
private hotkeyService = inject(HotKeyService)
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
@ViewChild('dropdown') dropdown: NgbDropdown @ViewChild('dropdown') dropdown: NgbDropdown
@ViewChild('buttonItems') buttonItems: ElementRef @ViewChild('buttonItems') buttonItems: ElementRef
@@ -540,7 +536,10 @@ export class FilterableDropdownComponent
private keyboardIndex: number private keyboardIndex: number
constructor() { constructor(
private filterPipe: FilterPipe,
private hotkeyService: HotKeyService
) {
super() super()
this.selectionModelChange.subscribe((updatedModel) => { this.selectionModelChange.subscribe((updatedModel) => {
this.modelIsDirty = updatedModel.isDirty() this.modelIsDirty = updatedModel.isDirty()

View File

@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core' import { Component } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
const SYMBOLS = { const SYMBOLS = {
@@ -19,11 +19,11 @@ const SYMBOLS = {
styleUrl: './hotkey-dialog.component.scss', styleUrl: './hotkey-dialog.component.scss',
}) })
export class HotkeyDialogComponent { export class HotkeyDialogComponent {
activeModal = inject(NgbActiveModal)
public title: string = $localize`Keyboard shortcuts` public title: string = $localize`Keyboard shortcuts`
public hotkeys: Map<string, string> = new Map() public hotkeys: Map<string, string> = new Map()
constructor(public activeModal: NgbActiveModal) {}
public close(): void { public close(): void {
this.activeModal.close() this.activeModal.close()
} }

View File

@@ -2,7 +2,6 @@ import {
Component, Component,
EventEmitter, EventEmitter,
forwardRef, forwardRef,
inject,
Input, Input,
Output, Output,
} from '@angular/core' } from '@angular/core'
@@ -56,9 +55,7 @@ import { UrlComponent } from '../url/url.component'
export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> { export class CustomFieldsValuesComponent extends AbstractInputComponent<Object> {
public CustomFieldDataType = CustomFieldDataType public CustomFieldDataType = CustomFieldDataType
constructor() { constructor(customFieldsService: CustomFieldsService) {
const customFieldsService = inject(CustomFieldsService)
super() super()
customFieldsService.listAll().subscribe((items) => { customFieldsService.listAll().subscribe((items) => {
this.fields = items.results this.fields = items.results

View File

@@ -2,7 +2,6 @@ import {
Component, Component,
EventEmitter, EventEmitter,
forwardRef, forwardRef,
inject,
Input, Input,
OnInit, OnInit,
Output, Output,
@@ -46,9 +45,13 @@ export class DateComponent
extends AbstractInputComponent<string> extends AbstractInputComponent<string>
implements OnInit implements OnInit
{ {
private settings = inject(SettingsService) constructor(
private ngbDateParserFormatter = inject(NgbDateParserFormatter) private settings: SettingsService,
private isoDateAdapter = inject<NgbDateAdapter<string>>(NgbDateAdapter) private ngbDateParserFormatter: NgbDateParserFormatter,
private isoDateAdapter: NgbDateAdapter<string>
) {
super()
}
@Input() @Input()
suggestions: string[] suggestions: string[]
@@ -59,7 +62,7 @@ export class DateComponent
@Output() @Output()
filterDocuments = new EventEmitter<NgbDateStruct[]>() filterDocuments = new EventEmitter<NgbDateStruct[]>()
public readonly today: string = new Date().toLocaleDateString('en-CA') public readonly today: string = new Date().toISOString().split('T')[0]
getSuggestions() { getSuggestions() {
return this.suggestions == null return this.suggestions == null

View File

@@ -1,12 +1,5 @@
import { AsyncPipe, NgTemplateOutlet } from '@angular/common' import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
import { import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'
Component,
forwardRef,
inject,
Input,
OnDestroy,
OnInit,
} from '@angular/core'
import { import {
FormsModule, FormsModule,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
@@ -59,8 +52,6 @@ export class DocumentLinkComponent
extends AbstractInputComponent<any[]> extends AbstractInputComponent<any[]>
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
private documentsService = inject(DocumentService)
documentsInput$ = new Subject<string>() documentsInput$ = new Subject<string>()
foundDocuments$: Observable<Document[]> foundDocuments$: Observable<Document[]>
loading = false loading = false
@@ -84,6 +75,10 @@ export class DocumentLinkComponent
return this.selectedDocuments.map((d) => d.id) return this.selectedDocuments.map((d) => d.id)
} }
constructor(private documentsService: DocumentService) {
super()
}
ngOnInit() { ngOnInit() {
this.loadDocs() this.loadDocs()
} }

View File

@@ -1,6 +1,5 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { LOCALE_ID } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NG_VALUE_ACCESSOR } from '@angular/forms' import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { MonetaryComponent } from './monetary.component' import { MonetaryComponent } from './monetary.component'
@@ -42,6 +41,8 @@ describe('MonetaryComponent', () => {
it('should set the default currency code based on LOCALE_ID', () => { it('should set the default currency code based on LOCALE_ID', () => {
expect(component.defaultCurrencyCode).toEqual('USD') // default expect(component.defaultCurrencyCode).toEqual('USD') // default
component = new MonetaryComponent('pt-BR')
expect(component.defaultCurrencyCode).toEqual('BRL')
}) })
it('should support setting a default currency code', () => { it('should support setting a default currency code', () => {
@@ -86,28 +87,3 @@ describe('MonetaryComponent', () => {
expect(component.value).toEqual('USD0.00') expect(component.value).toEqual('USD0.00')
}) })
}) })
describe('MonetaryComponent (Alternate Locale)', () => {
let component: MonetaryComponent
let fixture: ComponentFixture<MonetaryComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MonetaryComponent],
providers: [
{ provide: LOCALE_ID, useValue: 'pt-BR' }, // Brazilian Portuguese
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents()
fixture = TestBed.createComponent(MonetaryComponent)
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should set the default currency code based on LOCALE_ID', () => {
expect(component.defaultCurrencyCode).toEqual('BRL')
})
})

View File

@@ -1,5 +1,5 @@
import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common' import { CurrencyPipe, getLocaleCurrencyCode } from '@angular/common'
import { Component, forwardRef, inject, Input, LOCALE_ID } from '@angular/core' import { Component, forwardRef, Inject, Input, LOCALE_ID } from '@angular/core'
import { import {
FormsModule, FormsModule,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
@@ -27,8 +27,6 @@ import { AbstractInputComponent } from '../abstract-input'
], ],
}) })
export class MonetaryComponent extends AbstractInputComponent<string> { export class MonetaryComponent extends AbstractInputComponent<string> {
currentLocale = inject(LOCALE_ID)
public currency: string = '' public currency: string = ''
public _monetaryValue: string = '' public _monetaryValue: string = ''
@@ -47,10 +45,11 @@ export class MonetaryComponent extends AbstractInputComponent<string> {
if (currency) this.defaultCurrencyCode = currency if (currency) this.defaultCurrencyCode = currency
} }
constructor() { constructor(@Inject(LOCALE_ID) currentLocale: string) {
super() super()
this.currency = this.defaultCurrencyCode = this.currency = this.defaultCurrencyCode =
this.defaultCurrency ?? getLocaleCurrencyCode(this.currentLocale) this.defaultCurrency ?? getLocaleCurrencyCode(currentLocale)
} }
writeValue(newValue: any): void { writeValue(newValue: any): void {

View File

@@ -1,4 +1,4 @@
import { Component, forwardRef, inject, Input } from '@angular/core' import { Component, forwardRef, Input } from '@angular/core'
import { import {
FormsModule, FormsModule,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
@@ -22,14 +22,16 @@ import { AbstractInputComponent } from '../abstract-input'
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule], imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
}) })
export class NumberComponent extends AbstractInputComponent<number> { export class NumberComponent extends AbstractInputComponent<number> {
private documentService = inject(DocumentService)
@Input() @Input()
showAdd: boolean = true showAdd: boolean = true
@Input() @Input()
step: number = 1 step: number = 1
constructor(private documentService: DocumentService) {
super()
}
nextAsn() { nextAsn() {
if (this.value) { if (this.value) {
return return

View File

@@ -1,4 +1,4 @@
import { Component, forwardRef, inject } from '@angular/core' import { Component, forwardRef } from '@angular/core'
import { import {
FormsModule, FormsModule,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
@@ -26,9 +26,7 @@ import { AbstractInputComponent } from '../../abstract-input'
export class PermissionsGroupComponent extends AbstractInputComponent<Group> { export class PermissionsGroupComponent extends AbstractInputComponent<Group> {
groups: Group[] groups: Group[]
constructor() { constructor(groupService: GroupService) {
const groupService = inject(GroupService)
super() super()
groupService groupService
.listAll() .listAll()

View File

@@ -1,4 +1,4 @@
import { Component, forwardRef, inject } from '@angular/core' import { Component, forwardRef } from '@angular/core'
import { import {
FormsModule, FormsModule,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
@@ -8,6 +8,7 @@ import { NgSelectComponent } from '@ng-select/ng-select'
import { first } from 'rxjs/operators' import { first } from 'rxjs/operators'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { AbstractInputComponent } from '../../abstract-input' import { AbstractInputComponent } from '../../abstract-input'
@Component({ @Component({
@@ -26,9 +27,7 @@ import { AbstractInputComponent } from '../../abstract-input'
export class PermissionsUserComponent extends AbstractInputComponent<User[]> { export class PermissionsUserComponent extends AbstractInputComponent<User[]> {
users: User[] users: User[]
constructor() { constructor(userService: UserService, settings: SettingsService) {
const userService = inject(UserService)
super() super()
userService userService
.listAll() .listAll()

View File

@@ -2,7 +2,6 @@ import {
Component, Component,
EventEmitter, EventEmitter,
forwardRef, forwardRef,
inject,
Input, Input,
OnInit, OnInit,
Output, Output,
@@ -46,10 +45,10 @@ import { TagComponent } from '../../tag/tag.component'
], ],
}) })
export class TagsComponent implements OnInit, ControlValueAccessor { export class TagsComponent implements OnInit, ControlValueAccessor {
private tagService = inject(TagService) constructor(
private modalService = inject(NgbModal) private tagService: TagService,
private modalService: NgbModal
constructor() { ) {
this.createTagRef = this.createTag.bind(this) this.createTagRef = this.createTag.bind(this)
} }

View File

@@ -1,4 +1,4 @@
import { Component, Input, inject } from '@angular/core' import { Component, Input } from '@angular/core'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
@@ -9,8 +9,6 @@ import { environment } from 'src/environments/environment'
styleUrls: ['./logo.component.scss'], styleUrls: ['./logo.component.scss'],
}) })
export class LogoComponent { export class LogoComponent {
private settingsService = inject(SettingsService)
@Input() @Input()
extra_classes: string extra_classes: string
@@ -26,6 +24,8 @@ export class LogoComponent {
: null : null
} }
constructor(private settingsService: SettingsService) {}
getClasses() { getClasses() {
return ['logo'].concat(this.extra_classes).join(' ') return ['logo'].concat(this.extra_classes).join(' ')
} }

View File

@@ -1,4 +1,4 @@
import { Component, Input, inject } from '@angular/core' import { Component, Input } from '@angular/core'
import { Title } from '@angular/platform-browser' import { Title } from '@angular/platform-browser'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
@@ -12,7 +12,7 @@ import { environment } from 'src/environments/environment'
imports: [NgbPopoverModule, NgxBootstrapIconsModule, TourNgBootstrapModule], imports: [NgbPopoverModule, NgxBootstrapIconsModule, TourNgBootstrapModule],
}) })
export class PageHeaderComponent { export class PageHeaderComponent {
private titleService = inject(Title) constructor(private titleService: Title) {}
_title = '' _title = ''

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