diff --git a/.dockerignore b/.dockerignore index c33ee6452..8c39dd615 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,3 +26,5 @@ ./dist ./scripts ./resources +# Other stuff +**/*.drawio.png diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d00a8cb43..5e090a227 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,7 +131,7 @@ jobs: - name: Configure ImageMagick run: | - sudo cp docker/imagemagick-policy.xml /etc/ImageMagick-6/policy.xml + sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml - name: Install Python dependencies run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ddffaf9f..10f9d0084 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: - 'prettier-plugin-organize-imports@4.1.0' # Python hooks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.3 + rev: v0.9.4 hooks: - id: ruff - id: ruff-format diff --git a/.ruff.toml b/.ruff.toml index 96ee7430b..ae1bed609 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -32,13 +32,13 @@ extend-select = [ "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf "FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly "PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt ] ignore = ["DJ001", "SIM105", "RUF012"] [lint.per-file-ignores] ".github/scripts/*.py" = ["E501", "INP001", "SIM117"] "docker/wait-for-redis.py" = ["INP001", "T201"] -"src/documents/consumer.py" = ["PTH"] # TODO Enable & remove "src/documents/file_handling.py" = ["PTH"] # TODO Enable & remove "src/documents/management/commands/document_consumer.py" = ["PTH"] # TODO Enable & remove "src/documents/management/commands/document_exporter.py" = ["PTH"] # TODO Enable & remove @@ -51,8 +51,6 @@ ignore = ["DJ001", "SIM105", "RUF012"] "src/documents/signals/handlers.py" = ["PTH"] # TODO Enable & remove "src/documents/tasks.py" = ["PTH"] # TODO Enable & remove "src/documents/tests/test_api_app_config.py" = ["PTH"] # TODO Enable & remove -"src/documents/tests/test_api_bulk_download.py" = ["PTH"] # TODO Enable & remove -"src/documents/tests/test_api_documents.py" = ["PTH"] # TODO Enable & remove "src/documents/tests/test_classifier.py" = ["PTH"] # TODO Enable & remove "src/documents/tests/test_consumer.py" = ["PTH"] # TODO Enable & remove "src/documents/tests/test_file_handling.py" = ["PTH"] # TODO Enable & remove @@ -78,8 +76,12 @@ ignore = ["DJ001", "SIM105", "RUF012"] "src/paperless_tesseract/tests/test_parser.py" = ["RUF001", "PTH"] # TODO PTH Enable & remove "src/paperless_tika/tests/test_live_tika.py" = ["PTH"] # TODO Enable & remove "src/paperless_tika/tests/test_tika_parser.py" = ["PTH"] # TODO Enable & remove +# Testing "*/tests/*.py" = ["E501", "SIM117"] +# Migrations "*/migrations/*.py" = ["E501", "SIM", "T201"] +# Docker specific +"docker/rootfs/usr/local/bin/wait-for-redis.py" = ["INP001", "T201"] [lint.isort] force-single-line = true diff --git a/Dockerfile b/Dockerfile index d3be53dff..6a575be74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,11 +43,66 @@ RUN set -eux \ && echo "Generating requirement.txt" \ && pipenv requirements > requirements.txt +# Stage: s6-overlay-base +# Purpose: Installs s6-overlay and rootfs +# Comments: +# - Don't leave anything extra in here either +FROM docker.io/python:3.12-slim-bookworm AS s6-overlay-base + +WORKDIR /usr/src/s6 + +# https://github.com/just-containers/s6-overlay#customizing-s6-overlay-behaviour +ENV \ + S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \ + S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ + S6_VERBOSITY=1 \ + PATH=/command:$PATH + +# Buildx provided, must be defined to use though +ARG TARGETARCH +ARG TARGETVARIANT +# Lock this version +ARG S6_OVERLAY_VERSION=3.2.0.2 + +ARG S6_BUILD_TIME_PKGS="curl \ + xz-utils" + +RUN set -eux \ + && echo "Installing build time packages" \ + && apt-get update \ + && apt-get install --yes --quiet --no-install-recommends ${S6_BUILD_TIME_PKGS} \ + && echo "Determining arch" \ + && S6_ARCH="" \ + && if [ "${TARGETARCH}${TARGETVARIANT}" = "amd64" ]; then S6_ARCH="x86_64"; \ + elif [ "${TARGETARCH}${TARGETVARIANT}" = "arm64" ]; then S6_ARCH="aarch64"; fi\ + && if [ -z "${S6_ARCH}" ]; then { echo "Error: Not able to determine arch"; exit 1; }; fi \ + && echo "Installing s6-overlay for ${S6_ARCH}" \ + && curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \ + "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" \ + "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" \ + "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" \ + "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" \ + && echo "Validating s6-archive checksums" \ + && sha256sum --check ./*.sha256 \ + && echo "Unpacking archives" \ + && tar --directory / -Jxpf s6-overlay-noarch.tar.xz \ + && tar --directory / -Jxpf s6-overlay-${S6_ARCH}.tar.xz \ + && echo "Removing downloaded archives" \ + && rm ./*.tar.xz \ + && rm ./*.sha256 \ + && echo "Cleaning up image" \ + && apt-get --yes purge ${S6_BUILD_TIME_PKGS} \ + && apt-get --yes autoremove --purge \ + && rm -rf /var/lib/apt/lists/* + +# Copy our service defs and filesystem +COPY ./docker/rootfs / + # Stage: main-app # Purpose: The final image # Comments: # - Don't leave anything extra in here -FROM docker.io/python:3.12-slim-bookworm AS main-app +FROM s6-overlay-base AS main-app LABEL org.opencontainers.image.authors="paperless-ngx team " LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/" @@ -127,91 +182,39 @@ RUN set -eux \ && apt-get update \ && apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \ && echo "Installing pre-built updates" \ + && curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \ + https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \ + https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \ + https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \ + https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \ + https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \ + https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \ && echo "Installing qpdf ${QPDF_VERSION}" \ - && curl --fail --silent --show-error --location \ - --output libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \ - https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \ - && curl --fail --silent --show-error --location \ - --output qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \ - https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \ && dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \ && dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \ && echo "Installing Ghostscript ${GS_VERSION}" \ - && curl --fail --silent --show-error --location \ - --output libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \ - https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \ - && curl --fail --silent --show-error --location \ - --output ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \ - https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \ - && curl --fail --silent --show-error --location \ - --output libgs10-common_${GS_VERSION}.dfsg-1_all.deb \ - https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \ && dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \ && dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \ && dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \ && echo "Installing jbig2enc" \ - && curl --fail --silent --show-error --location \ - --output jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \ - https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \ && dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \ + && echo "Configuring imagemagick" \ + && cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \ && echo "Cleaning up image layer" \ && rm --force --verbose *.deb \ - && rm --recursive --force --verbose /var/lib/apt/lists/* \ - && echo "Installing supervisor" \ - && python3 -m pip install --default-timeout=1000 --upgrade --no-cache-dir supervisor==4.2.5 + && rm --recursive --force --verbose /var/lib/apt/lists/* # Copy gunicorn config # Changes very infrequently WORKDIR /usr/src/paperless/ -COPY gunicorn.conf.py . - -# setup docker-specific things -# These change sometimes, but rarely -WORKDIR /usr/src/paperless/src/docker/ - -COPY [ \ - "docker/imagemagick-policy.xml", \ - "docker/supervisord.conf", \ - "docker/docker-entrypoint.sh", \ - "docker/docker-prepare.sh", \ - "docker/paperless_cmd.sh", \ - "docker/wait-for-redis.py", \ - "docker/env-from-file.sh", \ - "docker/management_script.sh", \ - "docker/flower-conditional.sh", \ - "docker/install_management_commands.sh", \ - "/usr/src/paperless/src/docker/" \ -] - -RUN set -eux \ - && echo "Configuring ImageMagick" \ - && mv imagemagick-policy.xml /etc/ImageMagick-6/policy.xml \ - && echo "Configuring supervisord" \ - && mkdir /var/log/supervisord /var/run/supervisord \ - && mv supervisord.conf /etc/supervisord.conf \ - && echo "Setting up Docker scripts" \ - && mv docker-entrypoint.sh /sbin/docker-entrypoint.sh \ - && chmod 755 /sbin/docker-entrypoint.sh \ - && mv docker-prepare.sh /sbin/docker-prepare.sh \ - && chmod 755 /sbin/docker-prepare.sh \ - && mv wait-for-redis.py /sbin/wait-for-redis.py \ - && chmod 755 /sbin/wait-for-redis.py \ - && mv env-from-file.sh /sbin/env-from-file.sh \ - && chmod 755 /sbin/env-from-file.sh \ - && mv paperless_cmd.sh /usr/local/bin/paperless_cmd.sh \ - && chmod 755 /usr/local/bin/paperless_cmd.sh \ - && mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \ - && chmod 755 /usr/local/bin/flower-conditional.sh \ - && echo "Installing management commands" \ - && chmod +x install_management_commands.sh \ - && ./install_management_commands.sh +COPY --chown=1000:1000 gunicorn.conf.py /usr/src/paperless/gunicorn.conf.py WORKDIR /usr/src/paperless/src/ # Python dependencies # Change pretty frequently -COPY --from=pipenv-base /usr/src/pipenv/requirements.txt ./ +COPY --chown=1000:1000 --from=pipenv-base /usr/src/pipenv/requirements.txt ./ # Packages needed only for building a few quick Python # dependencies @@ -224,20 +227,22 @@ ARG BUILD_PACKAGES="\ default-libmysqlclient-dev \ pkg-config" +ARG ZXING_VERSION=2.3.0 +ARG PSYCOPG_VERSION=3.2.4 + # hadolint ignore=DL3042 RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \ set -eux \ && echo "Installing build system packages" \ && apt-get update \ && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \ - && python3 -m pip install --no-cache-dir --upgrade wheel \ + && python3 -m pip install --upgrade wheel \ && echo "Installing Python requirements" \ - && curl --fail --silent --show-error --location \ - --output psycopg_c-3.2.4-cp312-cp312-linux_x86_64.whl \ - https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.4/psycopg_c-3.2.4-cp312-cp312-linux_x86_64.whl \ - && curl --fail --silent --show-error --location \ - --output psycopg_c-3.2.4-cp312-cp312-linux_aarch64.whl \ - https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.4/psycopg_c-3.2.4-cp312-cp312-linux_aarch64.whl \ + && curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \ + https://github.com/paperless-ngx/builder/releases/download/psycopg-${PSYCOPG_VERSION}/psycopg_c-${PSYCOPG_VERSION}-cp312-cp312-linux_x86_64.whl \ + https://github.com/paperless-ngx/builder/releases/download/psycopg-${PSYCOPG_VERSION}/psycopg_c-${PSYCOPG_VERSION}-cp312-cp312-linux_aarch64.whl \ + https://github.com/paperless-ngx/builder/releases/download/zxing-${ZXING_VERSION}/zxing_cpp-${ZXING_VERSION}-cp312-cp312-linux_aarch64.whl \ + https://github.com/paperless-ngx/builder/releases/download/zxing-${ZXING_VERSION}/zxing_cpp-${ZXING_VERSION}-cp312-cp312-linux_x86_64.whl \ && python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \ && echo "Installing NLTK data" \ && python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \ @@ -276,18 +281,16 @@ RUN set -eux \ && echo "Adjusting all permissions" \ && chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \ && echo "Collecting static files" \ - && gosu paperless python3 manage.py collectstatic --clear --no-input --link \ - && gosu paperless python3 manage.py compilemessages + && s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \ + && s6-setuidgid paperless python3 manage.py compilemessages VOLUME ["/usr/src/paperless/data", \ "/usr/src/paperless/media", \ "/usr/src/paperless/consume", \ "/usr/src/paperless/export"] -ENTRYPOINT ["/sbin/docker-entrypoint.sh"] +ENTRYPOINT ["/init"] EXPOSE 8000 -CMD ["/usr/local/bin/paperless_cmd.sh"] - HEALTHCHECK --interval=30s --timeout=10s --retries=5 CMD [ "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000" ] diff --git a/Pipfile b/Pipfile index 98c95fa9a..863b157ec 100644 --- a/Pipfile +++ b/Pipfile @@ -60,7 +60,7 @@ uvicorn = {extras = ["standard"], version = "==0.25.0"} watchdog = "~=6.0" whitenoise = "~=6.8" whoosh = "~=2.7" -zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"} +zxing-cpp = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 2b7aae17b..aed779ecb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -597,12 +597,12 @@ }, "django-soft-delete": { "hashes": [ - "sha256:cc40398ccd869c75a6d6ba7f526e16c4afe2b0c0811c213a318d96bb4c58a787", - "sha256:fdaf2788d404930557f1300ce40bbd764f6938775a35a3175c66fe7778666093" + "sha256:603a29e82bbb7a5bada69f2754fad225ccd8cd7f485320ec06d0fc4e9dfddcf0", + "sha256:d2f9db449a4f008e9786f82fa4bafbe4075f7a0b3284844735007e988b2a4df6" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==1.0.16" + "version": "==1.0.18" }, "djangorestframework": { "hashes": [ @@ -2418,7 +2418,7 @@ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.11'", "version": "==4.12.2" }, "tzdata": { @@ -2953,7 +2953,6 @@ "sha256:fbd5b253ad0f8823c5c104feaaa19acab95c217cb924b012d55ff339c42b3583", "sha256:fd3f175f7b57cfbdea56afdb5335eaebaadeebc06e20a087d9aa3f99637c4aa5" ], - "markers": "platform_machine == 'x86_64'", "version": "==2.3.0" } }, @@ -3000,19 +2999,19 @@ }, "babel": { "hashes": [ - "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", - "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316" + "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", + "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2" ], "markers": "python_version >= '3.8'", - "version": "==2.16.0" + "version": "==2.17.0" }, "certifi": { "hashes": [ - "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", - "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db" + "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe" ], "markers": "python_version >= '3.6'", - "version": "==2024.12.14" + "version": "==2025.1.31" }, "cffi": { "hashes": [ @@ -3465,7 +3464,6 @@ "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" ], - "index": "pypi", "markers": "python_version >= '3.7'", "version": "==3.1.5" }, @@ -3578,12 +3576,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:ae5fe16f3d7c9ccd05bb6916a7da7420cf99a9ce5e33debd9d40403a090d5825", - "sha256:f24100f234741f4d423a9d672a909d859668a4f404796be3cf035f10d6050385" + "sha256:71d90dbd63b393ad11a4d90151dfe3dcbfcd802c0f29ce80bebd9bbac6abc753", + "sha256:a3de1c5d4c745f10afa78b1a02f917b9dce0808fb206adc0f5bb48b58c1ca21f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.50" + "version": "==9.6.2" }, "mkdocs-material-extensions": { "hashes": [ @@ -3821,11 +3819,11 @@ }, "pymdown-extensions": { "hashes": [ - "sha256:637951cbfbe9874ba28134fb3ce4b8bcadd6aca89ac4998ec29dcbafd554ae08", - "sha256:b65801996a0cd4f42a3110810c306c45b7313c09b0610a6f773730f2a9e3c96b" + "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", + "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b" ], "markers": "python_version >= '3.8'", - "version": "==10.14.1" + "version": "==10.14.3" }, "pyopenssl": { "hashes": [ @@ -3920,8 +3918,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, "pywavelets": { @@ -4145,28 +4142,28 @@ }, "ruff": { "hashes": [ - "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2", - "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4", - "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439", - "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730", - "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4", - "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5", - "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624", - "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b", - "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a", - "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b", - "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5", - "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4", - "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c", - "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519", - "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1", - "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4", - "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6", - "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c" + "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e", + "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214", + "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137", + "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c", + "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b", + "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b", + "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41", + "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706", + "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7", + "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf", + "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec", + "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6", + "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231", + "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0", + "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402", + "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e", + "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a", + "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.9.3" + "version": "==0.9.4" }, "scipy": { "hashes": [ @@ -4235,7 +4232,7 @@ "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.17.0" }, "sniffio": { @@ -4368,7 +4365,6 @@ "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2" ], - "index": "pypi", "markers": "python_version >= '3.9'", "version": "==6.0.0" }, diff --git a/docker/docker-prepare.sh b/docker/docker-prepare.sh deleted file mode 100755 index e3d924535..000000000 --- a/docker/docker-prepare.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env bash - -set -e - -wait_for_postgres() { - local attempt_num=1 - local -r max_attempts=5 - - echo "Waiting for PostgreSQL to start..." - - local -r host="${PAPERLESS_DBHOST:-localhost}" - local -r port="${PAPERLESS_DBPORT:-5432}" - - # Disable warning, host and port can't have spaces - # shellcheck disable=SC2086 - while [ ! "$(pg_isready --host ${host} --port ${port})" ]; do - - if [ $attempt_num -eq $max_attempts ]; then - echo "Unable to connect to database." - exit 1 - else - echo "Attempt $attempt_num failed! Trying again in 5 seconds..." - fi - - attempt_num=$(("$attempt_num" + 1)) - sleep 5 - done - echo "Connected to PostgreSQL" -} - -wait_for_mariadb() { - echo "Waiting for MariaDB to start..." - - local -r host="${PAPERLESS_DBHOST:=localhost}" - local -r port="${PAPERLESS_DBPORT:=3306}" - - local attempt_num=1 - local -r max_attempts=5 - - # Disable warning, host and port can't have spaces - # shellcheck disable=SC2086 - while ! true > /dev/tcp/$host/$port; do - - if [ $attempt_num -eq $max_attempts ]; then - echo "Unable to connect to database." - exit 1 - else - echo "Attempt $attempt_num failed! Trying again in 5 seconds..." - - fi - - attempt_num=$(("$attempt_num" + 1)) - sleep 5 - done - echo "Connected to MariaDB" -} - -wait_for_redis() { - # We use a Python script to send the Redis ping - # instead of installing redis-tools just for 1 thing - if ! python3 /sbin/wait-for-redis.py; then - exit 1 - fi -} - -migrations() { - ( - # flock is in place to prevent multiple containers from doing migrations - # simultaneously. This also ensures that the db is ready when the command - # of the current container starts. - flock 200 - echo "Apply database migrations..." - python3 manage.py migrate --skip-checks --no-input - ) 200>"${DATA_DIR}/migration_lock" -} - -django_checks() { - # Explicitly run the Django system checks - echo "Running Django checks" - python3 manage.py check -} - -search_index() { - - local -r index_version=9 - local -r index_version_file=${DATA_DIR}/.index_version - - if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then - echo "Search index out of date. Updating..." - python3 manage.py document_index reindex --no-progress-bar - echo ${index_version} | tee "${index_version_file}" >/dev/null - fi -} - -superuser() { - if [[ -n "${PAPERLESS_ADMIN_USER}" ]]; then - python3 manage.py manage_superuser - fi -} - -do_work() { - if [[ "${PAPERLESS_DBENGINE}" == "mariadb" ]]; then - wait_for_mariadb - elif [[ -n "${PAPERLESS_DBHOST}" ]]; then - wait_for_postgres - fi - - wait_for_redis - - migrations - - django_checks - - search_index - - superuser - -} - -do_work diff --git a/docker/env-from-file.sh b/docker/env-from-file.sh deleted file mode 100644 index 41f51d065..000000000 --- a/docker/env-from-file.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -# Scans the environment variables for those with the suffix _FILE -# When located, checks the file exists, and exports the contents -# of the file as the same name, minus the suffix -# This allows the use of Docker secrets or mounted files -# to fill in any of the settings configurable via environment -# variables - -set -eu - -for line in $(printenv) -do - # Extract the name of the environment variable - env_name=${line%%=*} - # Check if it starts with "PAPERLESS_" and ends in "_FILE" - if [[ ${env_name} == PAPERLESS_*_FILE ]]; then - # This should have been named different.. - if [[ ${env_name} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || ${env_name} == "PAPERLESS_MODEL_FILE" ]]; then - continue - fi - # Extract the value of the environment - env_value=${line#*=} - - # Check the file exists - if [[ -f ${env_value} ]]; then - - # Trim off the _FILE suffix - non_file_env_name=${env_name%"_FILE"} - echo "Setting ${non_file_env_name} from file" - - # Reads the value from th file - val="$(< "${!env_name}")" - - # Sets the normal name to the read file contents - export "${non_file_env_name}"="${val}" - - else - echo "File ${env_value} referenced by ${env_name} doesn't exist" - fi - fi -done diff --git a/docker/flower-conditional.sh b/docker/flower-conditional.sh deleted file mode 100644 index f8719e0fd..000000000 --- a/docker/flower-conditional.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -echo "Checking if we should start flower..." - -if [[ -n "${PAPERLESS_ENABLE_FLOWER}" ]]; then - # Small delay to allow celery to be up first - echo "Starting flower in 5s" - sleep 5 - celery --app paperless flower --conf=/usr/src/paperless/src/paperless/flowerconfig.py -else - echo "Not starting flower" -fi diff --git a/docker/init-flow.drawio.png b/docker/init-flow.drawio.png new file mode 100644 index 000000000..2e19ac16a Binary files /dev/null and b/docker/init-flow.drawio.png differ diff --git a/docker/install_management_commands.sh b/docker/install_management_commands.sh index 37c17058a..c7c65bfbb 100755 --- a/docker/install_management_commands.sh +++ b/docker/install_management_commands.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +# Run this script to generate the management commands again (for example if a new command is create or the template is updated) + set -eu for command in decrypt_documents \ @@ -19,6 +21,6 @@ for command in decrypt_documents \ prune_audit_logs; do echo "installing $command..." - sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command - chmod +x /usr/local/bin/$command + sed "s/management_command/$command/g" management_script.sh >"$PWD/rootfs/usr/local/bin/$command" + chmod +x "$PWD/rootfs/usr/local/bin/$command" done diff --git a/docker/management_script.sh b/docker/management_script.sh index 996435745..1fa31c372 100755 --- a/docker/management_script.sh +++ b/docker/management_script.sh @@ -1,17 +1,13 @@ -#!/usr/bin/env bash +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash set -e -cd /usr/src/paperless/src/ -# This ensures environment is setup -# shellcheck disable=SC1091 -source /sbin/env-from-file.sh +cd "${PAPERLESS_SRC_DIR}" -if [[ $(id -u) == 0 ]] ; -then - gosu paperless python3 manage.py management_command "$@" -elif [[ $(id -un) == "paperless" ]] ; -then +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py management_command "$@" +elif [[ $(id -un) == "paperless" ]]; then python3 manage.py management_command "$@" else echo "Unknown user." diff --git a/docker/paperless_cmd.sh b/docker/paperless_cmd.sh deleted file mode 100755 index afedb1599..000000000 --- a/docker/paperless_cmd.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -SUPERVISORD_WORKING_DIR="${PAPERLESS_SUPERVISORD_WORKING_DIR:-$PWD}" -rootless_args=() -if [ "$(id -u)" == "$(id -u paperless)" ]; then - rootless_args=( - --user - paperless - --logfile - "${SUPERVISORD_WORKING_DIR}/supervisord.log" - --pidfile - "${SUPERVISORD_WORKING_DIR}/supervisord.pid" - ) -fi - -exec /usr/local/bin/supervisord -c /etc/supervisord.conf "${rootless_args[@]}" diff --git a/docker/imagemagick-policy.xml b/docker/rootfs/etc/ImageMagick-6/paperless-policy.xml similarity index 100% rename from docker/imagemagick-policy.xml rename to docker/rootfs/etc/ImageMagick-6/paperless-policy.xml diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-custom-init b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-custom-init new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-env-file b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-env-file new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-folders b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-folders new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-migrations b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-migrations new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-modify-user b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-modify-user new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-search-index b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-search-index new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-start b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-start new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-superuser b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-superuser new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-system-checks b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-system-checks new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-tesseract-langs b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-tesseract-langs new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-wait-for-db b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-wait-for-db new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-wait-for-redis b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-wait-for-redis new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/run new file mode 100755 index 000000000..83c83df9d --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/run @@ -0,0 +1,8 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash +declare -r log_prefix="[init-complete]" +declare -r end_time=$(date +%s) +declare -r start_time=${PAPERLESS_START_TIME_S} + +echo "${log_prefix} paperless-ngx docker container init completed in $(($end_time-$start_time)) seconds" +echo "${log_prefix} Starting services" diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/up new file mode 100644 index 000000000..9fb31e420 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-complete/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-search-index b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-search-index new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-system-checks b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-system-checks new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-tesseract-langs b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-tesseract-langs new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-wait-for-db b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-wait-for-db new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-wait-for-redis b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/dependencies.d/init-wait-for-redis new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/run new file mode 100755 index 000000000..50cc4c241 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/run @@ -0,0 +1,44 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[custom-init]" + +# Mostly borrowed from the LinuxServer.io base image +# https://github.com/linuxserver/docker-baseimage-ubuntu/tree/bionic/root/etc/cont-init.d +declare -r custom_script_dir="/custom-cont-init.d" + +# Tamper checking. +# Don't run files which are owned by anyone except root +# Don't run files which are writeable by others +if [ -d "${custom_script_dir}" ]; then + if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 ! -user root)" ]; then + echo "${log_prefix} **** Potential tampering with custom scripts detected ****" + echo "${log_prefix} **** The folder '${custom_script_dir}' must be owned by root ****" + exit 0 + fi + if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 -perm -o+w)" ]; then + echo "${log_prefix} **** The folder '${custom_script_dir}' or some of contents have write permissions for others, which is a security risk. ****" + echo "${log_prefix} **** Please review the permissions and their contents to make sure they are owned by root, and can only be modified by root. ****" + exit 0 + fi + + # Make sure custom init directory has files in it + if [ -n "$(/bin/ls --almost-all "${custom_script_dir}" 2>/dev/null)" ]; then + echo "${log_prefix} files found in ${custom_script_dir} executing" + # Loop over files in the directory + for SCRIPT in "${custom_script_dir}"/*; do + NAME="$(basename "${SCRIPT}")" + if [ -f "${SCRIPT}" ]; then + echo "${log_prefix} ${NAME}: executing..." + /command/with-contenv /bin/bash "${SCRIPT}" + echo "${log_prefix} ${NAME}: exited $?" + elif [ ! -f "${SCRIPT}" ]; then + echo "${log_prefix} ${NAME}: is not a file" + fi + done + else + echo "${log_prefix} no custom files found exiting..." + fi +else + echo "${log_prefix} ${custom_script_dir} doesn't exist, nothing to do" +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/up new file mode 100644 index 000000000..69a261a39 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-custom-init/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-custom-init/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/dependencies.d/init-start b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/dependencies.d/init-start new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/run new file mode 100755 index 000000000..08b7635f8 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/run @@ -0,0 +1,30 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[env-init]" + +echo "${log_prefix} Checking for environment from files" + +if find /run/s6/container_environment/*"_FILE" -maxdepth 1 > /dev/null 2>&1; then + for FILENAME in /run/s6/container_environment/*; do + if [[ "${FILENAME##*/}" == PAPERLESS_*_FILE ]]; then + # This should have been named different.. + if [[ ${FILENAME} == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || ${FILENAME} == "PAPERLESS_MODEL_FILE" ]]; then + continue + fi + SECRETFILE=$(cat "${FILENAME}") + # Check the file exists + if [[ -f ${SECRETFILE} ]]; then + # Trim off trailing _FILE + FILESTRIP=${FILENAME//_FILE/} + # Set environment variable + cat "${SECRETFILE}" > "${FILESTRIP}" + echo "${log_prefix} ${FILESTRIP##*/} set from ${FILENAME##*/}" + else + echo "${log_prefix} cannot find secret in ${FILENAME##*/}" + fi + fi + done +else + echo "${log_prefix} No *_FILE environment found" +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/up new file mode 100644 index 000000000..0bb4c22fc --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-env-file/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-env-file/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/dependencies.d/init-modify-user b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/dependencies.d/init-modify-user new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/run new file mode 100755 index 000000000..5f731ceae --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/run @@ -0,0 +1,33 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[init-folders]" + +declare -r export_dir="/usr/src/paperless/export" +declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}" +declare -r media_root_dir="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}" +declare -r consume_dir="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}" +declare -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}" + +echo "${log_prefix} Checking for folder existence" + +for dir in \ + "${export_dir}" \ + "${data_dir}" "${data_dir}/index" \ + "${media_root_dir}" "${media_root_dir}/documents" "${media_root_dir}/documents/originals" "${media_root_dir}/documents/thumbnails" \ + "${consume_dir}" \ + "${tmp_dir}"; do + if [[ ! -d "${dir}" ]]; then + mkdir --parents --verbose "${dir}" + fi +done + +echo "${log_prefix} Adjusting file and folder permissions" +for dir in \ + "${export_dir}" \ + "${data_dir}" \ + "${media_root_dir}" \ + "${consume_dir}" \ + "${tmp_dir}"; do + find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} + +done diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/up new file mode 100644 index 000000000..0fb7dc7de --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-folders/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-folders/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/dependencies.d/init-folders b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/dependencies.d/init-folders new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/dependencies.d/init-wait-for-db b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/dependencies.d/init-wait-for-db new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/run new file mode 100755 index 000000000..db0dc26d3 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/run @@ -0,0 +1,20 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash +declare -r log_prefix="[init-migrations]" +declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}" + +( + # flock is in place to prevent multiple containers from doing migrations + # simultaneously. This also ensures that the db is ready when the command + # of the current container starts. + flock 200 + echo "${log_prefix} Apply database migrations..." + cd "${PAPERLESS_SRC_DIR}" + + if [[ -n "${USER_IS_NON_ROOT}" ]]; then + exec python3 manage.py migrate --skip-checks --no-input + else + exec s6-setuidgid paperless python3 manage.py migrate --skip-checks --no-input + fi + +) 200>"${data_dir}/migration_lock" diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/up new file mode 100644 index 000000000..7c4cbcf6f --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-migrations/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-migrations/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/dependencies.d/init-env-file b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/dependencies.d/init-env-file new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/run new file mode 100755 index 000000000..aa617355d --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/run @@ -0,0 +1,22 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash +declare -r log_prefix="[init-user]" + +declare -r usermap_original_uid=$(id -u paperless) +declare -r usermap_original_gid=$(id -g paperless) +declare -r usermap_new_uid=${USERMAP_UID:-$usermap_original_uid} +declare -r usermap_new_gid=${USERMAP_GID:-${usermap_original_gid:-$usermap_new_uid}} + +if [[ ${usermap_new_uid} != "${usermap_original_uid}" ]]; then + echo "${log_prefix} Mapping UID for paperless to $usermap_new_uid" + usermod --non-unique --uid "${usermap_new_uid}" paperless +else + echo "${log_prefix} No UID changes for paperless" +fi + +if [[ ${usermap_new_gid} != "${usermap_original_gid}" ]]; then + echo "${log_prefix} Mapping GID for paperless to $usermap_new_gid" + groupmod --non-unique --gid "${usermap_new_gid}" paperless +else + echo "${log_prefix} No GID changes for paperless" +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/up new file mode 100644 index 000000000..4c22ce857 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-modify-user/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/dependencies.d/init-migrations b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/dependencies.d/init-migrations new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/run new file mode 100755 index 000000000..2208faf67 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/run @@ -0,0 +1,28 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[init-index]" + +declare -r index_version=9 +declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}" +declare -r index_version_file="${data_dir}/.index_version" + +update_index () { + echo "${log_prefix} Search index out of date. Updating..." + cd "${PAPERLESS_SRC_DIR}" + if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py document_index reindex --no-progress-bar + echo ${index_version} | tee "${index_version_file}" > /dev/null + else + s6-setuidgid paperless python3 manage.py document_index reindex --no-progress-bar + echo ${index_version} | s6-setuidgid paperless tee "${index_version_file}" > /dev/null + fi +} + +if [[ (! -f "${index_version_file}") ]]; then + echo "${log_prefix} No index version file found" + update_index +elif [[ $(<"${index_version_file}") != "$index_version" ]]; then + echo "${log_prefix} index version updated" + update_index +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/up new file mode 100644 index 000000000..372763add --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-search-index/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-search-index/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/dependencies.d/base b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/dependencies.d/base new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/run new file mode 100755 index 000000000..b6a26fae7 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/run @@ -0,0 +1,19 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[init-start]" + +echo "${log_prefix} paperless-ngx docker container starting..." + +# Set some directories into environment for other steps to access via environment +# Sort of like variables for later +printf "/usr/src/paperless/src" > /var/run/s6/container_environment/PAPERLESS_SRC_DIR +echo $(date +%s) > /var/run/s6/container_environment/PAPERLESS_START_TIME_S + +# Check if we're starting as a non-root user +if [ $(id -u) == $(id -u paperless) ]; then + printf "true" > /var/run/s6/container_environment/USER_IS_NON_ROOT + echo "${log_prefix} paperless-ngx docker container running under a user" +else + echo "${log_prefix} paperless-ngx docker container starting init as root" +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/up new file mode 100644 index 000000000..3a6a26d81 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-start/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-start/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/dependencies.d/init-migrations b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/dependencies.d/init-migrations new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/run new file mode 100755 index 000000000..625185181 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/run @@ -0,0 +1,20 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[init-superuser]" + +if [[ -n "${PAPERLESS_ADMIN_USER}" ]]; then + echo "${log_prefix} Creating superuser..." + cd "${PAPERLESS_SRC_DIR}" + + if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py manage_superuser + else + s6-setuidgid paperless python3 manage.py manage_superuser + fi + + echo "${log_prefix} Superuser creation done" + +else + echo "${log_prefix} Not creating superuser" +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/up new file mode 100644 index 000000000..055f10a50 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-superuser/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-superuser/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/dependencies.d/init-superuser b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/dependencies.d/init-superuser new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/dependencies.d/init-tesseract-langs b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/dependencies.d/init-tesseract-langs new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/run new file mode 100755 index 000000000..d024765cc --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/run @@ -0,0 +1,15 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[init-checks]" + +# Explicitly run the Django system checks +echo "${log_prefix} Running Django checks" + +cd "${PAPERLESS_SRC_DIR}" + +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + python3 manage.py check +else + s6-setuidgid paperless python3 manage.py check +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/up new file mode 100644 index 000000000..7403acc2f --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-system-checks/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-system-checks/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/dependencies.d/init-env-file b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/dependencies.d/init-env-file new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/run new file mode 100755 index 000000000..7ab464583 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/run @@ -0,0 +1,65 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[init-tesseract-langs]" + +install_languages() { + echo "Installing languages..." + + read -ra langs <<<"$1" + + # Check that it is not empty + if [ ${#langs[@]} -eq 0 ]; then + return + fi + + # Build list of packages to install + to_install=() + for lang in "${langs[@]}"; do + pkg="tesseract-ocr-$lang" + + if dpkg --status "$pkg" &>/dev/null; then + echo "${log_prefix} Package $pkg already installed!" + continue + else + to_install+=("$pkg") + fi + done + + # Use apt only when we install packages + if [ ${#to_install[@]} -gt 0 ]; then + + # Warn the user if they're not root, but try anyway + if [[ -n "${USER_IS_NON_ROOT}" ]]; then + echo "${log_prefix} ERROR: Unable to install language ${pkg} as non-root, startup may fail" + fi + + apt-get --quiet update &>/dev/null + + for pkg in "${to_install[@]}"; do + if ! apt-cache --quiet show "$pkg" &>/dev/null; then + echo "${log_prefix} Skipped $pkg: Package not found! :(" + continue + fi + echo "${log_prefix} Installing package $pkg..." + if ! apt-get --quiet --assume-yes install "$pkg" &>/dev/null; then + echo "${log_prefix} Could not install $pkg" + exit 1 + else + echo "${log_prefix} Installed $pkg" + fi + done + + fi +} + +echo "${log_prefix} Checking if additional teseract languages needed" + +# Install additional languages if specified +if [[ -n "$PAPERLESS_OCR_LANGUAGES" ]]; then + + install_languages "$PAPERLESS_OCR_LANGUAGES" + echo "${log_prefix} Additional packages installed" +else + echo "${log_prefix} No additional installs requested" +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/up new file mode 100644 index 000000000..16ef2f4d1 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-tesseract-langs/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-tesseract-langs/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/dependencies.d/init-env-file b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/dependencies.d/init-env-file new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/run new file mode 100755 index 000000000..ede8a654a --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/run @@ -0,0 +1,70 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[init-db-wait]" + +wait_for_postgres() { + local attempt_num=1 + local -r max_attempts=5 + + echo "${log_prefix} Waiting for PostgreSQL to start..." + + local -r host="${PAPERLESS_DBHOST:-localhost}" + local -r port="${PAPERLESS_DBPORT:-5432}" + local -r user="${PAPERLESS_DBUSER:-paperless}" + + # Disable warning, host and port can't have spaces + # shellcheck disable=SC2086 + while [ ! "$(pg_isready -h ${host} -p ${port} --username ${user})" ]; do + + if [ $attempt_num -eq $max_attempts ]; then + echo "${log_prefix} Unable to connect to database." + exit 1 + else + echo "${log_prefix} Attempt $attempt_num failed! Trying again in 5 seconds..." + fi + + attempt_num=$(("$attempt_num" + 1)) + sleep 5 + done + # Extra in case this is a first start + sleep 5 + echo "Connected to PostgreSQL" +} + +wait_for_mariadb() { + echo "${log_prefix} Waiting for MariaDB to start..." + + local -r host="${PAPERLESS_DBHOST:=localhost}" + local -r port="${PAPERLESS_DBPORT:=3306}" + + local attempt_num=1 + local -r max_attempts=5 + + # Disable warning, host and port can't have spaces + # shellcheck disable=SC2086 + while ! true > /dev/tcp/$host/$port; do + + if [ $attempt_num -eq $max_attempts ]; then + echo "${log_prefix} Unable to connect to database." + exit 1 + else + echo "${log_prefix} Attempt $attempt_num failed! Trying again in 5 seconds..." + + fi + + attempt_num=$(("$attempt_num" + 1)) + sleep 5 + done + echo "Connected to MariaDB" +} + +if [[ "${PAPERLESS_DBENGINE}" == "mariadb" ]]; then + echo "${log_prefix} Waiting for MariaDB to report ready" + wait_for_mariadb +elif [[ -n "${PAPERLESS_DBHOST}" ]]; then + echo "${log_prefix} Waiting for postgresql to report ready" + wait_for_postgres +fi + + echo "${log_prefix} Database is ready" diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/up new file mode 100644 index 000000000..6cbecf5f4 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-db/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-wait-for-db/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/dependencies.d/init-env-file b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/dependencies.d/init-env-file new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/run new file mode 100755 index 000000000..407536aa2 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/run @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[init-redis-wait]" + +echo "${log_prefix} Waiting for Redis to report ready" + +# We use a Python script to send the Redis ping +# instead of installing redis-tools just for 1 thing +if ! python3 /usr/local/bin/wait-for-redis.py; then + exit 1 +else + echo "${log_prefix} Redis ready" +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/up new file mode 100644 index 000000000..8a5fa1601 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-wait-for-redis/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-wait-for-redis/run diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-consumer/dependencies.d/init-complete b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-consumer/dependencies.d/init-complete new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-consumer/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-consumer/run new file mode 100755 index 000000000..3e1c0472b --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-consumer/run @@ -0,0 +1,10 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +cd ${PAPERLESS_SRC_DIR} + +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + exec python3 manage.py document_consumer +else + exec s6-setuidgid paperless python3 manage.py document_consumer +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-consumer/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-consumer/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-consumer/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/dependencies.d/init-complete b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/dependencies.d/init-complete new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/dependencies.d/svc-scheduler b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/dependencies.d/svc-scheduler new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/dependencies.d/svc-worker b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/dependencies.d/svc-worker new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/run new file mode 100755 index 000000000..a3e4b6a3e --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/run @@ -0,0 +1,24 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +declare -r log_prefix="[svc-flower]" + +echo "${log_prefix} Checking if we should start flower..." + +if [[ -n "${PAPERLESS_ENABLE_FLOWER}" ]]; then + # Small delay to allow celery to be up first + echo "${log_prefix} Starting flower in 5s" + sleep 5 + cd ${PAPERLESS_SRC_DIR} + + if [[ -n "${USER_IS_NON_ROOT}" ]]; then + exec /usr/local/bin/celery --app paperless flower --conf=${PAPERLESS_SRC_DIR}/paperless/flowerconfig.py + else + exec s6-setuidgid paperless /usr/local/bin/celery --app paperless flower --conf=${PAPERLESS_SRC_DIR}/paperless/flowerconfig.py + fi + +else + echo "${log_prefix} Not starting flower" + # https://skarnet.org/software/s6/s6-svc.html + s6-svc -Od . +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-flower/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/dependencies.d/init-complete b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/dependencies.d/init-complete new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/dependencies.d/svc-worker b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/dependencies.d/svc-worker new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/run new file mode 100755 index 000000000..396a4d2e0 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/run @@ -0,0 +1,10 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +cd ${PAPERLESS_SRC_DIR} + +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + exec /usr/local/bin/celery --app paperless beat --loglevel INFO +else + exec s6-setuidgid paperless /usr/local/bin/celery --app paperless beat --loglevel INFO +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-scheduler/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/dependencies.d/init-complete b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/dependencies.d/init-complete new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/dependencies.d/svc-consumer b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/dependencies.d/svc-consumer new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/dependencies.d/svc-scheduler b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/dependencies.d/svc-scheduler new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/dependencies.d/svc-worker b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/dependencies.d/svc-worker new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/run new file mode 100755 index 000000000..423b17531 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/run @@ -0,0 +1,10 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +cd ${PAPERLESS_SRC_DIR} + +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + exec /usr/local/bin/gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.asgi:application +else + exec s6-setuidgid paperless /usr/local/bin/gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.asgi:application +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-webserver/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-worker/dependencies.d/init-complete b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-worker/dependencies.d/init-complete new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-worker/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-worker/run new file mode 100755 index 000000000..0bf833c56 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-worker/run @@ -0,0 +1,10 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +cd ${PAPERLESS_SRC_DIR} + +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + exec /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip +else + exec s6-setuidgid paperless /usr/local/bin/celery --app paperless worker --loglevel INFO --without-mingle --without-gossip +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-worker/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-worker/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/svc-worker/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-complete b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-complete new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-consumer b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-consumer new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-flower b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-flower new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-scheduler b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-scheduler new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-webserver b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-webserver new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-worker b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/svc-worker new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/usr/local/bin/convert_mariadb_uuid b/docker/rootfs/usr/local/bin/convert_mariadb_uuid new file mode 100755 index 000000000..806a98f3b --- /dev/null +++ b/docker/rootfs/usr/local/bin/convert_mariadb_uuid @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py convert_mariadb_uuid "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/decrypt_documents b/docker/rootfs/usr/local/bin/decrypt_documents new file mode 100755 index 000000000..4da1549ee --- /dev/null +++ b/docker/rootfs/usr/local/bin/decrypt_documents @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py decrypt_documents "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py decrypt_documents "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_archiver b/docker/rootfs/usr/local/bin/document_archiver new file mode 100755 index 000000000..383acfcc6 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_archiver @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_archiver "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_archiver "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_create_classifier b/docker/rootfs/usr/local/bin/document_create_classifier new file mode 100755 index 000000000..72dc33d6f --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_create_classifier @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_create_classifier "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_create_classifier "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_exporter b/docker/rootfs/usr/local/bin/document_exporter new file mode 100755 index 000000000..7f48215d7 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_exporter @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_exporter "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_exporter "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_fuzzy_match b/docker/rootfs/usr/local/bin/document_fuzzy_match new file mode 100755 index 000000000..5b9548557 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_fuzzy_match @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_fuzzy_match "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_importer b/docker/rootfs/usr/local/bin/document_importer new file mode 100755 index 000000000..2286e89f7 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_importer @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_importer "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_importer "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_index b/docker/rootfs/usr/local/bin/document_index new file mode 100755 index 000000000..2d518b5c5 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_index @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_index "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_index "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_renamer b/docker/rootfs/usr/local/bin/document_renamer new file mode 100755 index 000000000..326317a73 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_renamer @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_renamer "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_renamer "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_retagger b/docker/rootfs/usr/local/bin/document_retagger new file mode 100755 index 000000000..3bab3e790 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_retagger @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_retagger "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_retagger "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_sanity_checker b/docker/rootfs/usr/local/bin/document_sanity_checker new file mode 100755 index 000000000..5c0c29ef2 --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_sanity_checker @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_sanity_checker "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_sanity_checker "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/document_thumbnails b/docker/rootfs/usr/local/bin/document_thumbnails new file mode 100755 index 000000000..c1000c31a --- /dev/null +++ b/docker/rootfs/usr/local/bin/document_thumbnails @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_thumbnails "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_thumbnails "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/mail_fetcher b/docker/rootfs/usr/local/bin/mail_fetcher new file mode 100755 index 000000000..2ae1d1dfb --- /dev/null +++ b/docker/rootfs/usr/local/bin/mail_fetcher @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py mail_fetcher "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py mail_fetcher "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/manage_superuser b/docker/rootfs/usr/local/bin/manage_superuser new file mode 100755 index 000000000..9f7f37ecf --- /dev/null +++ b/docker/rootfs/usr/local/bin/manage_superuser @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py manage_superuser "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py manage_superuser "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/prune_audit_logs b/docker/rootfs/usr/local/bin/prune_audit_logs new file mode 100755 index 000000000..b9142e98e --- /dev/null +++ b/docker/rootfs/usr/local/bin/prune_audit_logs @@ -0,0 +1,14 @@ +#!/command/with-contenv /usr/bin/bash +# shellcheck shell=bash + +set -e + +cd "${PAPERLESS_SRC_DIR}" + +if [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py prune_audit_logs "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py prune_audit_logs "$@" +else + echo "Unknown user." +fi diff --git a/docker/rootfs/usr/local/bin/wait-for-redis.py b/docker/rootfs/usr/local/bin/wait-for-redis.py new file mode 100755 index 000000000..9ae4a35a5 --- /dev/null +++ b/docker/rootfs/usr/local/bin/wait-for-redis.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +Simple script which attempts to ping the Redis broker as set in the environment for +a certain number of times, waiting a little bit in between + +""" + +import os +import sys +import time + +import click +from redis import Redis + + +@click.command(context_settings={"show_default": True}) +@click.option( + "--retry-count", + default=5, + type=int, + help="Count of times to retry the Redis connection", +) +@click.option( + "--retry-sleep", + default=5, + type=int, + help="Seconds to wait between Redis connection retries", +) +@click.argument( + "redis_url", + type=str, + envvar="PAPERLESS_REDIS", + default="redis://localhost:6379", +) +def wait(redis_url: str, retry_count: int, retry_sleep: int) -> None: + click.echo("Waiting for Redis...") + + attempt = 0 + with Redis.from_url(url=redis_url) as client: + while attempt < retry_count: + try: + client.ping() + break + except Exception as e: + click.echo( + f"Redis ping #{attempt} failed.\n" + f"Error: {e!s}.\n" + f"Waiting {retry_sleep}s", + ) + time.sleep(retry_sleep) + attempt += 1 + + if attempt >= retry_count: + click.echo( + "Failed to connect to redis using environment variable PAPERLESS_REDIS.", + ) + sys.exit(os.EX_UNAVAILABLE) + else: + click.echo("Connected to Redis broker.") + sys.exit(os.EX_OK) + + +if __name__ == "__main__": + wait() diff --git a/docker/supervisord.conf b/docker/supervisord.conf deleted file mode 100644 index 009760771..000000000 --- a/docker/supervisord.conf +++ /dev/null @@ -1,65 +0,0 @@ -[supervisord] -nodaemon=true ; start in foreground if true; default false -logfile=/var/log/supervisord/supervisord.log ; main log file; default $CWD/supervisord.log -pidfile=/var/run/supervisord/supervisord.pid ; supervisord pidfile; default supervisord.pid -logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB -logfile_backups=10 ; # of main logfile backups; 0 means none, default 10 -loglevel=info ; log level; default info; others: debug,warn,trace -user=root - -[program:gunicorn] -command=gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.asgi:application -user=paperless -priority = 1 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -environment = HOME="/usr/src/paperless",USER="paperless" - -[program:consumer] -command=python3 manage.py document_consumer -user=paperless -stopsignal=INT -priority = 20 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -environment = HOME="/usr/src/paperless",USER="paperless" - -[program:celery] - -command = celery --app paperless worker --loglevel INFO --without-mingle --without-gossip -user=paperless -stopasgroup = true -stopwaitsecs = 60 -priority = 5 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -environment = HOME="/usr/src/paperless",USER="paperless" - -[program:celery-beat] - -command = celery --app paperless beat --loglevel INFO -user=paperless -stopasgroup = true -priority = 10 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -environment = HOME="/usr/src/paperless",USER="paperless" - -[program:celery-flower] -command = /usr/local/bin/flower-conditional.sh -user = paperless -startsecs = 0 -priority = 40 -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 -stderr_logfile=/dev/stderr -stderr_logfile_maxbytes=0 -environment = HOME="/usr/src/paperless",USER="paperless" diff --git a/docker/wait-for-redis.py b/docker/wait-for-redis.py deleted file mode 100755 index c3e4f1d59..000000000 --- a/docker/wait-for-redis.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple script which attempts to ping the Redis broker as set in the environment for -a certain number of times, waiting a little bit in between - -""" - -import os -import sys -import time -from typing import Final - -from redis import Redis - -if __name__ == "__main__": - MAX_RETRY_COUNT: Final[int] = 5 - RETRY_SLEEP_SECONDS: Final[int] = 5 - - REDIS_URL: Final[str] = os.getenv("PAPERLESS_REDIS", "redis://localhost:6379") - - print("Waiting for Redis...", flush=True) - - attempt = 0 - with Redis.from_url(url=REDIS_URL) as client: - while attempt < MAX_RETRY_COUNT: - try: - client.ping() - break - except Exception as e: - print( - f"Redis ping #{attempt} failed.\n" - f"Error: {e!s}.\n" - f"Waiting {RETRY_SLEEP_SECONDS}s", - flush=True, - ) - time.sleep(RETRY_SLEEP_SECONDS) - attempt += 1 - - if attempt >= MAX_RETRY_COUNT: - print("Failed to connect to redis using environment variable PAPERLESS_REDIS.") - sys.exit(os.EX_UNAVAILABLE) - else: - print("Connected to Redis broker.") - sys.exit(os.EX_OK) diff --git a/docs/changelog.md b/docs/changelog.md index e2d2b98ac..56316942c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,28 @@ # Changelog +## paperless-ngx 2.14.7 + +### Features + +- Enhancement: require totp code for obtain auth token by [@shamoon](https://github.com/shamoon) [#8936](https://github.com/paperless-ngx/paperless-ngx/pull/8936) + +### Bug Fixes + +- Enhancement: require totp code for obtain auth token by [@shamoon](https://github.com/shamoon) [#8936](https://github.com/paperless-ngx/paperless-ngx/pull/8936) +- Fix: reflect doc links in bulk modify custom fields by [@shamoon](https://github.com/shamoon) [#8962](https://github.com/paperless-ngx/paperless-ngx/pull/8962) +- Fix: also ensure symmetric doc link removal on bulk edit by [@shamoon](https://github.com/shamoon) [#8963](https://github.com/paperless-ngx/paperless-ngx/pull/8963) + +### All App Changes + +
+4 changes + +- Chore(deps-dev): Bump ruff from 0.9.2 to 0.9.3 in the development group by @[dependabot[bot]](https://github.com/apps/dependabot) [#8928](https://github.com/paperless-ngx/paperless-ngx/pull/8928) +- Enhancement: require totp code for obtain auth token by [@shamoon](https://github.com/shamoon) [#8936](https://github.com/paperless-ngx/paperless-ngx/pull/8936) +- Fix: reflect doc links in bulk modify custom fields by [@shamoon](https://github.com/shamoon) [#8962](https://github.com/paperless-ngx/paperless-ngx/pull/8962) +- Fix: also ensure symmetric doc link removal on bulk edit by [@shamoon](https://github.com/shamoon) [#8963](https://github.com/paperless-ngx/paperless-ngx/pull/8963) +
+ ## paperless-ngx 2.14.6 ### Bug Fixes diff --git a/docs/configuration.md b/docs/configuration.md index 799620c05..3724c792d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -198,6 +198,18 @@ Docker, this may be the `environment` key of the webserver or a containing the configuration parameters. Be sure to use the correct format and watch out for indentation if editing the YAML file. +### Email Parsing + +#### [`PAPERLESS_EMAIL_PARSE_DEFAULT_LAYOUT=`(#PAPERLESS_EMAIL_PARSE_DEFAULT_LAYOUT) {#PAPERLESS_EMAIL_PARSE_DEFAULT_LAYOUT} + +: The default layout to use for emails that are consumed as documents. Must be one of the integer choices below. Note that mail +rules can specify this setting, thus this fallback is used for the default selection and for .eml files consumed by other means. + + - `1` = Text, then HTML + - `2` = HTML, then text + - `3` = HTML only + - `4` = Text only + ## Paths and folders #### [`PAPERLESS_CONSUMPTION_DIR=`](#PAPERLESS_CONSUMPTION_DIR) {#PAPERLESS_CONSUMPTION_DIR} @@ -1073,8 +1085,6 @@ or hidden folders some tools use to store data. If you have problems that your Barcodes/QR-Codes are not detected (especially with bad scan quality and/or small codes), try the other one. - zxing is not available on all platforms. - #### [`PAPERLESS_PRE_CONSUME_SCRIPT=`](#PAPERLESS_PRE_CONSUME_SCRIPT) {#PAPERLESS_PRE_CONSUME_SCRIPT} : After some initial validation, Paperless can trigger an arbitrary @@ -1586,9 +1596,11 @@ started by the container. #### [`PAPERLESS_SUPERVISORD_WORKING_DIR=`](#PAPERLESS_SUPERVISORD_WORKING_DIR) {#PAPERLESS_SUPERVISORD_WORKING_DIR} -: If this environment variable is defined, the `supervisord.log` and `supervisord.pid` file will be created under the specified path in `PAPERLESS_SUPERVISORD_WORKING_DIR`. Setting `PAPERLESS_SUPERVISORD_WORKING_DIR=/tmp` and `PYTHONPYCACHEPREFIX=/tmp/pycache` would allow paperless to work on a read-only filesystem. +!!! warning - Please take note that the `PAPERLESS_DATA_DIR` and `PAPERLESS_MEDIA_ROOT` paths still have to be writable, just like the `PAPERLESS_SUPERVISORD_WORKING_DIR`. The can be archived by using bind or volume mounts. Only works in the container is run as user *paperless* + This option is deprecated and has no effect. For read only file system support, + see [S6_READ_ONLY_ROOT](https://github.com/just-containers/s6-overlay#customizing-s6-overlay-behaviour) + from s6-overlay. ## Frontend Settings diff --git a/src-ui/e2e/document-list/document-list.spec.ts b/src-ui/e2e/document-list/document-list.spec.ts index 7719873d3..cd1a4c54e 100644 --- a/src-ui/e2e/document-list/document-list.spec.ts +++ b/src-ui/e2e/document-list/document-list.spec.ts @@ -83,9 +83,9 @@ test('date filtering', async ({ page }) => { await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' }) await page.goto('/documents') await page.getByRole('button', { name: 'Dates' }).click() - await page.getByRole('menuitem', { name: 'Last 3 months' }).first().click() + await page.getByRole('menuitem', { name: 'Within 3 months' }).first().click() await expect(page.locator('pngx-document-list')).toHaveText(/one document/i) - await page.getByRole('menuitem', { name: 'Last 3 months' }).first().click() + await page.getByRole('menuitem', { name: 'Within 3 months' }).first().click() await page.getByLabel('Datesselected').getByRole('button').first().click() await page.getByRole('combobox', { name: 'Select month' }).selectOption('12') await page.getByRole('combobox', { name: 'Select year' }).selectOption('2022') diff --git a/src-ui/e2e/document-list/requests/api-document-list3.har b/src-ui/e2e/document-list/requests/api-document-list3.har index 6d5d1808f..291915a65 100644 --- a/src-ui/e2e/document-list/requests/api-document-list3.har +++ b/src-ui/e2e/document-list/requests/api-document-list3.har @@ -3687,7 +3687,7 @@ "time": 1.501, "request": { "method": "GET", - "url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&created__date__gt=2022-12-11", + "url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&created__date__gte=2022-12-11", "httpVersion": "HTTP/1.1", "cookies": [], "headers": [ @@ -3721,7 +3721,7 @@ "value": "true" }, { - "name": "created__date__gt", + "name": "created__date__gte", "value": "2022-12-11" } ], diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 981394b87..1c79ae1fe 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -241,22 +241,22 @@ Document was added to Paperless-ngx. src/app/app.component.ts - 93 + 95 src/app/app.component.ts - 102 + 104 Open document src/app/app.component.ts - 95 + 97 src/app/components/admin/trash/trash.component.ts - 141 + 146 src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -279,29 +279,29 @@ Could not add : src/app/app.component.ts - 117 + 119 Document is being processed by Paperless-ngx. src/app/app.component.ts - 132 + 134 Dashboard src/app/app.component.ts - 139 + 141 src/app/components/app-frame/app-frame.component.html - 81 + 82 src/app/components/app-frame/app-frame.component.html - 83 + 84 src/app/components/dashboard/dashboard.component.html @@ -312,15 +312,15 @@ Documents src/app/app.component.ts - 150 + 152 src/app/components/app-frame/app-frame.component.html - 88 + 89 src/app/components/app-frame/app-frame.component.html - 90 + 91 src/app/components/document-list/document-list.component.ts @@ -332,26 +332,26 @@ src/app/components/manage/management-list/management-list.component.html - 101 + 102 src/app/components/manage/management-list/management-list.component.html - 101 + 102 src/app/components/manage/management-list/management-list.component.html - 101 + 102 src/app/components/manage/management-list/management-list.component.html - 101 + 102 Settings src/app/app.component.ts - 162 + 164 src/app/components/admin/settings/settings.component.html @@ -359,89 +359,89 @@ src/app/components/app-frame/app-frame.component.html - 50 + 51 src/app/components/app-frame/app-frame.component.html - 244 + 245 src/app/components/app-frame/app-frame.component.html - 246 + 247 Prev src/app/app.component.ts - 168 + 170 Next src/app/app.component.ts - 169 + 171 src/app/components/document-detail/document-detail.component.html - 95 + 100 End src/app/app.component.ts - 170 + 172 The dashboard can be used to show saved views, such as an 'Inbox'. Views are found under Manage > Saved Views once you have created some. src/app/app.component.ts - 176 + 178 Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms. src/app/app.component.ts - 183 + 185 The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar. src/app/app.component.ts - 188 + 190 The filtering tools allow you to quickly find documents using various searches, dates, tags, etc. src/app/app.component.ts - 195 + 197 Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar. src/app/app.component.ts - 201 + 203 Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view. src/app/app.component.ts - 206 + 208 Manage e-mail accounts and rules for automatically importing documents. src/app/app.component.ts - 214 + 216 src/app/components/manage/mail/mail.component.html @@ -452,14 +452,14 @@ Workflows give you more control over the document pipeline. src/app/app.component.ts - 222 + 224 File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process. src/app/app.component.ts - 230 + 232 src/app/components/admin/tasks/tasks.component.html @@ -470,28 +470,28 @@ Check out the settings for various tweaks to the web app. src/app/app.component.ts - 238 + 240 Thank you! 🙏 src/app/app.component.ts - 246 + 248 There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues. src/app/app.component.ts - 248 + 250 Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx! src/app/app.component.ts - 250 + 252 @@ -534,7 +534,7 @@ src/app/components/document-detail/document-detail.component.html - 348 + 353 @@ -545,7 +545,7 @@ src/app/components/admin/settings/settings.component.html - 349 + 364 src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html @@ -569,7 +569,7 @@ src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 75 + 76 src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html @@ -593,7 +593,7 @@ src/app/components/document-detail/document-detail.component.html - 341 + 346 src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html @@ -658,11 +658,11 @@ src/app/components/app-frame/app-frame.component.html - 279 + 280 src/app/components/app-frame/app-frame.component.html - 282 + 283 @@ -739,7 +739,7 @@ src/app/components/document-detail/document-detail.component.html - 361 + 366 src/app/components/document-list/document-list.component.html @@ -944,60 +944,25 @@ 152 - - Document editing - - src/app/components/admin/settings/settings.component.html - 157 - - - - Use PDF viewer provided by the browser - - src/app/components/admin/settings/settings.component.html - 161 - - - - This is usually faster for displaying large PDF documents, but it might not work on some browsers. - - src/app/components/admin/settings/settings.component.html - 161 - - - - Automatically remove inbox tag(s) on save - - src/app/components/admin/settings/settings.component.html - 167 - - - - Show document thumbnail during loading - - src/app/components/admin/settings/settings.component.html - 173 - - Update checking src/app/components/admin/settings/settings.component.html - 178 + 157 Enable update checking src/app/components/admin/settings/settings.component.html - 181 + 160 What's this? src/app/components/admin/settings/settings.component.html - 182 + 161 src/app/components/common/page-header/page-header.component.html @@ -1012,97 +977,29 @@ Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually. src/app/components/admin/settings/settings.component.html - 186,188 + 165,167 No tracking data is collected by the app in any way. src/app/components/admin/settings/settings.component.html - 190 - - - - Bulk editing - - src/app/components/admin/settings/settings.component.html - 196 - - - - Show confirmation dialogs - - src/app/components/admin/settings/settings.component.html - 199 - - - - Apply on close - - src/app/components/admin/settings/settings.component.html - 200 - - - - Global search - - src/app/components/admin/settings/settings.component.html - 204 - - - src/app/components/app-frame/global-search/global-search.component.ts - 120 - - - - Do not include advanced search results - - src/app/components/admin/settings/settings.component.html - 207 - - - - Full search links to - - src/app/components/admin/settings/settings.component.html - 215 - - - - Title and content search - - src/app/components/admin/settings/settings.component.html - 219 - - - - Advanced search - - src/app/components/admin/settings/settings.component.html - 220 - - - src/app/components/app-frame/global-search/global-search.component.html - 24 - - - src/app/components/document-list/filter-editor/filter-editor.component.ts - 166 + 169 Saved Views src/app/components/admin/settings/settings.component.html - 227 + 175 src/app/components/app-frame/app-frame.component.html - 204 + 205 src/app/components/app-frame/app-frame.component.html - 206 + 207 src/app/components/manage/saved-views/saved-views.component.html @@ -1113,14 +1010,77 @@ Show warning when closing saved views with unsaved changes src/app/components/admin/settings/settings.component.html - 230 + 178 + + + + Document editing + + src/app/components/admin/settings/settings.component.html + 184 + + + + Use PDF viewer provided by the browser + + src/app/components/admin/settings/settings.component.html + 188 + + + + This is usually faster for displaying large PDF documents, but it might not work on some browsers. + + src/app/components/admin/settings/settings.component.html + 188 + + + + Default zoom: + + src/app/components/admin/settings/settings.component.html + 194 + + + + Fit width + + src/app/components/admin/settings/settings.component.html + 198 + + + + Fit page + + src/app/components/admin/settings/settings.component.html + 199 + + + + Only applies to the Paperless-ngx PDF viewer. + + src/app/components/admin/settings/settings.component.html + 201 + + + + Automatically remove inbox tag(s) on save + + src/app/components/admin/settings/settings.component.html + 207 + + + + Show document thumbnail during loading + + src/app/components/admin/settings/settings.component.html + 213 Notes src/app/components/admin/settings/settings.component.html - 234 + 217 src/app/components/document-list/document-list.component.html @@ -1139,14 +1099,82 @@ Enable notes src/app/components/admin/settings/settings.component.html - 237 + 220 + + + + Bulk editing + + src/app/components/admin/settings/settings.component.html + 224 + + + + Show confirmation dialogs + + src/app/components/admin/settings/settings.component.html + 227 + + + + Apply on close + + src/app/components/admin/settings/settings.component.html + 228 + + + + Global search + + src/app/components/admin/settings/settings.component.html + 232 + + + src/app/components/app-frame/global-search/global-search.component.ts + 120 + + + + Do not include advanced search results + + src/app/components/admin/settings/settings.component.html + 235 + + + + Full search links to + + src/app/components/admin/settings/settings.component.html + 243 + + + + Title and content search + + src/app/components/admin/settings/settings.component.html + 247 + + + + Advanced search + + src/app/components/admin/settings/settings.component.html + 248 + + + src/app/components/app-frame/global-search/global-search.component.html + 24 + + + src/app/components/document-list/filter-editor/filter-editor.component.ts + 173 Permissions src/app/components/admin/settings/settings.component.html - 247 + 262 src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html @@ -1162,7 +1190,7 @@ src/app/components/document-detail/document-detail.component.html - 317 + 322 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -1209,28 +1237,28 @@ Default Permissions src/app/components/admin/settings/settings.component.html - 250 + 265 Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI src/app/components/admin/settings/settings.component.html - 254,256 + 269,271 Default Owner src/app/components/admin/settings/settings.component.html - 261 + 276 Objects without an owner can be viewed and edited by all users src/app/components/admin/settings/settings.component.html - 265 + 280 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -1241,18 +1269,18 @@ Default View Permissions src/app/components/admin/settings/settings.component.html - 270 + 285 Users: src/app/components/admin/settings/settings.component.html - 275 + 290 src/app/components/admin/settings/settings.component.html - 302 + 317 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1283,11 +1311,11 @@ Groups: src/app/components/admin/settings/settings.component.html - 285 + 300 src/app/components/admin/settings/settings.component.html - 312 + 327 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1318,14 +1346,14 @@ Default Edit Permissions src/app/components/admin/settings/settings.component.html - 297 + 312 Edit permissions also grant viewing permissions src/app/components/admin/settings/settings.component.html - 321 + 336 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1344,56 +1372,60 @@ Notifications src/app/components/admin/settings/settings.component.html - 329 + 344 + + + src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html + 11 Document processing src/app/components/admin/settings/settings.component.html - 332 + 347 Show notifications when new documents are detected src/app/components/admin/settings/settings.component.html - 336 + 351 Show notifications when document processing completes successfully src/app/components/admin/settings/settings.component.html - 337 + 352 Show notifications when document processing fails src/app/components/admin/settings/settings.component.html - 338 + 353 Suppress notifications on dashboard src/app/components/admin/settings/settings.component.html - 339 + 354 This will suppress all messages about document processing status on the dashboard. src/app/components/admin/settings/settings.component.html - 339 + 354 Cancel src/app/components/admin/settings/settings.component.html - 350 + 365 src/app/components/common/confirm-dialog/confirm-dialog.component.ts @@ -1421,7 +1453,7 @@ src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 74 + 75 src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html @@ -1468,21 +1500,21 @@ Use system language src/app/components/admin/settings/settings.component.ts - 75 + 76 Use date format of display language src/app/components/admin/settings/settings.component.ts - 78 + 79 Error retrieving users src/app/components/admin/settings/settings.component.ts - 213 + 217 src/app/components/admin/users-groups/users-groups.component.ts @@ -1493,7 +1525,7 @@ Error retrieving groups src/app/components/admin/settings/settings.component.ts - 232 + 236 src/app/components/admin/users-groups/users-groups.component.ts @@ -1504,32 +1536,32 @@ Settings were saved successfully. src/app/components/admin/settings/settings.component.ts - 521 + 532 Settings were saved successfully. Reload is required to apply some changes. src/app/components/admin/settings/settings.component.ts - 525 + 536 Reload now src/app/components/admin/settings/settings.component.ts - 526 + 537 An error occurred while saving settings. src/app/components/admin/settings/settings.component.ts - 536 + 547 src/app/components/app-frame/app-frame.component.ts - 159 + 161 @@ -1540,11 +1572,11 @@ src/app/components/app-frame/app-frame.component.html - 267 + 268 src/app/components/app-frame/app-frame.component.html - 269 + 270 @@ -1757,7 +1789,7 @@ src/app/components/document-detail/document-detail.component.html - 45 + 50 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -1938,11 +1970,11 @@ src/app/components/app-frame/app-frame.component.html - 227 + 228 src/app/components/app-frame/app-frame.component.html - 230 + 231 @@ -2014,7 +2046,7 @@ src/app/components/admin/trash/trash.component.ts - 111 + 116 src/app/components/admin/users-groups/users-groups.component.html @@ -2030,7 +2062,7 @@ src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 64 + 88 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -2098,39 +2130,39 @@ src/app/components/manage/management-list/management-list.component.html - 83 + 84 src/app/components/manage/management-list/management-list.component.html - 83 + 84 src/app/components/manage/management-list/management-list.component.html - 83 + 84 src/app/components/manage/management-list/management-list.component.html - 83 + 84 src/app/components/manage/management-list/management-list.component.html - 95 + 96 src/app/components/manage/management-list/management-list.component.html - 95 + 96 src/app/components/manage/management-list/management-list.component.html - 95 + 96 src/app/components/manage/management-list/management-list.component.html - 95 + 96 src/app/components/manage/management-list/management-list.component.ts - 225 + 224 src/app/components/manage/saved-views/saved-views.component.html @@ -2160,15 +2192,15 @@ src/app/components/admin/trash/trash.component.ts - 105 + 110 src/app/components/manage/management-list/management-list.component.ts - 221 + 220 src/app/components/manage/management-list/management-list.component.ts - 338 + 337 @@ -2186,7 +2218,7 @@ src/app/components/admin/trash/trash.component.ts - 109 + 114 src/app/components/admin/users-groups/users-groups.component.ts @@ -2194,7 +2226,7 @@ src/app/components/admin/users-groups/users-groups.component.ts - 174 + 177 src/app/components/manage/custom-fields/custom-fields.component.ts @@ -2206,89 +2238,85 @@ src/app/components/manage/mail/mail.component.ts - 286 + 296 src/app/components/manage/management-list/management-list.component.ts - 340 + 339 src/app/components/manage/workflows/workflows.component.ts 137 - - Document deleted + + Document "" deleted src/app/components/admin/trash/trash.component.ts - 89 + 90 - - Error deleting document + + Error deleting document "" src/app/components/admin/trash/trash.component.ts - 94 - - - src/app/components/document-detail/document-detail.component.ts - 924 + 97 This operation will permanently delete the selected documents. src/app/components/admin/trash/trash.component.ts - 107 + 112 This operation will permanently delete all documents in the trash. src/app/components/admin/trash/trash.component.ts - 108 + 113 Document(s) deleted src/app/components/admin/trash/trash.component.ts - 119 + 124 Error deleting document(s) src/app/components/admin/trash/trash.component.ts - 126 + 131 - - Document restored + + Document "" restored src/app/components/admin/trash/trash.component.ts - 139 + 144 - - Error restoring document + + Error restoring document "" src/app/components/admin/trash/trash.component.ts - 149 + 155 Document(s) restored src/app/components/admin/trash/trash.component.ts - 159 + 167 Error restoring document(s) src/app/components/admin/trash/trash.component.ts - 165 + 173 @@ -2299,11 +2327,11 @@ src/app/components/app-frame/app-frame.component.html - 258 + 259 src/app/components/app-frame/app-frame.component.html - 260 + 261 @@ -2405,35 +2433,35 @@ src/app/components/manage/management-list/management-list.component.html - 82 + 83 src/app/components/manage/management-list/management-list.component.html - 82 + 83 src/app/components/manage/management-list/management-list.component.html - 82 + 83 src/app/components/manage/management-list/management-list.component.html - 82 + 83 src/app/components/manage/management-list/management-list.component.html - 92 + 93 src/app/components/manage/management-list/management-list.component.html - 92 + 93 src/app/components/manage/management-list/management-list.component.html - 92 + 93 src/app/components/manage/management-list/management-list.component.html - 92 + 93 src/app/components/manage/workflows/workflows.component.html @@ -2505,35 +2533,35 @@ src/app/components/admin/users-groups/users-groups.component.ts - 176 + 179 src/app/components/document-detail/document-detail.component.ts - 948 + 958 src/app/components/document-detail/document-detail.component.ts - 1255 + 1318 src/app/components/document-detail/document-detail.component.ts - 1294 + 1357 src/app/components/document-detail/document-detail.component.ts - 1335 + 1398 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 793 + 796 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 826 + 829 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 845 + 848 src/app/components/manage/custom-fields/custom-fields.component.ts @@ -2545,71 +2573,71 @@ src/app/components/manage/mail/mail.component.ts - 288 + 298 src/app/components/manage/management-list/management-list.component.ts - 342 + 341 src/app/components/manage/workflows/workflows.component.ts 139 - - Deleted user + + Deleted user "" src/app/components/admin/users-groups/users-groups.component.ts 132 - - Error deleting user. + + Error deleting user "". src/app/components/admin/users-groups/users-groups.component.ts - 138 + 139 Saved group "". src/app/components/admin/users-groups/users-groups.component.ts - 156 + 159 Error saving group. src/app/components/admin/users-groups/users-groups.component.ts - 164 + 167 Confirm delete user group src/app/components/admin/users-groups/users-groups.component.ts - 172 + 175 This operation will permanently delete this user group. src/app/components/admin/users-groups/users-groups.component.ts - 173 + 176 - - Deleted group + + Deleted group "" src/app/components/admin/users-groups/users-groups.component.ts - 182 + 185 - - Error deleting group. + + Error deleting group "". src/app/components/admin/users-groups/users-groups.component.ts - 188 + 192 @@ -2623,98 +2651,98 @@ Logged in as src/app/components/app-frame/app-frame.component.html - 42 + 43 My Profile src/app/components/app-frame/app-frame.component.html - 46 + 47 Logout src/app/components/app-frame/app-frame.component.html - 53 + 54 Documentation src/app/components/app-frame/app-frame.component.html - 58 + 59 src/app/components/app-frame/app-frame.component.html - 288 + 289 src/app/components/app-frame/app-frame.component.html - 291 + 292 Saved views src/app/components/app-frame/app-frame.component.html - 98 + 99 src/app/components/app-frame/app-frame.component.html - 103 + 104 Open documents src/app/components/app-frame/app-frame.component.html - 130 + 131 Close all src/app/components/app-frame/app-frame.component.html - 150 + 151 src/app/components/app-frame/app-frame.component.html - 152 + 153 Manage src/app/components/app-frame/app-frame.component.html - 161 + 162 Correspondents src/app/components/app-frame/app-frame.component.html - 167 + 168 src/app/components/app-frame/app-frame.component.html - 169 + 170 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html - 106 + 107 Tags src/app/components/app-frame/app-frame.component.html - 174 + 175 src/app/components/app-frame/app-frame.component.html - 177 + 178 src/app/components/common/input/tags/tags.component.ts @@ -2722,7 +2750,7 @@ src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html - 93 + 94 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -2745,41 +2773,41 @@ Document Types src/app/components/app-frame/app-frame.component.html - 183 + 184 src/app/components/app-frame/app-frame.component.html - 185 + 186 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html - 119 + 120 Storage Paths src/app/components/app-frame/app-frame.component.html - 190 + 191 src/app/components/app-frame/app-frame.component.html - 192 + 193 src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html - 132 + 133 Custom Fields src/app/components/app-frame/app-frame.component.html - 197 + 198 src/app/components/app-frame/app-frame.component.html - 199 + 200 src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html @@ -2794,11 +2822,11 @@ Workflows src/app/components/app-frame/app-frame.component.html - 213 + 214 src/app/components/app-frame/app-frame.component.html - 215 + 216 src/app/components/manage/workflows/workflows.component.html @@ -2809,92 +2837,92 @@ Mail src/app/components/app-frame/app-frame.component.html - 220 + 221 src/app/components/app-frame/app-frame.component.html - 223 + 224 Administration src/app/components/app-frame/app-frame.component.html - 238 + 239 Configuration src/app/components/app-frame/app-frame.component.html - 251 + 252 src/app/components/app-frame/app-frame.component.html - 253 + 254 GitHub src/app/components/app-frame/app-frame.component.html - 298 + 299 is available. src/app/components/app-frame/app-frame.component.html - 307,308 + 308,309 Click to view. src/app/components/app-frame/app-frame.component.html - 308 + 309 Paperless-ngx can automatically check for updates src/app/components/app-frame/app-frame.component.html - 312 + 313 How does this work? src/app/components/app-frame/app-frame.component.html - 319,321 + 320,322 Update available src/app/components/app-frame/app-frame.component.html - 332 + 333 Sidebar views updated src/app/components/app-frame/app-frame.component.ts - 243 + 245 Error updating sidebar views src/app/components/app-frame/app-frame.component.ts - 246 + 248 An error occurred while saving update checking settings. src/app/components/app-frame/app-frame.component.ts - 267 + 269 @@ -2954,7 +2982,7 @@ src/app/components/document-detail/document-detail.component.html - 29 + 34 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -3082,6 +3110,20 @@ 250 + + Clear All + + src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html + 16 + + + + No notifications + + src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html + 20 + + Clear @@ -3115,31 +3157,31 @@ src/app/components/document-detail/document-detail.component.ts - 901 + 911 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 436 + 439 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 476 + 479 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 514 + 517 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 552 + 555 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 614 + 617 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 747 + 750 @@ -3294,48 +3336,114 @@ 93 - - True + + Today + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 39 + + + src/app/components/common/dates-dropdown/dates-dropdown.component.html + 50 + + + src/app/components/common/dates-dropdown/dates-dropdown.component.html + 76 + + + src/app/components/common/dates-dropdown/dates-dropdown.component.html + 126 + + + src/app/components/common/dates-dropdown/dates-dropdown.component.html + 152 + + + src/app/components/common/input/date/date.component.html + 21 + + + + Close src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html 40 - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + src/app/components/common/dates-dropdown/dates-dropdown.component.html + 51 + + + src/app/components/common/dates-dropdown/dates-dropdown.component.html 77 + + src/app/components/common/dates-dropdown/dates-dropdown.component.html + 127 + + + src/app/components/common/dates-dropdown/dates-dropdown.component.html + 153 + + + src/app/components/common/input/date/date.component.html + 22 + + + src/app/components/document-detail/document-detail.component.html + 94 + + + src/app/components/document-detail/document-detail.component.ts + 1375 + + + src/app/guards/dirty-saved-view.guard.ts + 37 + + + + True src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 83 + 47 + + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 84 + + + src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html + 90 False src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 41 + 48 src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 78 + 85 src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 84 + 91 Search docs... src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 100 + 107 Any src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 132 + 139 src/app/components/common/filterable-dropdown/filterable-dropdown.component.html @@ -3346,7 +3454,7 @@ All src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 134 + 141 src/app/components/common/filterable-dropdown/filterable-dropdown.component.html @@ -3373,21 +3481,21 @@ Not src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 137 + 144 Add query src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 156 + 163 Add expression src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 159 + 166 @@ -3398,36 +3506,36 @@ src/app/components/common/dates-dropdown/dates-dropdown.component.html - 89 + 101 - - After + + From src/app/components/common/dates-dropdown/dates-dropdown.component.html 42 src/app/components/common/dates-dropdown/dates-dropdown.component.html - 106 + 118 - - Before + + To src/app/components/common/dates-dropdown/dates-dropdown.component.html - 62 + 68 src/app/components/common/dates-dropdown/dates-dropdown.component.html - 126 + 144 Added src/app/components/common/dates-dropdown/dates-dropdown.component.html - 74 + 86 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts @@ -3446,41 +3554,33 @@ 93 - - Last 7 days + + Within 1 week src/app/components/common/dates-dropdown/dates-dropdown.component.ts 67 - - Last month + + Within 1 month src/app/components/common/dates-dropdown/dates-dropdown.component.ts 72 - - src/app/pipes/custom-date.pipe.ts - 19 - - - Last 3 months + + Within 3 months src/app/components/common/dates-dropdown/dates-dropdown.component.ts 77 - - Last year + + Within 1 year src/app/components/common/dates-dropdown/dates-dropdown.component.ts 82 - - src/app/pipes/custom-date.pipe.ts - 14 - Matching algorithm @@ -3889,71 +3989,78 @@ 43 + + PDF layout + + src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html + 44 + + Include only files matching src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 46 + 47 Optional. Wildcards e.g. *.pdf or *invoice* allowed. Can be comma-separated list. Case insensitive. src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 46 + 47 src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 47 + 48 Exclude files matching src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 47 + 48 Action src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 53 + 54 Only performed if the mail is processed. src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 53 + 54 Action parameter src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 55 + 56 Assign title from src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 57 + 58 Assign owner from rule src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 58 + 59 Assign document type src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 62 + 63 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -3964,14 +4071,14 @@ Assign correspondent from src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 63 + 64 Assign correspondent src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 65 + 66 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -3982,138 +4089,173 @@ Error src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html - 72 + 73 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html 111 - src/app/components/common/toasts/toasts.component.html - 28 + src/app/components/common/toast/toast.component.html + 30 Only process attachments src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 38 + 39 src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 49 + 50 Process all files, including 'inline' attachments src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 42 + 43 Process message as .eml src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 53 + 54 Process message as .eml and attachments separately src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 57 + 58 + + + + System default + + src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts + 65 + + + + Text, then HTML + + src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts + 69 + + + + HTML, then text + + src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts + 73 + + + + HTML only + + src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts + 77 + + + + Text only + + src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts + 81 Move to specified folder src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 68 + 92 Mark as read, don't process read mails src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 72 + 96 Flag the mail, don't process flagged mails src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 76 + 100 Tag the mail with specified tag, don't process tagged mails src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 80 + 104 Use subject as title src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 87 + 111 Use attachment filename as title src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 91 + 115 Do not assign title from this rule src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 95 + 119 Do not assign a correspondent src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 102 + 126 Use mail address src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 106 + 130 Use name (or mail address if not available) src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 110 + 134 Use correspondent selected below src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 114 + 138 Create new mail rule src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 166 + 190 Edit mail rule src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts - 170 + 194 @@ -4142,7 +4284,7 @@ src/app/components/document-detail/document-detail.component.html - 283 + 288 @@ -4974,7 +5116,7 @@ Not assigned src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts - 380 + 392 Filter drop down element to filter for documents with no correspondent/type/tag assigned @@ -4982,7 +5124,7 @@ Open filter src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts - 501 + 513 @@ -5047,14 +5189,14 @@ Invalid date. src/app/components/common/input/date/date.component.html - 25 + 31 Suggestions: src/app/components/common/input/date/date.component.html - 31 + 37 src/app/components/common/input/select/select.component.html @@ -5069,7 +5211,7 @@ Filter documents with this src/app/components/common/input/date/date.component.ts - 121 + 123 src/app/components/common/input/select/select.component.ts @@ -5781,8 +5923,8 @@ 47 - src/app/components/common/toasts/toasts.component.html - 26 + src/app/components/common/toast/toast.component.html + 28 src/app/components/manage/mail/mail.component.html @@ -5873,8 +6015,8 @@ Copy Raw Error - src/app/components/common/toasts/toasts.component.html - 41 + src/app/components/common/toast/toast.component.html + 43 @@ -6050,7 +6192,7 @@ Other src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts - 79 + 83 @@ -6119,7 +6261,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 381 + 384 this string is used to separate processing, failed and added on the file upload widget @@ -6183,14 +6325,14 @@ Download original src/app/components/document-detail/document-detail.component.html - 36 + 41 Reprocess src/app/components/document-detail/document-detail.component.html - 49 + 54 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -6201,7 +6343,7 @@ More like this src/app/components/document-detail/document-detail.component.html - 53 + 58 src/app/components/document-list/document-card-large/document-card-large.component.html @@ -6212,14 +6354,14 @@ Split src/app/components/document-detail/document-detail.component.html - 57 + 62 Rotate src/app/components/document-detail/document-detail.component.html - 61 + 66 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -6230,43 +6372,28 @@ Delete page(s) src/app/components/document-detail/document-detail.component.html - 65 - - - - Close - - src/app/components/document-detail/document-detail.component.html - 89 - - - src/app/components/document-detail/document-detail.component.ts - 1312 - - - src/app/guards/dirty-saved-view.guard.ts - 37 + 70 Previous src/app/components/document-detail/document-detail.component.html - 92 + 97 Details src/app/components/document-detail/document-detail.component.html - 105 + 110 Title src/app/components/document-detail/document-detail.component.html - 108 + 113 src/app/components/document-list/document-list.component.html @@ -6274,7 +6401,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 154 + 160 src/app/data/document.ts @@ -6289,21 +6416,21 @@ Archive serial number src/app/components/document-detail/document-detail.component.html - 109 + 114 Date created src/app/components/document-detail/document-detail.component.html - 110 + 115 Correspondent src/app/components/document-detail/document-detail.component.html - 112 + 117 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -6330,7 +6457,7 @@ Document type src/app/components/document-detail/document-detail.component.html - 114 + 119 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -6357,7 +6484,7 @@ Storage path src/app/components/document-detail/document-detail.component.html - 116 + 121 src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -6380,7 +6507,7 @@ Default src/app/components/document-detail/document-detail.component.html - 117 + 122 src/app/components/manage/saved-views/saved-views.component.html @@ -6391,14 +6518,14 @@ Content src/app/components/document-detail/document-detail.component.html - 213 + 218 Metadata src/app/components/document-detail/document-detail.component.html - 222 + 227 src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts @@ -6409,175 +6536,175 @@ Date modified src/app/components/document-detail/document-detail.component.html - 229 + 234 Date added src/app/components/document-detail/document-detail.component.html - 233 + 238 Media filename src/app/components/document-detail/document-detail.component.html - 237 + 242 Original filename src/app/components/document-detail/document-detail.component.html - 241 + 246 Original MD5 checksum src/app/components/document-detail/document-detail.component.html - 245 + 250 Original file size src/app/components/document-detail/document-detail.component.html - 249 + 254 Original mime type src/app/components/document-detail/document-detail.component.html - 253 + 258 Archive MD5 checksum src/app/components/document-detail/document-detail.component.html - 258 + 263 Archive file size src/app/components/document-detail/document-detail.component.html - 264 + 269 Original document metadata src/app/components/document-detail/document-detail.component.html - 273 + 278 Archived document metadata src/app/components/document-detail/document-detail.component.html - 276 + 281 Notes src/app/components/document-detail/document-detail.component.html - 295,298 + 300,303 History src/app/components/document-detail/document-detail.component.html - 306 + 311 Save & next src/app/components/document-detail/document-detail.component.html - 343 + 348 Save & close src/app/components/document-detail/document-detail.component.html - 346 + 351 Document loading... src/app/components/document-detail/document-detail.component.html - 356 + 361 Enter Password src/app/components/document-detail/document-detail.component.html - 410 + 415 An error occurred loading content: src/app/components/document-detail/document-detail.component.ts - 406,408 + 412,414 Document changes detected src/app/components/document-detail/document-detail.component.ts - 436 + 435 The version of this document in your browser session appears older than the existing version. src/app/components/document-detail/document-detail.component.ts - 437 + 436 Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document. src/app/components/document-detail/document-detail.component.ts - 438 + 437 Ok src/app/components/document-detail/document-detail.component.ts - 440 + 439 Next document src/app/components/document-detail/document-detail.component.ts - 547 + 546 Previous document src/app/components/document-detail/document-detail.component.ts - 557 + 556 Close document src/app/components/document-detail/document-detail.component.ts - 565 + 564 src/app/services/open-documents.service.ts @@ -6588,224 +6715,241 @@ Save document src/app/components/document-detail/document-detail.component.ts - 572 + 571 Save and close / next src/app/components/document-detail/document-detail.component.ts - 581 + 580 Error retrieving metadata src/app/components/document-detail/document-detail.component.ts - 633 + 632 Error retrieving suggestions. src/app/components/document-detail/document-detail.component.ts - 662 + 661 - - Document saved successfully. + + Document "" saved successfully. src/app/components/document-detail/document-detail.component.ts 813 src/app/components/document-detail/document-detail.component.ts - 827 + 829 + + + + Error saving document "" + + src/app/components/document-detail/document-detail.component.ts + 835 Error saving document src/app/components/document-detail/document-detail.component.ts - 831 - - - src/app/components/document-detail/document-detail.component.ts - 874 + 880 Do you really want to move the document "" to the trash? src/app/components/document-detail/document-detail.component.ts - 902 + 912 Documents can be restored prior to permanent deletion. src/app/components/document-detail/document-detail.component.ts - 903 + 913 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 749 + 752 Move to trash src/app/components/document-detail/document-detail.component.ts - 905 + 915 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 751 + 754 + + + + Error deleting document + + src/app/components/document-detail/document-detail.component.ts + 934 Reprocess confirm src/app/components/document-detail/document-detail.component.ts - 944 + 954 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 789 + 792 This operation will permanently recreate the archive file for this document. src/app/components/document-detail/document-detail.component.ts - 945 + 955 The archive file will be re-generated with the current settings. src/app/components/document-detail/document-detail.component.ts - 946 + 956 - - Reprocess operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content. + + Reprocess operation for "" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content. src/app/components/document-detail/document-detail.component.ts - 956 + 966 Error executing operation src/app/components/document-detail/document-detail.component.ts - 967 + 977 + + + + Error downloading document + + src/app/components/document-detail/document-detail.component.ts + 1024 Page Fit src/app/components/document-detail/document-detail.component.ts - 1040 + 1103 Split confirm src/app/components/document-detail/document-detail.component.ts - 1253 + 1316 This operation will split the selected document(s) into new documents. src/app/components/document-detail/document-detail.component.ts - 1254 + 1317 - - Split operation will begin in the background. + + Split operation for "" will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1270 + 1333 Error executing split operation src/app/components/document-detail/document-detail.component.ts - 1279 + 1342 Rotate confirm src/app/components/document-detail/document-detail.component.ts - 1292 + 1355 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 823 + 826 This operation will permanently rotate the original version of the current document. src/app/components/document-detail/document-detail.component.ts - 1293 + 1356 - - Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes. + + Rotation of "" will begin in the background. Close and re-open the document after the operation has completed to see the changes. src/app/components/document-detail/document-detail.component.ts - 1309 + 1372 Error executing rotate operation src/app/components/document-detail/document-detail.component.ts - 1321 + 1384 Delete pages confirm src/app/components/document-detail/document-detail.component.ts - 1333 + 1396 This operation will permanently delete the selected pages from the original document. src/app/components/document-detail/document-detail.component.ts - 1334 + 1397 - - Delete pages operation will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes. + + Delete pages operation for "" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes. src/app/components/document-detail/document-detail.component.ts - 1349 + 1412 Error executing delete pages operation src/app/components/document-detail/document-detail.component.ts - 1358 + 1421 An error occurred loading tiff: src/app/components/document-detail/document-detail.component.ts - 1398 + 1461 src/app/components/document-detail/document-detail.component.ts - 1402 + 1465 @@ -6885,7 +7029,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 162 + 168 @@ -6941,25 +7085,25 @@ Error executing bulk operation src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 285 + 288 "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 373 + 376 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 379 + 382 "" and "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 375 + 378 This is for messages like 'modify "tag1" and "tag2"' @@ -6967,7 +7111,7 @@ and "" src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 383,385 + 386,388 this is for messages like 'modify "tag1", "tag2" and "tag3"' @@ -6975,14 +7119,14 @@ Confirm tags assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 400 + 403 This operation will add the tag "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 406 + 409 @@ -6991,14 +7135,14 @@ )"/> to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 411,413 + 414,416 This operation will remove the tag "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 419 + 422 @@ -7007,7 +7151,7 @@ )"/> from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 424,426 + 427,429 @@ -7018,84 +7162,84 @@ )"/> on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 428,432 + 431,435 Confirm correspondent assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 469 + 472 This operation will assign the correspondent "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 471 + 474 This operation will remove the correspondent from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 473 + 476 Confirm document type assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 507 + 510 This operation will assign the document type "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 509 + 512 This operation will remove the document type from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 511 + 514 Confirm storage path assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 545 + 548 This operation will assign the storage path "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 547 + 550 This operation will remove the storage path from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 549 + 552 Confirm custom field assignment src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 578 + 581 This operation will assign the custom field "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 584 + 587 @@ -7104,14 +7248,14 @@ )"/> to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 589,591 + 592,594 This operation will remove the custom field "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 597 + 600 @@ -7120,7 +7264,7 @@ )"/> from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 602,604 + 605,607 @@ -7131,70 +7275,70 @@ )"/> on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 606,610 + 609,613 Move selected document(s) to the trash? src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 748 + 751 This operation will permanently recreate the archive files for selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 790 + 793 The archive files will be re-generated with the current settings. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 791 + 794 This operation will permanently rotate the original version of document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 824 + 827 Merge confirm src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 843 + 846 This operation will merge selected documents into a new document. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 844 + 847 Merged document will be queued for consumption. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 860 + 863 Custom fields updated. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 882 + 885 Error updating custom fields. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 891 + 894 @@ -7373,7 +7517,7 @@ src/app/components/document-list/document-list.component.ts - 310 + 314 @@ -7384,7 +7528,7 @@ src/app/components/document-list/document-list.component.ts - 303 + 307 @@ -7487,7 +7631,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 159 + 165 src/app/data/document.ts @@ -7627,42 +7771,42 @@ Reset filters / selection src/app/components/document-list/document-list.component.ts - 291 + 295 Open first [selected] document src/app/components/document-list/document-list.component.ts - 319 + 323 Previous page src/app/components/document-list/document-list.component.ts - 335 + 339 Next page src/app/components/document-list/document-list.component.ts - 347 + 351 View "" saved successfully. src/app/components/document-list/document-list.component.ts - 379 + 383 View "" created successfully. src/app/components/document-list/document-list.component.ts - 422 + 426 @@ -7676,147 +7820,154 @@ Title & content src/app/components/document-list/filter-editor/filter-editor.component.ts - 157 + 163 + + + + File type + + src/app/components/document-list/filter-editor/filter-editor.component.ts + 170 More like src/app/components/document-list/filter-editor/filter-editor.component.ts - 172 + 179 equals src/app/components/document-list/filter-editor/filter-editor.component.ts - 178 + 185 is empty src/app/components/document-list/filter-editor/filter-editor.component.ts - 182 + 189 is not empty src/app/components/document-list/filter-editor/filter-editor.component.ts - 186 + 193 greater than src/app/components/document-list/filter-editor/filter-editor.component.ts - 190 + 197 less than src/app/components/document-list/filter-editor/filter-editor.component.ts - 194 + 201 Correspondent: src/app/components/document-list/filter-editor/filter-editor.component.ts - 226,228 + 233,235 Without correspondent src/app/components/document-list/filter-editor/filter-editor.component.ts - 230 + 237 Document type: src/app/components/document-list/filter-editor/filter-editor.component.ts - 236,238 + 243,245 Without document type src/app/components/document-list/filter-editor/filter-editor.component.ts - 240 + 247 Storage path: src/app/components/document-list/filter-editor/filter-editor.component.ts - 246,248 + 253,255 Without storage path src/app/components/document-list/filter-editor/filter-editor.component.ts - 250 + 257 Tag: src/app/components/document-list/filter-editor/filter-editor.component.ts - 254,256 + 261,263 Without any tag src/app/components/document-list/filter-editor/filter-editor.component.ts - 260 + 267 Custom fields query src/app/components/document-list/filter-editor/filter-editor.component.ts - 264 + 271 Title: src/app/components/document-list/filter-editor/filter-editor.component.ts - 267 + 274 ASN: src/app/components/document-list/filter-editor/filter-editor.component.ts - 270 + 277 Owner: src/app/components/document-list/filter-editor/filter-editor.component.ts - 273 + 280 Owner not in: src/app/components/document-list/filter-editor/filter-editor.component.ts - 276 + 283 Without an owner src/app/components/document-list/filter-editor/filter-editor.component.ts - 279 + 286 @@ -7979,19 +8130,19 @@ src/app/components/manage/management-list/management-list.component.html - 85 + 86 src/app/components/manage/management-list/management-list.component.html - 85 + 86 src/app/components/manage/management-list/management-list.component.html - 85 + 86 src/app/components/manage/management-list/management-list.component.html - 85 + 86 @@ -8015,18 +8166,18 @@ 102 - - Deleted field + + Deleted field "" src/app/components/manage/custom-fields/custom-fields.component.ts 111 - - Error deleting field. + + Error deleting field "". src/app/components/manage/custom-fields/custom-fields.component.ts - 117 + 118 @@ -8205,113 +8356,113 @@ 194 - - Deleted mail account + + Deleted mail account "" src/app/components/manage/mail/mail.component.ts - 203 + 204 - - Error deleting mail account. + + Error deleting mail account "". src/app/components/manage/mail/mail.component.ts - 213 + 215 - - Processing mail account - - src/app/components/manage/mail/mail.component.ts - 224 - - - - Error processing mail account + + Processing mail account "" src/app/components/manage/mail/mail.component.ts 227 + + Error processing mail account "" + + src/app/components/manage/mail/mail.component.ts + 232 + + Saved rule "". src/app/components/manage/mail/mail.component.ts - 243 + 250 Error saving rule. src/app/components/manage/mail/mail.component.ts - 254 + 261 Rule "" enabled. src/app/components/manage/mail/mail.component.ts - 270 + 277 Rule "" disabled. src/app/components/manage/mail/mail.component.ts - 271 + 278 - - Error toggling rule. + + Error toggling rule "". src/app/components/manage/mail/mail.component.ts - 275 + 283 Confirm delete mail rule src/app/components/manage/mail/mail.component.ts - 284 + 294 This operation will permanently delete this mail rule. src/app/components/manage/mail/mail.component.ts - 285 + 295 - - Deleted mail rule + + Deleted mail rule "" src/app/components/manage/mail/mail.component.ts - 294 + 305 - - Error deleting mail rule. + + Error deleting mail rule "". src/app/components/manage/mail/mail.component.ts - 303 + 316 Permissions updated src/app/components/manage/mail/mail.component.ts - 325 + 340 Error updating permissions src/app/components/manage/mail/mail.component.ts - 330 + 345 src/app/components/manage/management-list/management-list.component.ts - 325 + 324 @@ -8375,26 +8526,26 @@ {VAR_PLURAL, plural, =1 {One } other { total }} src/app/components/manage/management-list/management-list.component.html - 116 + 118 src/app/components/manage/management-list/management-list.component.html - 116 + 118 src/app/components/manage/management-list/management-list.component.html - 116 + 118 src/app/components/manage/management-list/management-list.component.html - 116 + 118 Automatic src/app/components/manage/management-list/management-list.component.ts - 117 + 116 src/app/data/matching-model.ts @@ -8405,7 +8556,7 @@ None src/app/components/manage/management-list/management-list.component.ts - 119 + 118 src/app/data/matching-model.ts @@ -8416,70 +8567,70 @@ Successfully created . src/app/components/manage/management-list/management-list.component.ts - 178 + 177 Error occurred while creating . src/app/components/manage/management-list/management-list.component.ts - 183 + 182 - - Successfully updated . + + Successfully updated "". src/app/components/manage/management-list/management-list.component.ts - 198 + 197 Error occurred while saving . src/app/components/manage/management-list/management-list.component.ts - 203 + 202 Associated documents will not be deleted. src/app/components/manage/management-list/management-list.component.ts - 223 + 222 Error while deleting element src/app/components/manage/management-list/management-list.component.ts - 239 + 238 Permissions updated successfully src/app/components/manage/management-list/management-list.component.ts - 318 + 317 This operation will permanently delete all objects. src/app/components/manage/management-list/management-list.component.ts - 339 + 338 Objects deleted successfully src/app/components/manage/management-list/management-list.component.ts - 353 + 352 Error deleting objects src/app/components/manage/management-list/management-list.component.ts - 359 + 358 @@ -8643,39 +8794,39 @@ 136 - - Deleted workflow + + Deleted workflow "". src/app/components/manage/workflows/workflows.component.ts - 145 + 146 - - Error deleting workflow. + + Error deleting workflow "". src/app/components/manage/workflows/workflows.component.ts - 150 + 153 - - Enabled workflow + + Enabled workflow "" src/app/components/manage/workflows/workflows.component.ts - 161 + 166 - - Disabled workflow + + Disabled workflow "" src/app/components/manage/workflows/workflows.component.ts - 162 + 167 - - Error toggling workflow. + + Error toggling workflow "". src/app/components/manage/workflows/workflows.component.ts - 168 + 174 @@ -9108,6 +9259,13 @@ 36 + + Last year + + src/app/pipes/custom-date.pipe.ts + 14 + + %s years ago @@ -9115,6 +9273,13 @@ 15 + + Last month + + src/app/pipes/custom-date.pipe.ts + 19 + + %s months ago @@ -9192,122 +9357,6 @@ 11 - - Document already exists. - - src/app/services/consumer-status.service.ts - 17 - - - - Document already exists. Note: existing document is in the trash. - - src/app/services/consumer-status.service.ts - 18 - - - - Document with ASN already exists. - - src/app/services/consumer-status.service.ts - 19 - - - - Document with ASN already exists. Note: existing document is in the trash. - - src/app/services/consumer-status.service.ts - 20 - - - - File not found. - - src/app/services/consumer-status.service.ts - 21 - - - - Pre-consume script does not exist. - - src/app/services/consumer-status.service.ts - 22 - - Pre-Consume is a term that appears like that in the documentation as well and does not need a specific translation - - - Error while executing pre-consume script. - - src/app/services/consumer-status.service.ts - 23 - - Pre-Consume is a term that appears like that in the documentation as well and does not need a specific translation - - - Post-consume script does not exist. - - src/app/services/consumer-status.service.ts - 24 - - Post-Consume is a term that appears like that in the documentation as well and does not need a specific translation - - - Error while executing post-consume script. - - src/app/services/consumer-status.service.ts - 25 - - Post-Consume is a term that appears like that in the documentation as well and does not need a specific translation - - - Received new file. - - src/app/services/consumer-status.service.ts - 26 - - - - File type not supported. - - src/app/services/consumer-status.service.ts - 27 - - - - Processing document... - - src/app/services/consumer-status.service.ts - 28 - - - - Generating thumbnail... - - src/app/services/consumer-status.service.ts - 29 - - - - Retrieving date from document... - - src/app/services/consumer-status.service.ts - 30 - - - - Saving document... - - src/app/services/consumer-status.service.ts - 31 - - - - Finished. - - src/app/services/consumer-status.service.ts - 32 - - You have unsaved changes to the document @@ -9623,6 +9672,122 @@ 70 + + Document already exists. + + src/app/services/websocket-status.service.ts + 23 + + + + Document already exists. Note: existing document is in the trash. + + src/app/services/websocket-status.service.ts + 24 + + + + Document with ASN already exists. + + src/app/services/websocket-status.service.ts + 25 + + + + Document with ASN already exists. Note: existing document is in the trash. + + src/app/services/websocket-status.service.ts + 26 + + + + File not found. + + src/app/services/websocket-status.service.ts + 27 + + + + Pre-consume script does not exist. + + src/app/services/websocket-status.service.ts + 28 + + Pre-Consume is a term that appears like that in the documentation as well and does not need a specific translation + + + Error while executing pre-consume script. + + src/app/services/websocket-status.service.ts + 29 + + Pre-Consume is a term that appears like that in the documentation as well and does not need a specific translation + + + Post-consume script does not exist. + + src/app/services/websocket-status.service.ts + 30 + + Post-Consume is a term that appears like that in the documentation as well and does not need a specific translation + + + Error while executing post-consume script. + + src/app/services/websocket-status.service.ts + 31 + + Post-Consume is a term that appears like that in the documentation as well and does not need a specific translation + + + Received new file. + + src/app/services/websocket-status.service.ts + 32 + + + + File type not supported. + + src/app/services/websocket-status.service.ts + 33 + + + + Processing document... + + src/app/services/websocket-status.service.ts + 34 + + + + Generating thumbnail... + + src/app/services/websocket-status.service.ts + 35 + + + + Retrieving date from document... + + src/app/services/websocket-status.service.ts + 36 + + + + Saving document... + + src/app/services/websocket-status.service.ts + 37 + + + + Finished. + + src/app/services/websocket-status.service.ts + 38 + + diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index c09875247..37314ebb1 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -9,17 +9,17 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { - "@angular/cdk": "^19.0.2", - "@angular/common": "~19.0.3", - "@angular/compiler": "~19.0.3", - "@angular/core": "~19.0.3", - "@angular/forms": "~19.0.3", - "@angular/localize": "~19.0.3", - "@angular/platform-browser": "~19.0.3", - "@angular/platform-browser-dynamic": "~19.0.3", - "@angular/router": "~19.0.3", + "@angular/cdk": "^19.1.2", + "@angular/common": "~19.1.4", + "@angular/compiler": "~19.1.4", + "@angular/core": "~19.1.4", + "@angular/forms": "~19.1.4", + "@angular/localize": "~19.1.4", + "@angular/platform-browser": "~19.1.4", + "@angular/platform-browser-dynamic": "~19.1.4", + "@angular/router": "~19.1.4", "@ng-bootstrap/ng-bootstrap": "^18.0.0", - "@ng-select/ng-select": "^14.1.0", + "@ng-select/ng-select": "^14.2.0", "@ngneat/dirty-check-forms": "^3.0.3", "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", @@ -28,36 +28,37 @@ "ng2-pdf-viewer": "^10.4.0", "ngx-bootstrap-icons": "^1.9.3", "ngx-color": "^9.0.0", - "ngx-cookie-service": "^19.0.0", + "ngx-cookie-service": "^19.1.0", + "ngx-device-detector": "^9.0.0", "ngx-file-drop": "^16.0.0", "ngx-ui-tour-ng-bootstrap": "^16.0.0", "rxjs": "^7.8.1", "tslib": "^2.8.1", "utif": "^3.1.0", - "uuid": "^11.0.2", + "uuid": "^11.0.5", "zone.js": "^0.15.0" }, "devDependencies": { - "@angular-builders/custom-webpack": "^19.0.0-beta.0", - "@angular-builders/jest": "^19.0.0-beta.1", + "@angular-builders/custom-webpack": "^19.0.0", + "@angular-builders/jest": "^19.0.0", "@angular-devkit/build-angular": "^19.0.4", - "@angular-devkit/core": "^19.0.4", - "@angular-devkit/schematics": "^19.0.4", - "@angular-eslint/builder": "19.0.0", - "@angular-eslint/eslint-plugin": "19.0.0", - "@angular-eslint/eslint-plugin-template": "19.0.0", - "@angular-eslint/schematics": "19.0.0", - "@angular-eslint/template-parser": "19.0.0", - "@angular/cli": "~19.0.4", - "@angular/compiler-cli": "~19.0.3", - "@codecov/webpack-plugin": "^1.2.1", - "@playwright/test": "^1.48.2", + "@angular-devkit/core": "^19.1.5", + "@angular-devkit/schematics": "^19.1.5", + "@angular-eslint/builder": "19.0.2", + "@angular-eslint/eslint-plugin": "19.0.2", + "@angular-eslint/eslint-plugin-template": "19.0.2", + "@angular-eslint/schematics": "19.0.2", + "@angular-eslint/template-parser": "19.0.2", + "@angular/cli": "~19.1.5", + "@angular/compiler-cli": "~19.1.4", + "@codecov/webpack-plugin": "^1.8.0", + "@playwright/test": "^1.50.1", "@types/jest": "^29.5.14", - "@types/node": "^22.8.6", - "@typescript-eslint/eslint-plugin": "^8.12.2", - "@typescript-eslint/parser": "^8.12.2", + "@types/node": "^22.13.0", + "@typescript-eslint/eslint-plugin": "^8.22.0", + "@typescript-eslint/parser": "^8.22.0", "@typescript-eslint/utils": "^8.0.0", - "eslint": "^9.14.0", + "eslint": "^9.19.0", "jest": "29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-preset-angular": "^14.4.2", @@ -82,6 +83,7 @@ "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dev": true, + "license": "MIT", "dependencies": { "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1" @@ -92,6 +94,7 @@ "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "dev": true, + "license": "MIT", "dependencies": { "@actions/io": "^1.0.1" } @@ -101,6 +104,7 @@ "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", "dev": true, + "license": "MIT", "dependencies": { "@actions/http-client": "^2.2.0", "@octokit/core": "^5.0.1", @@ -113,6 +117,7 @@ "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", "dev": true, + "license": "MIT", "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" @@ -122,7 +127,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@ampproject/remapping": { "version": "2.3.0", @@ -137,9 +143,9 @@ } }, "node_modules/@angular-builders/common": { - "version": "3.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-3.0.0-beta.0.tgz", - "integrity": "sha512-3OUBr4UMUoyZJkDxne3HWAKLMLYZWf3nTQ/34ICErvkfBnV9NPsYs47nGuKechHWqhz7MVk2JeKztZw3hXWobA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-3.0.0.tgz", + "integrity": "sha512-AACGMwlBFYF3PaFekgJDCmqO1hMBrK5eyjHMN5aqJk3PV46BhnlNcQEa9pftLUKxoGijXBQzlalDZkceatyoMw==", "dev": true, "license": "MIT", "dependencies": { @@ -152,13 +158,13 @@ } }, "node_modules/@angular-builders/custom-webpack": { - "version": "19.0.0-beta.0", - "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-19.0.0-beta.0.tgz", - "integrity": "sha512-FdpYku8Q9rh6L04FU+yMZqWGKVY2OD9k5I0veIztYLoRFc9VxEPp7gmoO/9GmXfCL90zv89BPJw/QZ7sonQ5YA==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-19.0.0.tgz", + "integrity": "sha512-MD3N+OPl/50u+N6YJ6UwS1kOT5C6RkOsSih8F88lR/TACQCSDf0FLmJmROktNgRNADhySGnK18o874Vftyqi4w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-builders/common": "3.0.0-beta.0", + "@angular-builders/common": "3.0.0", "@angular-devkit/architect": ">=0.1900.0 < 0.2000.0", "@angular-devkit/build-angular": "^19.0.0", "@angular-devkit/core": "^19.0.0", @@ -173,13 +179,13 @@ } }, "node_modules/@angular-builders/jest": { - "version": "19.0.0-beta.1", - "resolved": "https://registry.npmjs.org/@angular-builders/jest/-/jest-19.0.0-beta.1.tgz", - "integrity": "sha512-s6NeJGHf09F/kArhnxj+DypEQ1nuloA5RwmkTjCCCGAsXnS8WdJiyizaTmWj1WiSnYkY2431HqjLfwvUmRo3qQ==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@angular-builders/jest/-/jest-19.0.0.tgz", + "integrity": "sha512-DEKragHT26kwUhXx9goYehQ/WxFzpVrMHIicYF+L7sLVmFyCwPwAslYZZBe/eTm/x++tKRHnun1lbcf1ZDccRg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-builders/common": "3.0.0-beta.0", + "@angular-builders/common": "3.0.0", "@angular-devkit/architect": ">=0.1900.0 < 0.2000.0", "@angular-devkit/core": "^19.0.0", "jest-preset-angular": "14.4.2", @@ -196,14 +202,42 @@ "jest": ">=29" } }, - "node_modules/@angular-devkit/architect": { - "version": "0.1900.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1900.4.tgz", - "integrity": "sha512-9XwZ21BPYS2vGOOwVB40fsMyuwJT0H1lWaAMo8Umwi6XbKBVfaWbEhjtR9dlarrySKtFuTz9hmTZkIXHLjXPdA==", + "node_modules/@angular-builders/jest/node_modules/jest-preset-angular": { + "version": "14.4.2", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.4.2.tgz", + "integrity": "sha512-BYYv0FaTDfBNh8WyA9mpOV3krfw20kurBGK8INZUnv7KZDAWZuQtCET4TwTWxSNQ9jS1OX1+a5weCm/bTDDM1A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.0.4", + "bs-logger": "^0.2.6", + "esbuild-wasm": ">=0.15.13", + "jest-environment-jsdom": "^29.0.0", + "jest-util": "^29.0.0", + "pretty-format": "^29.0.0", + "ts-jest": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0" + }, + "optionalDependencies": { + "esbuild": ">=0.15.13" + }, + "peerDependencies": { + "@angular/compiler-cli": ">=15.0.0 <20.0.0", + "@angular/core": ">=15.0.0 <20.0.0", + "@angular/platform-browser-dynamic": ">=15.0.0 <20.0.0", + "jest": "^29.0.0", + "typescript": ">=4.8" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1901.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.5.tgz", + "integrity": "sha512-zlRudZx34FkFZnSdaQCjxDleHwbQYNLdBFcLi+FBwt0UXqxmhbEIasK3l/3kCOC3QledrjUzVXgouji+OZ/WGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.1.5", "rxjs": "7.8.1" }, "engines": { @@ -213,19 +247,19 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.0.4.tgz", - "integrity": "sha512-n7fcRdNB7ed5j6aZI+qPI/1LylFv1OiRNgBIeJxX3HEmzQxsHHLcxWog2yZK2Fvw3390xFx/VjZaklITj6tBFA==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.1.5.tgz", + "integrity": "sha512-ny7ktNOTxaEi6cS3V6XFP5bbJkgiMt3OUNUYLdfdbv4y6wolVlPVHKl+wb4xs6tgbnmx63+e6zGpoDMCRytgcg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1900.4", - "@angular-devkit/build-webpack": "0.1900.4", - "@angular-devkit/core": "19.0.4", - "@angular/build": "19.0.4", + "@angular-devkit/architect": "0.1901.5", + "@angular-devkit/build-webpack": "0.1901.5", + "@angular-devkit/core": "19.1.5", + "@angular/build": "19.1.5", "@babel/core": "7.26.0", - "@babel/generator": "7.26.2", + "@babel/generator": "7.26.3", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", "@babel/plugin-transform-async-generator-functions": "7.25.9", @@ -234,21 +268,21 @@ "@babel/preset-env": "7.26.0", "@babel/runtime": "7.26.0", "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "19.0.4", - "@vitejs/plugin-basic-ssl": "1.1.0", + "@ngtools/webpack": "19.1.5", + "@vitejs/plugin-basic-ssl": "1.2.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "browserslist": "^4.21.5", "copy-webpack-plugin": "12.0.2", "css-loader": "7.1.2", - "esbuild-wasm": "0.24.0", - "fast-glob": "3.3.2", + "esbuild-wasm": "0.24.2", + "fast-glob": "3.3.3", "http-proxy-middleware": "3.0.3", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", "karma-source-map-support": "1.4.0", - "less": "4.2.0", + "less": "4.2.1", "less-loader": "12.2.0", "license-webpack-plugin": "4.0.2", "loader-utils": "3.3.1", @@ -256,22 +290,22 @@ "open": "10.1.0", "ora": "5.4.1", "picomatch": "4.0.2", - "piscina": "4.7.0", + "piscina": "4.8.0", "postcss": "8.4.49", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.80.7", - "sass-loader": "16.0.3", + "sass": "1.83.1", + "sass-loader": "16.0.4", "semver": "7.6.3", "source-map-loader": "5.0.0", "source-map-support": "0.5.21", - "terser": "5.36.0", + "terser": "5.37.0", "tree-kill": "1.2.2", "tslib": "2.8.1", - "webpack": "5.96.1", + "webpack": "5.97.1", "webpack-dev-middleware": "7.4.2", - "webpack-dev-server": "5.1.0", + "webpack-dev-server": "5.2.0", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" }, @@ -281,14 +315,14 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.24.0" + "esbuild": "0.24.2" }, "peerDependencies": { "@angular/compiler-cli": "^19.0.0", "@angular/localize": "^19.0.0", "@angular/platform-server": "^19.0.0", "@angular/service-worker": "^19.0.0", - "@angular/ssr": "^19.0.4", + "@angular/ssr": "^19.1.5", "@web/test-runner": "^0.19.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", @@ -296,8 +330,8 @@ "karma": "^6.3.0", "ng-packagr": "^19.0.0", "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.5 <5.7" + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.8" }, "peerDependenciesMeta": { "@angular/localize": { @@ -338,27 +372,92 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", - "cpu": [ - "ppc64" - ], + "node_modules/@angular-devkit/build-angular/node_modules/@angular/build": { + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.1.5.tgz", + "integrity": "sha512-byoHcv0/s6WGWap59s43N/eC+4NsviuTnGoj+iR0ayubk8snn6jdkZLbFDfnTuQlTiu4ok8/XcksjzeMkgGyyw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1901.5", + "@angular-devkit/core": "19.1.5", + "@babel/core": "7.26.0", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.1.1", + "@vitejs/plugin-basic-ssl": "1.2.0", + "beasties": "0.2.0", + "browserslist": "^4.23.0", + "esbuild": "0.24.2", + "fast-glob": "3.3.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.17", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "rollup": "4.30.1", + "sass": "1.83.1", + "semver": "7.6.3", + "vite": "6.0.11", + "watchpack": "2.4.2" + }, "engines": { - "node": ">=18" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.2.2" + }, + "peerDependencies": { + "@angular/compiler": "^19.0.0", + "@angular/compiler-cli": "^19.0.0", + "@angular/localize": "^19.0.0", + "@angular/platform-server": "^19.0.0", + "@angular/service-worker": "^19.0.0", + "@angular/ssr": "^19.1.5", + "less": "^4.2.0", + "ng-packagr": "^19.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.8" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", + "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", "cpu": [ "arm" ], @@ -367,15 +466,12 @@ "optional": true, "os": [ "android" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", + "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", "cpu": [ "arm64" ], @@ -384,32 +480,12 @@ "optional": true, "os": [ "android" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", - "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", + "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", "cpu": [ "arm64" ], @@ -418,15 +494,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", + "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", "cpu": [ "x64" ], @@ -435,15 +508,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", + "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", "cpu": [ "arm64" ], @@ -452,15 +522,12 @@ "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", + "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", "cpu": [ "x64" ], @@ -469,15 +536,12 @@ "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", + "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", "cpu": [ "arm" ], @@ -486,15 +550,26 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", + "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", + "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", "cpu": [ "arm64" ], @@ -503,32 +578,26 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", + "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", + "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", "cpu": [ "loong64" ], @@ -537,32 +606,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", + "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", "cpu": [ "ppc64" ], @@ -571,15 +620,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", + "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", "cpu": [ "riscv64" ], @@ -588,15 +634,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", + "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", "cpu": [ "s390x" ], @@ -605,15 +648,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", + "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", "cpu": [ "x64" ], @@ -622,15 +662,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", + "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", "cpu": [ "x64" ], @@ -638,50 +675,13 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", + "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", "cpu": [ "arm64" ], @@ -690,15 +690,12 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", + "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", "cpu": [ "ia32" ], @@ -707,15 +704,12 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", + "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", "cpu": [ "x64" ], @@ -724,57 +718,14 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", - "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.0", - "@esbuild/android-arm": "0.24.0", - "@esbuild/android-arm64": "0.24.0", - "@esbuild/android-x64": "0.24.0", - "@esbuild/darwin-arm64": "0.24.0", - "@esbuild/darwin-x64": "0.24.0", - "@esbuild/freebsd-arm64": "0.24.0", - "@esbuild/freebsd-x64": "0.24.0", - "@esbuild/linux-arm": "0.24.0", - "@esbuild/linux-arm64": "0.24.0", - "@esbuild/linux-ia32": "0.24.0", - "@esbuild/linux-loong64": "0.24.0", - "@esbuild/linux-mips64el": "0.24.0", - "@esbuild/linux-ppc64": "0.24.0", - "@esbuild/linux-riscv64": "0.24.0", - "@esbuild/linux-s390x": "0.24.0", - "@esbuild/linux-x64": "0.24.0", - "@esbuild/netbsd-x64": "0.24.0", - "@esbuild/openbsd-arm64": "0.24.0", - "@esbuild/openbsd-x64": "0.24.0", - "@esbuild/sunos-x64": "0.24.0", - "@esbuild/win32-arm64": "0.24.0", - "@esbuild/win32-ia32": "0.24.0", - "@esbuild/win32-x64": "0.24.0" - } + ] }, "node_modules/@angular-devkit/build-angular/node_modules/istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -786,14 +737,53 @@ "node": ">=10" } }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1900.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1900.4.tgz", - "integrity": "sha512-eovr5Am8EwxF7d/y0Hbfz/KYWnOXXVXVwquPUcg8JBI19lLbfctz4+71Vjz2qGroijr2FlZztRpmhd498SLt/A==", + "node_modules/@angular-devkit/build-angular/node_modules/rollup": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", + "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1900.4", + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.30.1", + "@rollup/rollup-android-arm64": "4.30.1", + "@rollup/rollup-darwin-arm64": "4.30.1", + "@rollup/rollup-darwin-x64": "4.30.1", + "@rollup/rollup-freebsd-arm64": "4.30.1", + "@rollup/rollup-freebsd-x64": "4.30.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", + "@rollup/rollup-linux-arm-musleabihf": "4.30.1", + "@rollup/rollup-linux-arm64-gnu": "4.30.1", + "@rollup/rollup-linux-arm64-musl": "4.30.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", + "@rollup/rollup-linux-riscv64-gnu": "4.30.1", + "@rollup/rollup-linux-s390x-gnu": "4.30.1", + "@rollup/rollup-linux-x64-gnu": "4.30.1", + "@rollup/rollup-linux-x64-musl": "4.30.1", + "@rollup/rollup-win32-arm64-msvc": "4.30.1", + "@rollup/rollup-win32-ia32-msvc": "4.30.1", + "@rollup/rollup-win32-x64-msvc": "4.30.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1901.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1901.5.tgz", + "integrity": "sha512-UxEoF7F8L1GpH/N4me7VGe5ZPfxIiVHyhw5/ck3rcVbT6YD22/GYFGSJRGYP+D7LLTJ7OOQvfD6Bc/q62HhWvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1901.5", "rxjs": "7.8.1" }, "engines": { @@ -807,9 +797,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.0.4.tgz", - "integrity": "sha512-+imxIj1JLr2hbUYQePHgkTUKr0VmlxNSZvIREcCWtXUcdCypiwhJAtGXv6MfpB4hAx+FJZYEpVWeLwYOS/gW0A==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.5.tgz", + "integrity": "sha512-wGKV+i5mCM/Hd/3CsdrIYcVi5G2Wg/D5941bUDXivrbsqHfKVINxAkI3OI1eaD90VnAL8ICrQEoAhh6ni2Umkg==", "dev": true, "license": "MIT", "dependencies": { @@ -852,15 +842,15 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.0.4.tgz", - "integrity": "sha512-2r6Qs4N5NSPho+qzegCYS8kIgylXyH4DHaS7HJ5+4XvM1I8V8AII8payLWkUK0i29XufVoD5XfPUFnjxZrBfYQ==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.5.tgz", + "integrity": "sha512-8QjOlO2CktcTT0TWcaABea2xSePxoPKaZu96+6gc8oZzj/y8DbdGiO9mRvIac9+m4hiZI41Cqm1W+yMsCzYMkA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.0.4", + "@angular-devkit/core": "19.1.5", "jsonc-parser": "3.3.1", - "magic-string": "0.30.12", + "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -871,9 +861,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.0.0.tgz", - "integrity": "sha512-vi68ADoEKrg2SB87jwUCaVhOhWPpXyG6X8QJzg8AiYDCQY721x1l6Pdz6WZOPruWALyoIyFGFXqtuysDGqIBhw==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.0.2.tgz", + "integrity": "sha512-BdmMSndQt2fSBiTVniskUcUpQaeweUapbsL0IDfQ7a13vL0NVXpc3K89YXuVE/xsb08uHtqphuwxPAAj6kX3OA==", "dev": true, "license": "MIT", "dependencies": { @@ -886,21 +876,21 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.0.0.tgz", - "integrity": "sha512-q6IaiqKYcmBW/gw55tytDucguo5E48szVCLNLHUFdN98YDDsP+KM3MPWYPyZcXpusmFfIjLdr8d41PlKmyMUpg==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.0.2.tgz", + "integrity": "sha512-HPmp92r70SNO/0NdIaIhxrgVSpomqryuUk7jszvNRtu+OzYCJGcbLhQD38T3dbBWT/AV0QXzyzExn6/2ai9fEw==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.0.0.tgz", - "integrity": "sha512-WkUnH5zmvC/cH6f8BGiRK+KebrKdGbQmhtu3IHLEyzG9U4mBiIV8XkSzhdkY3RCN8bKqhmE5C3oNBLNCtvg4QQ==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.0.2.tgz", + "integrity": "sha512-DLuNVVGGFicSThOcMSJyNje+FZSPdG0B3lCBRiqcgKH/16kfM4pV8MobPM7RGK2NhaOmmZ4zzJNwpwWPSgi+Lw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.0.0", - "@angular-eslint/utils": "19.0.0" + "@angular-eslint/bundled-angular-compiler": "19.0.2", + "@angular-eslint/utils": "19.0.2" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -909,14 +899,14 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.0.0.tgz", - "integrity": "sha512-d2NzuAyvFo00QGBv6BLno0KZ3Ptd+UNVHpI9vwU0giaZcjVsdKbcMvMfynkvHAAwVIVw5aSLwabIjnm0rc3x3A==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.0.2.tgz", + "integrity": "sha512-f/OCF9ThnxQ8m0eNYPwnCrySQPhYfCOF6STL7F9LnS8Bs3ZeW3/oT1yLaMIZ1Eg0ogIkgxksMAJZjrJPUPBD1Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.0.0", - "@angular-eslint/utils": "19.0.0", + "@angular-eslint/bundled-angular-compiler": "19.0.2", + "@angular-eslint/utils": "19.0.2", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, @@ -927,17 +917,47 @@ "typescript": "*" } }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@angular-eslint/utils": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.0.2.tgz", + "integrity": "sha512-HotBT8OKr7zCaX1S9k27JuhRiTVIbbYVl6whlb3uwdMIPIWY8iOcEh1tjI4qDPUafpLfR72Dhwi5bO1E17F3/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.0.2" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@angular-eslint/utils": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.0.2.tgz", + "integrity": "sha512-HotBT8OKr7zCaX1S9k27JuhRiTVIbbYVl6whlb3uwdMIPIWY8iOcEh1tjI4qDPUafpLfR72Dhwi5bO1E17F3/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.0.2" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, "node_modules/@angular-eslint/schematics": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.0.0.tgz", - "integrity": "sha512-fle4SMxjI+91y5eR6hVG7yhzJHAw87LudHw918hGUVn2INIAW1TTuuQNoah8kNg9I6ICIDat26IenD4nOau6Gg==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.0.2.tgz", + "integrity": "sha512-wI4SyiAnUCrpigtK6PHRlVWMC9vWljqmlLhbsJV5O5yDajlmRdvgXvSHDefhJm0hSfvZYRXuiAARYv2+QVfnGA==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 19.0.0 < 20.0.0", "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", - "@angular-eslint/eslint-plugin": "19.0.0", - "@angular-eslint/eslint-plugin-template": "19.0.0", + "@angular-eslint/eslint-plugin": "19.0.2", + "@angular-eslint/eslint-plugin-template": "19.0.2", "ignore": "6.0.2", "semver": "7.6.3", "strip-json-comments": "3.1.1" @@ -954,13 +974,13 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.0.0.tgz", - "integrity": "sha512-bOLMNBQbrLMujGWSda0SF8ka7snQ9Uzxie1dr5LquI104p2J4Wt90DOoaWzhNaBBwedt3WXmhSHmvvR9720kHA==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.0.2.tgz", + "integrity": "sha512-z3rZd2sBfuYcFf9rGDsB2zz2fbGX8kkF+0ftg9eocyQmzWrlZHFmuw9ha7oP/Mz8gpblyCS/aa1U/Srs6gz0UQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.0.0", + "@angular-eslint/bundled-angular-compiler": "19.0.2", "eslint-scope": "^8.0.2" }, "peerDependencies": { @@ -968,550 +988,10 @@ "typescript": "*" } }, - "node_modules/@angular-eslint/utils": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.0.0.tgz", - "integrity": "sha512-PH40BmIcIr5ldr08XYnqJ8cTzJfScJjBym4SECsilBnz5fhCdTD7UEQiW4d0P78Ie8H5PxvOJx9ZE+L4WBNrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.0.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular/build": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.0.4.tgz", - "integrity": "sha512-ubsNjLb54VkZwcPQ21Ke8aAHiIrRIcv7gG3R6/6XOoWeK1K2+tsv8bnO4mz5cHgzWOspLOT7FDC83NJjrKX3Nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1900.4", - "@babel/core": "7.26.0", - "@babel/helper-annotate-as-pure": "7.25.9", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-syntax-import-attributes": "7.26.0", - "@inquirer/confirm": "5.0.2", - "@vitejs/plugin-basic-ssl": "1.1.0", - "beasties": "0.1.0", - "browserslist": "^4.23.0", - "esbuild": "0.24.0", - "fast-glob": "3.3.2", - "https-proxy-agent": "7.0.5", - "istanbul-lib-instrument": "6.0.3", - "listr2": "8.2.5", - "magic-string": "0.30.12", - "mrmime": "2.0.0", - "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.2", - "piscina": "4.7.0", - "rollup": "4.26.0", - "sass": "1.80.7", - "semver": "7.6.3", - "vite": "5.4.11", - "watchpack": "2.4.2" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "lmdb": "3.1.5" - }, - "peerDependencies": { - "@angular/compiler": "^19.0.0", - "@angular/compiler-cli": "^19.0.0", - "@angular/localize": "^19.0.0", - "@angular/platform-server": "^19.0.0", - "@angular/service-worker": "^19.0.0", - "@angular/ssr": "^19.0.4", - "less": "^4.2.0", - "postcss": "^8.4.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.5 <5.7" - }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tailwindcss": { - "optional": true - } - } - }, - "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", - "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular/build/node_modules/esbuild": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", - "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.0", - "@esbuild/android-arm": "0.24.0", - "@esbuild/android-arm64": "0.24.0", - "@esbuild/android-x64": "0.24.0", - "@esbuild/darwin-arm64": "0.24.0", - "@esbuild/darwin-x64": "0.24.0", - "@esbuild/freebsd-arm64": "0.24.0", - "@esbuild/freebsd-x64": "0.24.0", - "@esbuild/linux-arm": "0.24.0", - "@esbuild/linux-arm64": "0.24.0", - "@esbuild/linux-ia32": "0.24.0", - "@esbuild/linux-loong64": "0.24.0", - "@esbuild/linux-mips64el": "0.24.0", - "@esbuild/linux-ppc64": "0.24.0", - "@esbuild/linux-riscv64": "0.24.0", - "@esbuild/linux-s390x": "0.24.0", - "@esbuild/linux-x64": "0.24.0", - "@esbuild/netbsd-x64": "0.24.0", - "@esbuild/openbsd-arm64": "0.24.0", - "@esbuild/openbsd-x64": "0.24.0", - "@esbuild/sunos-x64": "0.24.0", - "@esbuild/win32-arm64": "0.24.0", - "@esbuild/win32-ia32": "0.24.0", - "@esbuild/win32-x64": "0.24.0" - } - }, - "node_modules/@angular/build/node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular/cdk": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.0.2.tgz", - "integrity": "sha512-eDjHJJWpgnzC3pR6N0gCdh51Q1ffoh6mql06YSqprj005aNKBjmCMnpU4bPPzdGSkKsjwAZWGUNWg4RS+R+iZQ==", + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.1.2.tgz", + "integrity": "sha512-rzrZ4BkGNIZWSdw0OsuSB/H9UB5ppPvmBq+uRHdYmZoYjo5wu1pmePxAIZDIBR8xdaNy9rZ4ecS6IebDkgYPrg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1526,26 +1006,26 @@ } }, "node_modules/@angular/cli": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.0.4.tgz", - "integrity": "sha512-jxnD9qkhelcRMCrHDCxNsWgn6HQCvMIj8uI0T2eB9Vy93q2YWUo/fWl2Sy4gFlR+VNeF+1hYhPLb/vqLLzjWuA==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.5.tgz", + "integrity": "sha512-bedjH3jUcrLgN3GOTTuvjbPcY3Lm0YcYBVY35S1ugI88UK6nbtttiRdgK++Qk2Q8wbg6zuaBAr4ACbfPMsnRaA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1900.4", - "@angular-devkit/core": "19.0.4", - "@angular-devkit/schematics": "19.0.4", - "@inquirer/prompts": "7.1.0", + "@angular-devkit/architect": "0.1901.5", + "@angular-devkit/core": "19.1.5", + "@angular-devkit/schematics": "19.1.5", + "@inquirer/prompts": "7.2.1", "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.0.4", + "@schematics/angular": "19.1.5", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", "listr2": "8.2.5", - "npm-package-arg": "12.0.0", + "npm-package-arg": "12.0.1", "npm-pick-manifest": "10.0.0", "pacote": "20.0.0", - "resolve": "1.22.8", + "resolve": "1.22.10", "semver": "7.6.3", "symbol-observable": "4.0.0", "yargs": "17.7.2" @@ -1560,9 +1040,9 @@ } }, "node_modules/@angular/common": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.0.3.tgz", - "integrity": "sha512-YyBVZU+LQ38R+/U5vF/b1T3muROKpR0kkupMw7VKnGhQfgrRX5Dk3H2nr9ritt0zPc7TOUuQSlHMf3QWah2GDg==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.1.4.tgz", + "integrity": "sha512-E4MCl13VIotOxmzKQ/UGciPeaRXQgH7ymesEjYVGcT8jmC+qz5dEcoN7L5Jvq9aUsmLBt9MFp/B5QqKCIXMqYA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1571,14 +1051,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.0.3", + "@angular/core": "19.1.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.0.3.tgz", - "integrity": "sha512-cxtK4SlHAPstcXfjwOaoR1dAszrzo2iDF8ZiihbZPgKUG3m27qIU3Lp5XBgxfZPlO4jh6TXkWznY7f6Tyxkb0Q==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.1.4.tgz", + "integrity": "sha512-9vGUZ+QhGWvf5dfeILybrh5rvZQtNqS8WumMeX2/vCb0JTA0N4DsL1Sy47HuWcgKBxbmHVUdF5/iufcFaqk2FA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1587,7 +1067,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.0.3" + "@angular/core": "19.1.4" }, "peerDependenciesMeta": { "@angular/core": { @@ -1596,9 +1076,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.0.3.tgz", - "integrity": "sha512-nayLcC3hSHoGKXCZInMdFcIZJEHYkEGNsdAutgCMuSj+lXCGuRUysuGC0rGzJc2R6nhgfaLJnO8T/O5acqaqdA==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.1.4.tgz", + "integrity": "sha512-ozJvTUzPOgFqlz69YnV14Ncod+iH0cXZvUKerjw8o+JsixLG2LmJpwQ79Gh4a/ZQmAkAxMAYYK5izCiio8MmTg==", "license": "MIT", "dependencies": { "@babel/core": "7.26.0", @@ -1619,14 +1099,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.0.3", - "typescript": ">=5.5 <5.7" + "@angular/compiler": "19.1.4", + "typescript": ">=5.5 <5.8" } }, "node_modules/@angular/core": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.0.3.tgz", - "integrity": "sha512-WM844gDzrbHtcM2TJB9DmfCmenUYyNSI6h924CeppDW5oG8ShinQGiWNjF5oI6EZ4tG60uK3QvCm3kjr1dmbOA==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.1.4.tgz", + "integrity": "sha512-r3T81lM9evmuW36HA3VAxIJ61M8kirGR8yHoln9fXSnYG8UeJ7JlWEbVRHmVHKOB48VK0bS/VxqN+w9TOq3bZg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1640,9 +1120,9 @@ } }, "node_modules/@angular/forms": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.0.3.tgz", - "integrity": "sha512-8wf8yDR6cW+lOhpzhmxUOiI5Wjr1Kf7o8NuJ2P5K6b7IMNRzRyR5q/6R4NUwtF6aaJ1wNqmSof+goQmtn1HOcw==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.1.4.tgz", + "integrity": "sha512-dcf4G+vXrfvy5NAP+C4A2rBeaZuwKs/TeWjZDpkRUPQMwTvDJcSNH+pqOeVsYUGNY2BkY1uPjzmgZh4F5NMQ9A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1651,21 +1131,21 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.0.3", - "@angular/core": "19.0.3", - "@angular/platform-browser": "19.0.3", + "@angular/common": "19.1.4", + "@angular/core": "19.1.4", + "@angular/platform-browser": "19.1.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/localize": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.0.3.tgz", - "integrity": "sha512-xou8bCPpIn0h6GJm6isiV0qkzi7C/fnF5fC4ueiN/Bp6fOuRNdwTSwaTWz4RoWvgwbQs5eZ6yIKUb+9toUAOPw==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-19.1.4.tgz", + "integrity": "sha512-AFfaxnGUWl1QZGmhYNTH8adWynSqjNwHweOUQ/ItIQ+MkbIPOpAtZp+ar6SRJZpatR59O8797jPKVFTAebLvLQ==", "license": "MIT", "dependencies": { "@babel/core": "7.26.0", "@types/babel__core": "7.20.5", - "fast-glob": "3.3.2", + "fast-glob": "3.3.3", "yargs": "^17.2.1" }, "bin": { @@ -1677,14 +1157,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.0.3", - "@angular/compiler-cli": "19.0.3" + "@angular/compiler": "19.1.4", + "@angular/compiler-cli": "19.1.4" } }, "node_modules/@angular/platform-browser": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.0.3.tgz", - "integrity": "sha512-vggWHSzOsCpYqnGq5IIN+n7xdEvXfgUGaMdgzPhFMTsnlMTUs5+VEFl9tX9FANHkXKB5S1RttVyvEXRqJM9ncQ==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.4.tgz", + "integrity": "sha512-IoVIvemj7ni6GLDCvwtZhTgMQjPyG+xPW7rASN2RVl9T3uS1fJUpXrh5JzBcCikIj20O2KV9mqt7p4iIXy9jbQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1693,9 +1173,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "19.0.3", - "@angular/common": "19.0.3", - "@angular/core": "19.0.3" + "@angular/animations": "19.1.4", + "@angular/common": "19.1.4", + "@angular/core": "19.1.4" }, "peerDependenciesMeta": { "@angular/animations": { @@ -1704,9 +1184,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.0.3.tgz", - "integrity": "sha512-gFh+QN7JvepnD3mS0XmOtDmfY8h5sSkk2/guesE2A68Na8q+M3fGZlz7I37tCXToLth5us1X0Gi0UPCSESc4SA==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.1.4.tgz", + "integrity": "sha512-r1AM8qkjl63cg46tgOHsVV4URHDctcVpt98DU/d/yN8JAugrx6GA1qOM/HMDspMjEIU4aYcSkUUY6h6uIkYmOQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1715,16 +1195,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.0.3", - "@angular/compiler": "19.0.3", - "@angular/core": "19.0.3", - "@angular/platform-browser": "19.0.3" + "@angular/common": "19.1.4", + "@angular/compiler": "19.1.4", + "@angular/core": "19.1.4", + "@angular/platform-browser": "19.1.4" } }, "node_modules/@angular/router": { - "version": "19.0.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.0.3.tgz", - "integrity": "sha512-L/s8crRC6nj5knmHsnPeOXMNdC7vUOSOvTQonXhmT0FdlP9bPnnRrNeVDnLnd8AzjPSBfIFE2eQw6T8jCwdxMA==", + "version": "19.1.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.1.4.tgz", + "integrity": "sha512-0gEhGGqcCS7adKuv/XeQjRbhEqRXPhIH4ygjwfonV+uvmK+C1sf+bnAt4o01hxwf12w4FcnNPkgBKt+rJJ+LpA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1733,9 +1213,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.0.3", - "@angular/core": "19.0.3", - "@angular/platform-browser": "19.0.3", + "@angular/common": "19.1.4", + "@angular/core": "19.1.4", + "@angular/platform-browser": "19.1.4", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1804,13 +1284,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -1988,10 +1468,11 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -2015,15 +1496,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/traverse": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -2112,11 +1593,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -2501,13 +1983,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -2921,13 +2403,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -3234,13 +2716,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", - "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", + "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -3452,15 +2934,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", + "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -3468,10 +2951,27 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -3487,13 +2987,15 @@ "dev": true }, "node_modules/@codecov/bundler-plugin-core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.2.1.tgz", - "integrity": "sha512-9Iqr+PAQ/QVEnvKr56W/Jo8IaWJDFQkbcffNgG8Oc5GIQ1NRWsPmL/jxhkGrnp2LMJnpG2gPtV3+fet1jy6gbA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@codecov/bundler-plugin-core/-/bundler-plugin-core-1.8.0.tgz", + "integrity": "sha512-D1aeA8u3RHOkQVLImLHxW6zFdUrw5wgoeDzYrKZeDExGp5ePs4RpJxKwklg1N0e1JxRcAgKj+zZo/y3Q4nV+sA==", "dev": true, + "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.0", + "@sentry/core": "^8.42.0", "chalk": "4.1.2", "semver": "^7.5.4", "unplugin": "^1.10.1", @@ -3504,12 +3006,13 @@ } }, "node_modules/@codecov/webpack-plugin": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@codecov/webpack-plugin/-/webpack-plugin-1.2.1.tgz", - "integrity": "sha512-puA2Zb9A6e5sZ3AGpoYyymC64qgp0uUzDjnEa3EWtD/bQKa+m2HDu71sXUyJU/QFrWWa8xNaju4SPNVJU0m5EQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@codecov/webpack-plugin/-/webpack-plugin-1.8.0.tgz", + "integrity": "sha512-G62kbpAFuutQIrbuw97MAf5vkgIKXf64Nj0pRe6OPbC5Aps0nWrEL9v0Sr0WPdHcbQnr3i6noIXyUDdV1lUZ5g==", "dev": true, + "license": "MIT", "dependencies": { - "@codecov/bundler-plugin-core": "^1.2.1", + "@codecov/bundler-plugin-core": "^1.8.0", "unplugin": "^1.10.1" }, "engines": { @@ -3560,297 +3063,332 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", "cpu": [ - "x64" + "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", - "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", "cpu": [ "arm64" ], @@ -3865,83 +3403,88 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -3969,12 +3512,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -3983,19 +3527,24 @@ } }, "node_modules/@eslint/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", - "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -4019,6 +3568,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4034,13 +3584,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -4053,6 +3605,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4064,32 +3617,37 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", - "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", + "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", - "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { @@ -4101,6 +3659,7 @@ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" } @@ -4154,10 +3713,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz", - "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -4167,15 +3727,15 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.2.tgz", - "integrity": "sha512-+gznPl8ip8P8HYHYecDtUtdsh1t2jvb+sWCD72GAiZ9m45RqwrLmReDaqdC0umQfamtFXVRoMVJ2/qINKGm9Tg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.7.tgz", + "integrity": "sha512-lyoF4uYdBBTnqeB1gjPdYkiQ++fz/iYKaP9DON1ZGlldkvAEJsjaOBRdbl5UW1pOSslBRd701jxhAG0MlhHd2w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/figures": "^1.0.8", - "@inquirer/type": "^3.0.1", + "@inquirer/core": "^10.1.5", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -4187,14 +3747,14 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.2.tgz", - "integrity": "sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.1.tgz", + "integrity": "sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1" + "@inquirer/core": "^10.1.2", + "@inquirer/type": "^3.0.2" }, "engines": { "node": ">=18" @@ -4204,19 +3764,18 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.0.tgz", - "integrity": "sha512-I+ETk2AL+yAVbvuKx5AJpQmoaWhpiTFOg/UJb7ZkMAK4blmtG8ATh5ct+T/8xNld0CZG/2UhtkdMwpgvld92XQ==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.5.tgz", + "integrity": "sha512-/vyCWhET0ktav/mUeBqJRYTwmjFPIKPRYb3COAw7qORULgipGSUO2vL32lQKki3UxDKJ8BvuEbokaoyCA6YlWw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.8", - "@inquirer/type": "^3.0.1", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.3", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, @@ -4238,14 +3797,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.1.0.tgz", - "integrity": "sha512-K1gGWsxEqO23tVdp5MT3H799OZ4ER1za7Dlc8F4um0W7lwSv0KGR/YyrUEyimj0g7dXZd8XknM/5QA2/Uy+TbA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.4.tgz", + "integrity": "sha512-S8b6+K9PLzxiFGGc02m4syhEu5JsH0BukzRsuZ+tpjJ5aDsDX1WfNfOil2fmsO36Y1RMcpJGxlfQ1yh4WfU28Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1", + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3", "external-editor": "^3.1.0" }, "engines": { @@ -4256,14 +3815,14 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.2.tgz", - "integrity": "sha512-WdgCX1cUtinz+syKyZdJomovULYlKUWZbVYZzhf+ZeeYf4htAQ3jLymoNs3koIAKfZZl3HUBb819ClCBfyznaw==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.7.tgz", + "integrity": "sha512-PsUQ5t7r+DPjW0VVEHzssOTBM2UPHnvBNse7hzuki7f6ekRL94drjjfBLrGEDe7cgj3pguufy/cuFwMeWUWHXw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1", + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -4274,9 +3833,9 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", - "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.10.tgz", + "integrity": "sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==", "dev": true, "license": "MIT", "engines": { @@ -4284,14 +3843,14 @@ } }, "node_modules/@inquirer/input": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.0.2.tgz", - "integrity": "sha512-yCLCraigU085EcdpIVEDgyfGv4vBiE4I+k1qRkc9C5dMjWF42ADMGy1RFU94+eZlz4YlkmFsiyHZy0W1wdhaNg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.4.tgz", + "integrity": "sha512-CKKF8otRBdIaVnRxkFLs00VNA9HWlEh3x4SqUfC3A8819TeOZpTYG/p+4Nqu3hh97G+A0lxkOZNYE7KISgU8BA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1" + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3" }, "engines": { "node": ">=18" @@ -4301,14 +3860,14 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.2.tgz", - "integrity": "sha512-MKQhYofdUNk7eqJtz52KvM1dH6R93OMrqHduXCvuefKrsiMjHiMwjc3NZw5Imm2nqY7gWd9xdhYrtcHMJQZUxA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.7.tgz", + "integrity": "sha512-uU2nmXGC0kD8+BLgwZqcgBD1jcw2XFww2GmtP6b4504DkOp+fFAhydt7JzRR1TAI2dmj175p4SZB0lxVssNreA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1" + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3" }, "engines": { "node": ">=18" @@ -4318,14 +3877,14 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.2.tgz", - "integrity": "sha512-tQXGSu7IO07gsYlGy3VgXRVsbOWqFBMbqAUrJSc1PDTQQ5Qdm+QVwkP0OC0jnUZ62D19iPgXOMO+tnWG+HhjNQ==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.7.tgz", + "integrity": "sha512-DFpqWLx+C5GV5zeFWuxwDYaeYnTWYphO07pQ2VnP403RIqRIpwBG0ATWf7pF+3IDbaXEtWatCJWxyDrJ+rkj2A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1", + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3", "ansi-escapes": "^4.3.2" }, "engines": { @@ -4336,22 +3895,22 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.1.0.tgz", - "integrity": "sha512-5U/XiVRH2pp1X6gpNAjWOglMf38/Ys522ncEHIKT1voRUvSj/DQnR22OVxHnwu5S+rCFaUiPQ57JOtMFQayqYA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.2.1.tgz", + "integrity": "sha512-v2JSGri6/HXSfoGIwuKEn8sNCQK6nsB2BNpy2lSX6QH9bsECrMv93QHnj5+f+1ZWpF/VNioIV2B/PDox8EvGuQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.0.2", - "@inquirer/confirm": "^5.0.2", - "@inquirer/editor": "^4.1.0", - "@inquirer/expand": "^4.0.2", - "@inquirer/input": "^4.0.2", - "@inquirer/number": "^3.0.2", - "@inquirer/password": "^4.0.2", - "@inquirer/rawlist": "^4.0.2", - "@inquirer/search": "^3.0.2", - "@inquirer/select": "^4.0.2" + "@inquirer/checkbox": "^4.0.4", + "@inquirer/confirm": "^5.1.1", + "@inquirer/editor": "^4.2.1", + "@inquirer/expand": "^4.0.4", + "@inquirer/input": "^4.1.1", + "@inquirer/number": "^3.0.4", + "@inquirer/password": "^4.0.4", + "@inquirer/rawlist": "^4.0.4", + "@inquirer/search": "^3.0.4", + "@inquirer/select": "^4.0.4" }, "engines": { "node": ">=18" @@ -4361,14 +3920,14 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.2.tgz", - "integrity": "sha512-3XGcskMoVF8H0Dl1S5TSZ3rMPPBWXRcM0VeNVsS4ByWeWjSeb0lPqfnBg6N7T0608I1B2bSVnbi2cwCrmOD1Yw==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.7.tgz", + "integrity": "sha512-ZeBca+JCCtEIwQMvhuROT6rgFQWWvAImdQmIIP3XoyDFjrp2E0gZlEn65sWIoR6pP2EatYK96pvx0887OATWQQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1", + "@inquirer/core": "^10.1.5", + "@inquirer/type": "^3.0.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -4379,15 +3938,15 @@ } }, "node_modules/@inquirer/search": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.2.tgz", - "integrity": "sha512-Zv4FC7w4dJ13BOJfKRQCICQfShinGjb1bCEIHxTSnjj2telu3+3RHwHubPG9HyD4aix5s+lyAMEK/wSFD75HLA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.7.tgz", + "integrity": "sha512-Krq925SDoLh9AWSNee8mbSIysgyWtcPnSAp5YtPBGCQ+OCO+5KGC8FwLpyxl8wZ2YAov/8Tp21stTRK/fw5SGg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/figures": "^1.0.8", - "@inquirer/type": "^3.0.1", + "@inquirer/core": "^10.1.5", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -4398,15 +3957,15 @@ } }, "node_modules/@inquirer/select": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.2.tgz", - "integrity": "sha512-uSWUzaSYAEj0hlzxa1mUB6VqrKaYx0QxGBLZzU4xWFxaSyGaXxsSE4OSOwdU24j0xl8OajgayqFXW0l2bkl2kg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.7.tgz", + "integrity": "sha512-ejGBMDSD+Iqk60u5t0Zf2UQhGlJWDM78Ep70XpNufIfc+f4VOTeybYKXu9pDjz87FkRzLiVsGpQG2SzuGlhaJw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/figures": "^1.0.8", - "@inquirer/type": "^3.0.1", + "@inquirer/core": "^10.1.5", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -4418,9 +3977,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.1.tgz", - "integrity": "sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.3.tgz", + "integrity": "sha512-I4VIHFxUuY1bshGbXZTxCmhwaaEst9s/lll3ekok+o1Z26/ZUKdx8y1b7lsoG6rtsBDwEGfiBJ2SfirjoISLpg==", "dev": true, "license": "MIT", "engines": { @@ -4942,9 +4501,9 @@ } }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz", - "integrity": "sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", + "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5028,9 +4587,9 @@ } }, "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.1.5.tgz", - "integrity": "sha512-ue5PSOzHMCIYrfvPP/MRS6hsKKLzqqhcdAvJCO8uFlDdj598EhgnacuOTuqA6uBK5rgiZXfDWyb7DVZSiBKxBA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.2.tgz", + "integrity": "sha512-WBSJT9Z7DTol5viq+DZD2TapeWOw7mlwXxiSBHgAzqVwsaVb0h/ekMD9iu/jDD8MUA20tO9N0WEdnT06fsUp+g==", "cpu": [ "arm64" ], @@ -5042,9 +4601,9 @@ ] }, "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.1.5.tgz", - "integrity": "sha512-CGhsb0R5vE6mMNCoSfxHFD8QTvBHM51gs4DBeigTYHWnYv2V5YpJkC4rMo5qAAFifuUcc0+a8a3SIU0c9NrfNw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.2.tgz", + "integrity": "sha512-4S13kUtR7c/j/MzkTIBJCXv52hQ41LG2ukeaqw4Eng9K0pNKLFjo1sDSz96/yKhwykxrWDb13ddJ/ZqD3rAhUA==", "cpu": [ "x64" ], @@ -5056,9 +4615,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.1.5.tgz", - "integrity": "sha512-3WeW328DN+xB5PZdhSWmqE+t3+44xWXEbqQ+caWJEZfOFdLp9yklBZEbVqVdqzznkoaXJYxTCp996KD6HmANeg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.2.tgz", + "integrity": "sha512-uW31JmfuPAaLUYW7NsEU8gzwgDAzpGPwjvkxnKlcWd8iDutoPKDJi8Wk9lFmPEZRxVSB0j1/wDQ7N2qliR9UFA==", "cpu": [ "arm" ], @@ -5070,9 +4629,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.1.5.tgz", - "integrity": "sha512-LAjaoOcBHGj6fiYB8ureiqPoph4eygbXu4vcOF+hsxiY74n8ilA7rJMmGUT0K0JOB5lmRQHSmor3mytRjS4qeQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.2.tgz", + "integrity": "sha512-4hdgZtWI1idQlWRp+eleWXD9KLvObgboRaVoBj2POdPEYvsKANllvMW0El8tEQwtw74yB9NT6P8ENBB5UJf5+g==", "cpu": [ "arm64" ], @@ -5084,9 +4643,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.1.5.tgz", - "integrity": "sha512-k/IklElP70qdCXOQixclSl2GPLFiopynGoKX1FqDd1/H0E3Fo1oPwjY2rEVu+0nS3AOw1sryStdXk8CW3cVIsw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.2.tgz", + "integrity": "sha512-A0zjf4a2vM4B4GAx78ncuOTZ8Ka1DbTaG1Axf1e00Sa7f5coqlWiLg1PX7Gxvyibc2YqtqB+8tg1KKrE8guZVw==", "cpu": [ "x64" ], @@ -5098,9 +4657,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.1.5.tgz", - "integrity": "sha512-KYar6W8nraZfSJspcK7Kp7hdj238X/FNauYbZyrqPBrtsXI1hvI4/KcRcRGP50aQoV7fkKDyJERlrQGMGTZUsA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.2.tgz", + "integrity": "sha512-Y0qoSCAja+xZE7QQ0LCHoYAuyI1n9ZqukQJa8lv9X3yCvWahFF7OYHAgVH1ejp43XWstj3U89/PAAzcowgF/uQ==", "cpu": [ "x64" ], @@ -5622,9 +5181,9 @@ } }, "node_modules/@ng-select/ng-select": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-14.1.0.tgz", - "integrity": "sha512-cE/e7WIqLAgUF83mpmDWbgmy7OvzWTjCTjtcIzhabRbhN0RDqp7u39noC12kSN+viAfYnA1TS7rBru+IouNt1g==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-14.2.0.tgz", + "integrity": "sha512-Dq3PgOb0EBL31TV1Byr6RnQW/Vd5cdaaCEFbII2tIwiIT1r15oMdeSEZqzutuslBqgyggnponYAaBEgQBVorAg==", "license": "MIT", "dependencies": { "tslib": "^2.3.1" @@ -5655,9 +5214,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.0.4.tgz", - "integrity": "sha512-N3WCbQz5ipdAZoSWHNf81RLET6+isq35+GZu9u0StpFtJCpXAmRRAv4vdMUYL7DLOzRmvEgwww6Rd5AwGeLFSw==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.1.5.tgz", + "integrity": "sha512-oIpE5Ci/Gl2iZqa0Hs6IOxaXEDHkF/zisHcflzYGkMnYcSFj+wRgYEuBFaHLCwuxQf9OdGu31i05w849i6tY1Q==", "dev": true, "license": "MIT", "engines": { @@ -5667,7 +5226,7 @@ }, "peerDependencies": { "@angular/compiler-cli": "^19.0.0", - "typescript": ">=5.5 <5.7", + "typescript": ">=5.5 <5.8", "webpack": "^5.54.0" } }, @@ -6009,6 +5568,7 @@ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 18" } @@ -6018,6 +5578,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -6036,6 +5597,7 @@ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" @@ -6049,6 +5611,7 @@ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/request": "^8.3.0", "@octokit/types": "^13.0.0", @@ -6059,16 +5622,18 @@ } }, "node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^12.6.0" }, @@ -6083,13 +5648,15 @@ "version": "20.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/openapi-types": "^20.0.0" } @@ -6099,6 +5666,7 @@ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^12.6.0" }, @@ -6113,13 +5681,15 @@ "version": "20.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { "version": "12.6.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/openapi-types": "^20.0.0" } @@ -6129,6 +5699,7 @@ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/endpoint": "^9.0.1", "@octokit/request-error": "^5.1.0", @@ -6144,6 +5715,7 @@ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^13.1.0", "deprecation": "^2.0.0", @@ -6154,18 +5726,19 @@ } }, "node_modules/@octokit/types": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", - "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "@octokit/openapi-types": "^23.0.1" } }, "node_modules/@parcel/watcher": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", - "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6184,25 +5757,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.0", - "@parcel/watcher-darwin-arm64": "2.5.0", - "@parcel/watcher-darwin-x64": "2.5.0", - "@parcel/watcher-freebsd-x64": "2.5.0", - "@parcel/watcher-linux-arm-glibc": "2.5.0", - "@parcel/watcher-linux-arm-musl": "2.5.0", - "@parcel/watcher-linux-arm64-glibc": "2.5.0", - "@parcel/watcher-linux-arm64-musl": "2.5.0", - "@parcel/watcher-linux-x64-glibc": "2.5.0", - "@parcel/watcher-linux-x64-musl": "2.5.0", - "@parcel/watcher-win32-arm64": "2.5.0", - "@parcel/watcher-win32-ia32": "2.5.0", - "@parcel/watcher-win32-x64": "2.5.0" + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -6221,9 +5794,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", - "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -6242,9 +5815,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ "x64" ], @@ -6263,9 +5836,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ "x64" ], @@ -6284,9 +5857,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ "arm" ], @@ -6305,9 +5878,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ "arm" ], @@ -6326,9 +5899,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ "arm64" ], @@ -6347,9 +5920,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ "arm64" ], @@ -6368,9 +5941,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -6389,9 +5962,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ "x64" ], @@ -6410,9 +5983,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -6431,9 +6004,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], @@ -6452,9 +6025,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], @@ -6486,14 +6059,6 @@ "node": ">=0.10" } }, - "node_modules/@parcel/watcher/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6506,12 +6071,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", - "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", + "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright": "1.48.2" + "playwright": "1.50.1" }, "bin": { "playwright": "cli.js" @@ -6530,9 +6096,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.26.0.tgz", - "integrity": "sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.0.tgz", + "integrity": "sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ==", "cpu": [ "arm" ], @@ -6544,9 +6110,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.26.0.tgz", - "integrity": "sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.0.tgz", + "integrity": "sha512-yVh0Kf1f0Fq4tWNf6mWcbQBCLDpDrDEl88lzPgKhrgTcDrTtlmun92ywEF9dCjmYO3EFiSuJeeo9cYRxl2FswA==", "cpu": [ "arm64" ], @@ -6558,9 +6124,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.26.0.tgz", - "integrity": "sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.0.tgz", + "integrity": "sha512-gCs0ErAZ9s0Osejpc3qahTsqIPUDjSKIyxK/0BGKvL+Tn0n3Kwvj8BrCv7Y5sR1Ypz1K2qz9Ny0VvkVyoXBVUQ==", "cpu": [ "arm64" ], @@ -6572,9 +6138,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.26.0.tgz", - "integrity": "sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.0.tgz", + "integrity": "sha512-aIB5Anc8hngk15t3GUkiO4pv42ykXHfmpXGS+CzM9CTyiWyT8HIS5ygRAy7KcFb/wiw4Br+vh1byqcHRTfq2tQ==", "cpu": [ "x64" ], @@ -6586,9 +6152,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.26.0.tgz", - "integrity": "sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.0.tgz", + "integrity": "sha512-kpdsUdMlVJMRMaOf/tIvxk8TQdzHhY47imwmASOuMajg/GXpw8GKNd8LNwIHE5Yd1onehNpcUB9jHY6wgw9nHQ==", "cpu": [ "arm64" ], @@ -6600,9 +6166,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.26.0.tgz", - "integrity": "sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.0.tgz", + "integrity": "sha512-D0RDyHygOBCQiqookcPevrvgEarN0CttBecG4chOeIYCNtlKHmf5oi5kAVpXV7qs0Xh/WO2RnxeicZPtT50V0g==", "cpu": [ "x64" ], @@ -6614,9 +6180,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.26.0.tgz", - "integrity": "sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.0.tgz", + "integrity": "sha512-mCIw8j5LPDXmCOW8mfMZwT6F/Kza03EnSr4wGYEswrEfjTfVsFOxvgYfuRMxTuUF/XmRb9WSMD5GhCWDe2iNrg==", "cpu": [ "arm" ], @@ -6628,9 +6194,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.26.0.tgz", - "integrity": "sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.0.tgz", + "integrity": "sha512-AwwldAu4aCJPob7zmjuDUMvvuatgs8B/QiVB0KwkUarAcPB3W+ToOT+18TQwY4z09Al7G0BvCcmLRop5zBLTag==", "cpu": [ "arm" ], @@ -6642,9 +6208,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.26.0.tgz", - "integrity": "sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.0.tgz", + "integrity": "sha512-e7kDUGVP+xw05pV65ZKb0zulRploU3gTu6qH1qL58PrULDGxULIS0OSDQJLH7WiFnpd3ZKUU4VM3u/Z7Zw+e7Q==", "cpu": [ "arm64" ], @@ -6656,9 +6222,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.26.0.tgz", - "integrity": "sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.0.tgz", + "integrity": "sha512-SXYJw3zpwHgaBqTXeAZ31qfW/v50wq4HhNVvKFhRr5MnptRX2Af4KebLWR1wpxGJtLgfS2hEPuALRIY3LPAAcA==", "cpu": [ "arm64" ], @@ -6669,10 +6235,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.0.tgz", + "integrity": "sha512-e5XiCinINCI4RdyU3sFyBH4zzz7LiQRvHqDtRe9Dt8o/8hTBaYpdPimayF00eY2qy5j4PaaWK0azRgUench6WQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.26.0.tgz", - "integrity": "sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.0.tgz", + "integrity": "sha512-3SWN3e0bAsm9ToprLFBSro8nJe6YN+5xmB11N4FfNf92wvLye/+Rh5JGQtKOpwLKt6e61R1RBc9g+luLJsc23A==", "cpu": [ "ppc64" ], @@ -6684,9 +6264,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.26.0.tgz", - "integrity": "sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.0.tgz", + "integrity": "sha512-B1Oqt3GLh7qmhvfnc2WQla4NuHlcxAD5LyueUi5WtMc76ZWY+6qDtQYqnxARx9r+7mDGfamD+8kTJO0pKUJeJA==", "cpu": [ "riscv64" ], @@ -6698,9 +6278,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.26.0.tgz", - "integrity": "sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.0.tgz", + "integrity": "sha512-UfUCo0h/uj48Jq2lnhX0AOhZPSTAq3Eostas+XZ+GGk22pI+Op1Y6cxQ1JkUuKYu2iU+mXj1QjPrZm9nNWV9rg==", "cpu": [ "s390x" ], @@ -6712,9 +6292,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.26.0.tgz", - "integrity": "sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.0.tgz", + "integrity": "sha512-chZLTUIPbgcpm+Z7ALmomXW8Zh+wE2icrG+K6nt/HenPLmtwCajhQC5flNSk1Xy5EDMt/QAOz2MhzfOfJOLSiA==", "cpu": [ "x64" ], @@ -6726,9 +6306,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.26.0.tgz", - "integrity": "sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.0.tgz", + "integrity": "sha512-jo0UolK70O28BifvEsFD/8r25shFezl0aUk2t0VJzREWHkq19e+pcLu4kX5HiVXNz5qqkD+aAq04Ct8rkxgbyQ==", "cpu": [ "x64" ], @@ -6740,9 +6320,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.26.0.tgz", - "integrity": "sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.0.tgz", + "integrity": "sha512-Vmg0NhAap2S54JojJchiu5An54qa6t/oKT7LmDaWggpIcaiL8WcWHEN6OQrfTdL6mQ2GFyH7j2T5/3YPEDOOGA==", "cpu": [ "arm64" ], @@ -6754,9 +6334,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.26.0.tgz", - "integrity": "sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.0.tgz", + "integrity": "sha512-CV2aqhDDOsABKHKhNcs1SZFryffQf8vK2XrxP6lxC99ELZAdvsDgPklIBfd65R8R+qvOm1SmLaZ/Fdq961+m7A==", "cpu": [ "ia32" ], @@ -6768,9 +6348,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.26.0.tgz", - "integrity": "sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.0.tgz", + "integrity": "sha512-g2ASy1QwHP88y5KWvblUolJz9rN+i4ZOsYzkEwcNfaNooxNUXG+ON6F5xFo0NIItpHqxcdAyls05VXpBnludGw==", "cpu": [ "x64" ], @@ -6782,14 +6362,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.0.4.tgz", - "integrity": "sha512-1fXBtkA/AjgMPxHLpGlw7NuT/wggCqAwBAmDnSiRnBBV7Pgs/tHorLgh7A9eoUi3c8CYCuAh8zqWNyjBGGigOQ==", + "version": "19.1.5", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.1.5.tgz", + "integrity": "sha512-Yks2QD87z2qJhVLi6O0tQDBG4pyX5n5c8BYEyZ+yiThjzIXBRkHjWS1jIFvd/y1+yU/NQFHYG/sy8sVOxfQ9IA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.0.4", - "@angular-devkit/schematics": "19.0.4", + "@angular-devkit/core": "19.1.5", + "@angular-devkit/schematics": "19.1.5", "jsonc-parser": "3.3.1" }, "engines": { @@ -6798,6 +6378,16 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@sentry/core": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.53.0.tgz", + "integrity": "sha512-u6p5JeGSgvcoDqVcPve2gcJuhks8EQXPELzeYKuW3rHpsUfkLG6X5RVtk32dKOqqL2qzvMelnknBN7tyIf5PiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sigstore/bundle": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.0.0.tgz", @@ -6889,6 +6479,7 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -7118,9 +6709,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", - "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "dev": true, "license": "MIT", "dependencies": { @@ -7164,6 +6755,7 @@ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -7227,12 +6819,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.8.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", - "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", + "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-forge": { @@ -7246,9 +6839,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", "dev": true, "license": "MIT" }, @@ -7322,9 +6915,9 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", "dev": true, "license": "MIT", "dependencies": { @@ -7347,20 +6940,21 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", - "integrity": "sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz", + "integrity": "sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/type-utils": "8.12.2", - "@typescript-eslint/utils": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/type-utils": "8.22.0", + "@typescript-eslint/utils": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7371,24 +6965,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", - "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.22.0.tgz", + "integrity": "sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/typescript-estree": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "debug": "^4.3.4" }, "engines": { @@ -7399,22 +6990,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", - "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz", + "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2" + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7425,15 +7013,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.2.tgz", - "integrity": "sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz", + "integrity": "sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.12.2", - "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/utils": "8.22.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7442,17 +7031,17 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", - "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", + "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7462,19 +7051,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", - "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz", + "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/visitor-keys": "8.12.2", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7483,10 +7073,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -7494,6 +7082,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7503,6 +7092,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7514,15 +7104,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", - "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz", + "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.12.2", - "@typescript-eslint/types": "8.12.2", - "@typescript-eslint/typescript-estree": "8.12.2" + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7532,17 +7123,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.12.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", - "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz", + "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.12.2", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.22.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7552,162 +7145,190 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", - "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.6.0" + "node": ">=14.21.3" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -7715,13 +7336,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", @@ -7797,6 +7420,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -7815,6 +7439,7 @@ "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", "dev": true, + "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "regex-parser": "^2.2.11" @@ -7828,6 +7453,7 @@ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, + "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -7838,14 +7464,11 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, "engines": { "node": ">= 14" } @@ -7871,6 +7494,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -7888,6 +7512,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -7900,6 +7525,7 @@ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -8033,6 +7659,13 @@ "dev": true, "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -8067,6 +7700,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", @@ -8289,9 +7923,9 @@ "license": "MIT" }, "node_modules/beasties": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.1.0.tgz", - "integrity": "sha512-+Ssscd2gVG24qRNC+E2g88D+xsQW4xwakWtKAiGEQ3Pw54/FGdyo9RrfxhGhEv6ilFVbB7r3Lgx+QnAxnSpECw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.2.0.tgz", + "integrity": "sha512-Ljqskqx/tbZagIglYoJIMzH5zgssyp+in9+9sAyh15N22AornBeIDnb8EZ6Rk+6ShfMxd92uO3gfpT0NtZbpow==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8299,23 +7933,28 @@ "css-what": "^6.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "htmlparser2": "^9.0.0", + "htmlparser2": "^9.1.0", "picocolors": "^1.1.1", - "postcss": "^8.4.47", + "postcss": "^8.4.49", "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -8444,9 +8083,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "funding": [ { "type": "opencollective", @@ -8461,10 +8100,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { @@ -8479,6 +8119,7 @@ "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, + "license": "MIT", "dependencies": { "fast-json-stable-stringify": "2.x" }, @@ -8732,6 +8373,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -8751,9 +8423,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001676", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz", - "integrity": "sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==", + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", "funding": [ { "type": "opencollective", @@ -8767,7 +8439,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/canvas": { "version": "2.11.2", @@ -9237,6 +8910,7 @@ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", "dev": true, + "license": "MIT", "dependencies": { "is-what": "^3.14.1" }, @@ -9249,6 +8923,7 @@ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, + "license": "MIT", "dependencies": { "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", @@ -9273,6 +8948,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -9281,13 +8957,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.2" + "browserslist": "^4.24.3" }, "funding": { "type": "opencollective", @@ -9306,6 +8982,7 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -9331,13 +9008,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9373,10 +9052,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -9391,6 +9071,7 @@ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.33", @@ -9456,6 +9137,7 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -9677,7 +9359,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/destroy": { "version": "1.2.0", @@ -9804,9 +9487,9 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -9818,6 +9501,21 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -9832,10 +9530,27 @@ "dev": true, "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.50", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.50.tgz", - "integrity": "sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==" + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", + "license": "ISC" }, "node_modules/emittery": { "version": "0.13.1", @@ -9859,6 +9574,7 @@ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -9965,6 +9681,7 @@ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "prr": "~1.0.1" @@ -9983,13 +9700,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -10009,48 +9724,64 @@ "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, "node_modules/esbuild-wasm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.24.0.tgz", - "integrity": "sha512-xhNn5tL1AhkPg4ft59yXT6FkwKXiPSYyz1IeinJHUJpjvOHOIPvdmFQc0pGdjxlKSbzZc2mNmtVOWAR1EF/JAg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.24.2.tgz", + "integrity": "sha512-03/7Z1gD+ohDnScFztvI4XddTAbKVmMEzCvvkBpQdWKEXJ+73dTyeNrmdxP1Q0zpDMFjzUJwtK4rLjqwiHbzkw==", "dev": true, "license": "MIT", "bin": { @@ -10107,26 +9838,27 @@ } }, "node_modules/eslint": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", - "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", + "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.7.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.14.0", - "@eslint/plugin-kit": "^0.2.0", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.10.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.19.0", + "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.0", + "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -10145,8 +9877,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -10303,6 +10034,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -10320,6 +10052,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -10396,7 +10129,8 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", @@ -10558,15 +10292,16 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -10637,6 +10372,39 @@ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -10762,6 +10530,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -10830,6 +10599,7 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -10949,16 +10719,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -10976,6 +10752,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -11045,6 +10835,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.2", @@ -11065,6 +10856,7 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -11073,12 +10865,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11124,23 +10917,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -11157,10 +10939,11 @@ "peer": true }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -11253,23 +11036,6 @@ "node": ">=12" } }, - "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -11328,9 +11094,9 @@ } }, "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", "dev": true, "license": "MIT" }, @@ -11339,6 +11105,7 @@ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, + "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -11379,6 +11146,7 @@ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", @@ -11396,18 +11164,19 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -11451,6 +11220,7 @@ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -11531,6 +11301,7 @@ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", "dev": true, + "license": "MIT", "optional": true, "bin": { "image-size": "bin/image-size.js" @@ -11688,12 +11459,16 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11862,7 +11637,8 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-wsl": { "version": "2.2.0", @@ -11997,6 +11773,25 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -12347,17 +12142,17 @@ } }, "node_modules/jest-preset-angular": { - "version": "14.4.2", - "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.4.2.tgz", - "integrity": "sha512-BYYv0FaTDfBNh8WyA9mpOV3krfw20kurBGK8INZUnv7KZDAWZuQtCET4TwTWxSNQ9jS1OX1+a5weCm/bTDDM1A==", + "version": "14.5.1", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-14.5.1.tgz", + "integrity": "sha512-HLYYMwNcv3mFrKbOPJwR29tKqVg+yWxez8ilCIsEj1HRYZ/OVsBy5+dcMok+VqL5nmeukTsGnEfGWt+SsQqtkA==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "esbuild-wasm": ">=0.15.13", - "jest-environment-jsdom": "^29.0.0", - "jest-util": "^29.0.0", - "pretty-format": "^29.0.0", + "jest-environment-jsdom": "^29.7.0", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0", "ts-jest": "^29.0.0" }, "engines": { @@ -12371,7 +12166,13 @@ "@angular/core": ">=15.0.0 <20.0.0", "@angular/platform-browser-dynamic": ">=15.0.0 <20.0.0", "jest": "^29.0.0", + "jsdom": ">=20.0.0", "typescript": ">=4.8" + }, + "peerDependenciesMeta": { + "jsdom": { + "optional": true + } } }, "node_modules/jest-regex-util": { @@ -12859,6 +12660,7 @@ "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", "dev": true, + "license": "MIT", "dependencies": { "source-map-support": "^0.5.5" } @@ -12911,10 +12713,11 @@ } }, "node_modules/less": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", - "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.1.tgz", + "integrity": "sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -12941,6 +12744,7 @@ "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 18.12.0" }, @@ -12967,6 +12771,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "pify": "^4.0.1", @@ -12981,6 +12786,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "optional": true, "bin": { "semver": "bin/semver" @@ -12991,6 +12797,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -13023,6 +12830,7 @@ "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", "dev": true, + "license": "ISC", "dependencies": { "webpack-sources": "^3.0.0" }, @@ -13146,9 +12954,9 @@ } }, "node_modules/lmdb": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.1.5.tgz", - "integrity": "sha512-46Mch5Drq+A93Ss3gtbg+Xuvf5BOgIuvhKDWoGa3HcPHI6BL2NCOkRdSx1D4VfzwrxhnsjbyIVsLRlQHu6URvw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.2.tgz", + "integrity": "sha512-LriG93la4PbmPMwI7Hbv8W+0ncLK7549w4sbZSi4QGDjnnxnmNMgxUkaQTEMzH8TpwsfFvgEjpLX7V8B/I9e3g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -13164,14 +12972,22 @@ "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.1.5", - "@lmdb/lmdb-darwin-x64": "3.1.5", - "@lmdb/lmdb-linux-arm": "3.1.5", - "@lmdb/lmdb-linux-arm64": "3.1.5", - "@lmdb/lmdb-linux-x64": "3.1.5", - "@lmdb/lmdb-win32-x64": "3.1.5" + "@lmdb/lmdb-darwin-arm64": "3.2.2", + "@lmdb/lmdb-darwin-x64": "3.2.2", + "@lmdb/lmdb-linux-arm": "3.2.2", + "@lmdb/lmdb-linux-arm64": "3.2.2", + "@lmdb/lmdb-linux-x64": "3.2.2", + "@lmdb/lmdb-win32-x64": "3.2.2" } }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -13186,6 +13002,7 @@ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12.13.0" } @@ -13225,7 +13042,8 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -13486,9 +13304,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "license": "MIT", "dependencies": { @@ -13553,6 +13371,16 @@ "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -13564,9 +13392,9 @@ } }, "node_modules/memfs": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.1.tgz", - "integrity": "sha512-Fq5CMEth+2iprLJ5mNizRcWuiwRZYjNkUD0zKk224jZunE9CRacTRDK8QLALbMBlNX2y3nY6lKZbesCwDwacig==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -13645,6 +13473,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -14119,9 +13948,9 @@ "peer": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -14129,6 +13958,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -14154,6 +13984,7 @@ "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.3", @@ -14171,6 +14002,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -14236,9 +14068,9 @@ } }, "node_modules/ngx-cookie-service": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-19.0.0.tgz", - "integrity": "sha512-itxGY1BlIRoEjEtDsSsRKnJuiQteTMLKPNHrykiH06tjUQ1bi3orE7YKU1D210VBqVy1jNrB7hKuGOOIQtQJDA==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-19.1.0.tgz", + "integrity": "sha512-EddQiinQ/EicUp4Lg/j0RwTsAJp7FCAGv+Z5dfwll/BoROb7hTnf7Suqbx2xZFNM4YqLXpZfXlAe8zig9cBfhw==", "license": "MIT", "dependencies": { "tslib": "^2.8.0" @@ -14248,6 +14080,19 @@ "@angular/core": "^19.0.0" } }, + "node_modules/ngx-device-detector": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ngx-device-detector/-/ngx-device-detector-9.0.0.tgz", + "integrity": "sha512-zpio/wqH1GnxIpWCdA7cp5fmWf7YLycgzfXzQHmB9vaS7eAcqpBF5mxDS65IhE7TzNTJziDrYJCT+9tVqBcsDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0" + } + }, "node_modules/ngx-file-drop": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-16.0.0.tgz", @@ -14264,6 +14109,21 @@ "@angular/core": ">=14.0.0" } }, + "node_modules/ngx-ui-tour-core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/ngx-ui-tour-core/-/ngx-ui-tour-core-14.0.0.tgz", + "integrity": "sha512-6pzzEwxn/gCS3puEXDqgINBRbhvhzHYjmiA9DTCNEx1dPfYwjZVmPqNvNeZIVHucVnVZViAAKvA6MTc3Gm7aOw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0", + "@angular/router": "^19.0.0", + "rxjs": "^7.4.0" + } + }, "node_modules/ngx-ui-tour-ng-bootstrap": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/ngx-ui-tour-ng-bootstrap/-/ngx-ui-tour-ng-bootstrap-16.0.0.tgz", @@ -14279,21 +14139,6 @@ "@ng-bootstrap/ng-bootstrap": "^18.0.0" } }, - "node_modules/ngx-ui-tour-ng-bootstrap/node_modules/ngx-ui-tour-core": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/ngx-ui-tour-core/-/ngx-ui-tour-core-14.0.0.tgz", - "integrity": "sha512-6pzzEwxn/gCS3puEXDqgINBRbhvhzHYjmiA9DTCNEx1dPfYwjZVmPqNvNeZIVHucVnVZViAAKvA6MTc3Gm7aOw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@angular/common": "^19.0.0", - "@angular/core": "^19.0.0", - "@angular/router": "^19.0.0", - "rxjs": "^7.4.0" - } - }, "node_modules/node-abi": { "version": "3.71.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", @@ -14308,10 +14153,9 @@ } }, "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true, + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT", "optional": true }, @@ -14580,9 +14424,10 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" }, "node_modules/nopt": { "version": "8.0.0", @@ -14629,6 +14474,7 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -14670,9 +14516,9 @@ } }, "node_modules/npm-package-arg": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.0.tgz", - "integrity": "sha512-ZTE0hbwSdTNL+Stx2zxSqdu2KZfNDcrtrLdIk7XGnQFYBWYDho/ORvXtn5XEePcL3tFpGjHCV3X3xrtDh7eZ+A==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.1.tgz", + "integrity": "sha512-aDxjFfPV3Liw0WOBWlyZLMBqtbgbg03rmGvHDJa2Ttv7tIz+1oB5qWec4psCDFZcZi9b5XdGkPdQiJxOPzvQRQ==", "dev": true, "license": "ISC", "dependencies": { @@ -15225,6 +15071,7 @@ "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -15430,6 +15277,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -15476,13 +15324,6 @@ "node": "^18.12.0 || >= 20.9.0" } }, - "node_modules/pdfjs-dist/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -15506,6 +15347,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=6" @@ -15521,9 +15363,9 @@ } }, "node_modules/piscina": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.7.0.tgz", - "integrity": "sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", + "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -15635,12 +15477,13 @@ } }, "node_modules/playwright": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", - "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", + "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.48.2" + "playwright-core": "1.50.1" }, "bin": { "playwright": "cli.js" @@ -15653,10 +15496,11 @@ } }, "node_modules/playwright-core": { - "version": "1.48.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", - "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", + "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, @@ -15670,6 +15514,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -15712,6 +15557,7 @@ "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dev": true, + "license": "MIT", "dependencies": { "cosmiconfig": "^9.0.0", "jiti": "^1.20.0", @@ -15750,6 +15596,7 @@ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, + "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -15758,13 +15605,14 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dev": true, + "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -15775,12 +15623,13 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, + "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -15794,6 +15643,7 @@ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, + "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" }, @@ -15805,10 +15655,11 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -15821,7 +15672,8 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/prebuild-install": { "version": "7.1.2", @@ -16054,6 +15906,7 @@ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, + "license": "MIT", "optional": true }, "node_modules/psl": { @@ -16286,7 +16139,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/regexpu-core": { "version": "6.2.0", @@ -16350,18 +16204,22 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -16392,6 +16250,7 @@ "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", "dev": true, + "license": "MIT", "dependencies": { "adjust-sourcemap-loader": "^4.0.0", "convert-source-map": "^1.7.0", @@ -16408,6 +16267,7 @@ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, + "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -16422,6 +16282,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -16492,9 +16353,9 @@ } }, "node_modules/rollup": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.26.0.tgz", - "integrity": "sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==", + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.0.tgz", + "integrity": "sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -16508,24 +16369,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.26.0", - "@rollup/rollup-android-arm64": "4.26.0", - "@rollup/rollup-darwin-arm64": "4.26.0", - "@rollup/rollup-darwin-x64": "4.26.0", - "@rollup/rollup-freebsd-arm64": "4.26.0", - "@rollup/rollup-freebsd-x64": "4.26.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.26.0", - "@rollup/rollup-linux-arm-musleabihf": "4.26.0", - "@rollup/rollup-linux-arm64-gnu": "4.26.0", - "@rollup/rollup-linux-arm64-musl": "4.26.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.26.0", - "@rollup/rollup-linux-riscv64-gnu": "4.26.0", - "@rollup/rollup-linux-s390x-gnu": "4.26.0", - "@rollup/rollup-linux-x64-gnu": "4.26.0", - "@rollup/rollup-linux-x64-musl": "4.26.0", - "@rollup/rollup-win32-arm64-msvc": "4.26.0", - "@rollup/rollup-win32-ia32-msvc": "4.26.0", - "@rollup/rollup-win32-x64-msvc": "4.26.0", + "@rollup/rollup-android-arm-eabi": "4.34.0", + "@rollup/rollup-android-arm64": "4.34.0", + "@rollup/rollup-darwin-arm64": "4.34.0", + "@rollup/rollup-darwin-x64": "4.34.0", + "@rollup/rollup-freebsd-arm64": "4.34.0", + "@rollup/rollup-freebsd-x64": "4.34.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.0", + "@rollup/rollup-linux-arm-musleabihf": "4.34.0", + "@rollup/rollup-linux-arm64-gnu": "4.34.0", + "@rollup/rollup-linux-arm64-musl": "4.34.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.0", + "@rollup/rollup-linux-riscv64-gnu": "4.34.0", + "@rollup/rollup-linux-s390x-gnu": "4.34.0", + "@rollup/rollup-linux-x64-gnu": "4.34.0", + "@rollup/rollup-linux-x64-musl": "4.34.0", + "@rollup/rollup-win32-arm64-msvc": "4.34.0", + "@rollup/rollup-win32-ia32-msvc": "4.34.0", + "@rollup/rollup-win32-x64-msvc": "4.34.0", "fsevents": "~2.3.2" } }, @@ -16599,9 +16461,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.80.7", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.7.tgz", - "integrity": "sha512-MVWvN0u5meytrSjsU7AWsbhoXi1sc58zADXFllfZzbsBT1GHjjar6JwBINYPRrkx/zqnQ6uqbQuHgE95O+C+eQ==", + "version": "1.83.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", + "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", "dev": true, "license": "MIT", "dependencies": { @@ -16620,9 +16482,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.3.tgz", - "integrity": "sha512-gosNorT1RCkuCMyihv6FBRR7BMV06oKRAs+l4UMp1mlcVg9rWN6KMmUj3igjQwmYys4mDP3etEYJgiHRbgHCHA==", + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", + "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "dev": true, "license": "MIT", "dependencies": { @@ -16665,6 +16527,7 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, + "license": "ISC", "optional": true }, "node_modules/saxes": { @@ -16680,10 +16543,11 @@ } }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -16691,7 +16555,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -16972,16 +16836,73 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -17180,6 +17101,7 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -17189,6 +17111,7 @@ "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", "dev": true, + "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" @@ -17209,6 +17132,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -17601,9 +17525,9 @@ "dev": true }, "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -17745,12 +17669,6 @@ "node": ">=8" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/thingies": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", @@ -17868,36 +17786,40 @@ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, + "license": "MIT", "bin": { "tree-kill": "cli.js" } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-jest": { - "version": "29.1.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.5.tgz", - "integrity": "sha512-UuClSYxM7byvvYfyWdFI+/2UxMmwNyJb0NPkZPQE2hew3RurV7l7zURgOHAd/1I1ZdPpe3GUsXNXAcN8TFKSIg==", + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, + "license": "MIT", "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" @@ -18024,6 +17946,7 @@ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } @@ -18092,7 +18015,8 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/typescript": { "version": "5.5.4", @@ -18120,10 +18044,11 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -18174,6 +18099,7 @@ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -18211,7 +18137,8 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/universalify": { "version": "2.0.1", @@ -18233,24 +18160,17 @@ } }, "node_modules/unplugin": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.15.0.tgz", - "integrity": "sha512-jTPIs63W+DUEDW207ztbaoO7cQ4p5aVaB823LSlxpsFEU3Mykwxf3ZGC/wzxFJeZlASZYgVrWeo7LgOrqJZ8RA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", "dev": true, + "license": "MIT", "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" }, "engines": { "node": ">=14.0.0" - }, - "peerDependencies": { - "webpack-sources": "^3" - }, - "peerDependenciesMeta": { - "webpack-sources": { - "optional": true - } } }, "node_modules/update-browserslist-db": { @@ -18327,13 +18247,14 @@ } }, "node_modules/uuid": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", - "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist/esm/bin/uuid" } @@ -18396,21 +18317,21 @@ } }, "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", + "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -18419,19 +18340,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -18452,6 +18379,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -18527,17 +18460,17 @@ } }, "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", @@ -18604,9 +18537,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz", - "integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", "dev": true, "license": "MIT", "dependencies": { @@ -18623,10 +18556,9 @@ "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "express": "^4.19.2", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", - "http-proxy-middleware": "^2.0.3", + "http-proxy-middleware": "^2.0.7", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", @@ -18765,6 +18697,7 @@ "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", "dev": true, + "license": "MIT", "dependencies": { "typed-assert": "^1.0.8" }, @@ -18785,7 +18718,8 @@ "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", @@ -19065,10 +18999,14 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -19133,10 +19071,11 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src-ui/package.json b/src-ui/package.json index f9d4d8158..1aecf9c06 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -11,17 +11,17 @@ }, "private": true, "dependencies": { - "@angular/cdk": "^19.0.2", - "@angular/common": "~19.0.3", - "@angular/compiler": "~19.0.3", - "@angular/core": "~19.0.3", - "@angular/forms": "~19.0.3", - "@angular/localize": "~19.0.3", - "@angular/platform-browser": "~19.0.3", - "@angular/platform-browser-dynamic": "~19.0.3", - "@angular/router": "~19.0.3", + "@angular/cdk": "^19.1.2", + "@angular/common": "~19.1.4", + "@angular/compiler": "~19.1.4", + "@angular/core": "~19.1.4", + "@angular/forms": "~19.1.4", + "@angular/localize": "~19.1.4", + "@angular/platform-browser": "~19.1.4", + "@angular/platform-browser-dynamic": "~19.1.4", + "@angular/router": "~19.1.4", "@ng-bootstrap/ng-bootstrap": "^18.0.0", - "@ng-select/ng-select": "^14.1.0", + "@ng-select/ng-select": "^14.2.0", "@ngneat/dirty-check-forms": "^3.0.3", "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.3", @@ -30,36 +30,37 @@ "ng2-pdf-viewer": "^10.4.0", "ngx-bootstrap-icons": "^1.9.3", "ngx-color": "^9.0.0", - "ngx-cookie-service": "^19.0.0", + "ngx-cookie-service": "^19.1.0", + "ngx-device-detector": "^9.0.0", "ngx-file-drop": "^16.0.0", "ngx-ui-tour-ng-bootstrap": "^16.0.0", "rxjs": "^7.8.1", "tslib": "^2.8.1", "utif": "^3.1.0", - "uuid": "^11.0.2", + "uuid": "^11.0.5", "zone.js": "^0.15.0" }, "devDependencies": { - "@angular-builders/custom-webpack": "^19.0.0-beta.0", - "@angular-builders/jest": "^19.0.0-beta.1", + "@angular-builders/custom-webpack": "^19.0.0", + "@angular-builders/jest": "^19.0.0", "@angular-devkit/build-angular": "^19.0.4", - "@angular-devkit/core": "^19.0.4", - "@angular-devkit/schematics": "^19.0.4", - "@angular-eslint/builder": "19.0.0", - "@angular-eslint/eslint-plugin": "19.0.0", - "@angular-eslint/eslint-plugin-template": "19.0.0", - "@angular-eslint/schematics": "19.0.0", - "@angular-eslint/template-parser": "19.0.0", - "@angular/cli": "~19.0.4", - "@angular/compiler-cli": "~19.0.3", - "@codecov/webpack-plugin": "^1.2.1", - "@playwright/test": "^1.48.2", + "@angular-devkit/core": "^19.1.5", + "@angular-devkit/schematics": "^19.1.5", + "@angular-eslint/builder": "19.0.2", + "@angular-eslint/eslint-plugin": "19.0.2", + "@angular-eslint/eslint-plugin-template": "19.0.2", + "@angular-eslint/schematics": "19.0.2", + "@angular-eslint/template-parser": "19.0.2", + "@angular/cli": "~19.1.5", + "@angular/compiler-cli": "~19.1.4", + "@codecov/webpack-plugin": "^1.8.0", + "@playwright/test": "^1.50.1", "@types/jest": "^29.5.14", - "@types/node": "^22.8.6", - "@typescript-eslint/eslint-plugin": "^8.12.2", - "@typescript-eslint/parser": "^8.12.2", + "@types/node": "^22.13.0", + "@typescript-eslint/eslint-plugin": "^8.22.0", + "@typescript-eslint/parser": "^8.22.0", "@typescript-eslint/utils": "^8.0.0", - "eslint": "^9.14.0", + "eslint": "^9.19.0", "jest": "29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-preset-angular": "^14.4.2", diff --git a/src-ui/setup-jest.ts b/src-ui/setup-jest.ts index 244938606..52dccaf02 100644 --- a/src-ui/setup-jest.ts +++ b/src-ui/setup-jest.ts @@ -100,6 +100,15 @@ Object.defineProperty(navigator, 'clipboard', { }, }) Object.defineProperty(navigator, 'canShare', { value: () => true }) +if (!navigator.share) { + Object.defineProperty(navigator, 'share', { value: jest.fn() }) +} +if (!URL.createObjectURL) { + Object.defineProperty(window.URL, 'createObjectURL', { value: jest.fn() }) +} +if (!URL.revokeObjectURL) { + Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() }) +} Object.defineProperty(window, 'ResizeObserver', { value: mock() }) Object.defineProperty(window, 'location', { configurable: true, diff --git a/src-ui/src/app/app.component.spec.ts b/src-ui/src/app/app.component.spec.ts index 74626f847..bc59f78dc 100644 --- a/src-ui/src/app/app.component.spec.ts +++ b/src-ui/src/app/app.component.spec.ts @@ -18,20 +18,20 @@ import { ToastsComponent } from './components/common/toasts/toasts.component' import { FileDropComponent } from './components/file-drop/file-drop.component' import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard' import { PermissionsGuard } from './guards/permissions.guard' -import { - ConsumerStatusService, - FileStatus, -} from './services/consumer-status.service' import { HotKeyService } from './services/hot-key.service' import { PermissionsService } from './services/permissions.service' import { SettingsService } from './services/settings.service' import { Toast, ToastService } from './services/toast.service' +import { + FileStatus, + WebsocketStatusService, +} from './services/websocket-status.service' describe('AppComponent', () => { let component: AppComponent let fixture: ComponentFixture let tourService: TourService - let consumerStatusService: ConsumerStatusService + let websocketStatusService: WebsocketStatusService let permissionsService: PermissionsService let toastService: ToastService let router: Router @@ -59,7 +59,7 @@ describe('AppComponent', () => { }).compileComponents() tourService = TestBed.inject(TourService) - consumerStatusService = TestBed.inject(ConsumerStatusService) + websocketStatusService = TestBed.inject(WebsocketStatusService) permissionsService = TestBed.inject(PermissionsService) settingsService = TestBed.inject(SettingsService) toastService = TestBed.inject(ToastService) @@ -90,7 +90,7 @@ describe('AppComponent', () => { const toastSpy = jest.spyOn(toastService, 'show') const fileStatusSubject = new Subject() jest - .spyOn(consumerStatusService, 'onDocumentConsumptionFinished') + .spyOn(websocketStatusService, 'onDocumentConsumptionFinished') .mockReturnValue(fileStatusSubject) component.ngOnInit() const status = new FileStatus() @@ -109,7 +109,7 @@ describe('AppComponent', () => { const toastSpy = jest.spyOn(toastService, 'show') const fileStatusSubject = new Subject() jest - .spyOn(consumerStatusService, 'onDocumentConsumptionFinished') + .spyOn(websocketStatusService, 'onDocumentConsumptionFinished') .mockReturnValue(fileStatusSubject) component.ngOnInit() fileStatusSubject.next(new FileStatus()) @@ -122,7 +122,7 @@ describe('AppComponent', () => { const toastSpy = jest.spyOn(toastService, 'show') const fileStatusSubject = new Subject() jest - .spyOn(consumerStatusService, 'onDocumentDetected') + .spyOn(websocketStatusService, 'onDocumentDetected') .mockReturnValue(fileStatusSubject) component.ngOnInit() fileStatusSubject.next(new FileStatus()) @@ -136,7 +136,7 @@ describe('AppComponent', () => { const toastSpy = jest.spyOn(toastService, 'show') const fileStatusSubject = new Subject() jest - .spyOn(consumerStatusService, 'onDocumentDetected') + .spyOn(websocketStatusService, 'onDocumentDetected') .mockReturnValue(fileStatusSubject) component.ngOnInit() fileStatusSubject.next(new FileStatus()) @@ -148,7 +148,7 @@ describe('AppComponent', () => { const toastSpy = jest.spyOn(toastService, 'showError') const fileStatusSubject = new Subject() jest - .spyOn(consumerStatusService, 'onDocumentConsumptionFailed') + .spyOn(websocketStatusService, 'onDocumentConsumptionFailed') .mockReturnValue(fileStatusSubject) component.ngOnInit() fileStatusSubject.next(new FileStatus()) diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts index c89f5d4c2..a6c4702b7 100644 --- a/src-ui/src/app/app.component.ts +++ b/src-ui/src/app/app.component.ts @@ -6,7 +6,6 @@ import { ToastsComponent } from './components/common/toasts/toasts.component' import { FileDropComponent } from './components/file-drop/file-drop.component' import { SETTINGS_KEYS } from './data/ui-settings' import { ComponentRouterService } from './services/component-router.service' -import { ConsumerStatusService } from './services/consumer-status.service' import { HotKeyService } from './services/hot-key.service' import { PermissionAction, @@ -16,6 +15,7 @@ import { import { SettingsService } from './services/settings.service' import { TasksService } from './services/tasks.service' import { ToastService } from './services/toast.service' +import { WebsocketStatusService } from './services/websocket-status.service' @Component({ selector: 'pngx-root', @@ -35,7 +35,7 @@ export class AppComponent implements OnInit, OnDestroy { constructor( private settings: SettingsService, - private consumerStatusService: ConsumerStatusService, + private websocketStatusService: WebsocketStatusService, private toastService: ToastService, private router: Router, private tasksService: TasksService, @@ -51,7 +51,7 @@ export class AppComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.consumerStatusService.disconnect() + this.websocketStatusService.disconnect() if (this.successSubscription) { this.successSubscription.unsubscribe() } @@ -76,9 +76,9 @@ export class AppComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.consumerStatusService.connect() + this.websocketStatusService.connect() - this.successSubscription = this.consumerStatusService + this.successSubscription = this.websocketStatusService .onDocumentConsumptionFinished() .subscribe((status) => { this.tasksService.reload() @@ -108,7 +108,7 @@ export class AppComponent implements OnInit, OnDestroy { } }) - this.failedSubscription = this.consumerStatusService + this.failedSubscription = this.websocketStatusService .onDocumentConsumptionFailed() .subscribe((status) => { this.tasksService.reload() @@ -121,7 +121,7 @@ export class AppComponent implements OnInit, OnDestroy { } }) - this.newDocumentSubscription = this.consumerStatusService + this.newDocumentSubscription = this.websocketStatusService .onDocumentDetected() .subscribe((status) => { this.tasksService.reload() diff --git a/src-ui/src/app/components/admin/settings/settings.component.html b/src-ui/src/app/components/admin/settings/settings.component.html index 097015973..b8a46e57e 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.html +++ b/src-ui/src/app/components/admin/settings/settings.component.html @@ -41,7 +41,7 @@
-

Appearance

+
Appearance
Display language @@ -154,28 +154,7 @@
-

Document editing

- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
-

Update checking

+
Update checking
@@ -193,7 +172,56 @@
-

Bulk editing

+
Saved Views
+
+
+ +
+
+ +
+
+
Document editing
+ +
+
+ +
+
+ +
+
+ Default zoom: +
+
+ +

Only applies to the Paperless-ngx PDF viewer.

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
Notes
+
+
+ +
+
+ +
Bulk editing
@@ -201,7 +229,7 @@
-

Global search

+
Global search
@@ -224,19 +252,6 @@
-

Saved Views

-
-
- -
-
- -

Notes

-
-
- -
-
@@ -247,7 +262,7 @@ Permissions -

Default Permissions

+
Default Permissions
@@ -329,7 +344,7 @@ Notifications -

Document processing

+
Document processing
diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index 5f587cf9e..4f50e7453 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -212,7 +212,7 @@ describe('SettingsComponent', () => { expect(toastErrorSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled() - expect(setSpy).toHaveBeenCalledTimes(28) + expect(setSpy).toHaveBeenCalledTimes(29) // succeed storeSpy.mockReturnValueOnce(of(true)) diff --git a/src-ui/src/app/components/admin/settings/settings.component.ts b/src-ui/src/app/components/admin/settings/settings.component.ts index 9bd044f78..68f702cfa 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.ts @@ -63,6 +63,7 @@ import { PermissionsUserComponent } from '../../common/input/permissions/permiss import { SelectComponent } from '../../common/input/select/select.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component' +import { ZoomSetting } from '../../document-detail/document-detail.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' enum SettingsNavIDs { @@ -125,6 +126,7 @@ export class SettingsComponent defaultPermsEditUsers: new FormControl(null), defaultPermsEditGroups: new FormControl(null), useNativePdfViewer: new FormControl(null), + pdfViewerDefaultZoom: new FormControl(null), documentEditingRemoveInboxTags: new FormControl(null), documentEditingOverlayThumbnail: new FormControl(null), searchDbOnly: new FormControl(null), @@ -154,6 +156,8 @@ export class SettingsComponent public readonly GlobalSearchType = GlobalSearchType + public readonly ZoomSetting = ZoomSetting + get systemStatusHasErrors(): boolean { return ( this.systemStatus.database.status === SystemStatusItemStatus.ERROR || @@ -276,6 +280,9 @@ export class SettingsComponent useNativePdfViewer: this.settings.get( SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER ), + pdfViewerDefaultZoom: this.settings.get( + SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING + ), displayLanguage: this.settings.getLanguage(), dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), @@ -435,6 +442,10 @@ export class SettingsComponent SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, this.settingsForm.value.useNativePdfViewer ) + this.settings.set( + SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING, + this.settingsForm.value.pdfViewerDefaultZoom + ) this.settings.set( SETTINGS_KEYS.DATE_LOCALE, this.settingsForm.value.dateLocale diff --git a/src-ui/src/app/components/admin/trash/trash.component.ts b/src-ui/src/app/components/admin/trash/trash.component.ts index fcf43e1c1..1df6ceff4 100644 --- a/src-ui/src/app/components/admin/trash/trash.component.ts +++ b/src-ui/src/app/components/admin/trash/trash.component.ts @@ -86,12 +86,17 @@ export class TrashComponent modal.componentInstance.buttonsEnabled = false this.trashService.emptyTrash([document.id]).subscribe({ next: () => { - this.toastService.showInfo($localize`Document deleted`) + this.toastService.showInfo( + $localize`Document "${document.title}" deleted` + ) modal.close() this.reload() }, error: (err) => { - this.toastService.showError($localize`Error deleting document`, err) + this.toastService.showError( + $localize`Error deleting document "${document.title}"`, + err + ) modal.close() }, }) @@ -136,7 +141,7 @@ export class TrashComponent this.trashService.restoreDocuments([document.id]).subscribe({ next: () => { this.toastService.show({ - content: $localize`Document restored`, + content: $localize`Document "${document.title}" restored`, delay: 5000, actionName: $localize`Open document`, action: () => { @@ -146,7 +151,10 @@ export class TrashComponent this.reload() }, error: (err) => { - this.toastService.showError($localize`Error restoring document`, err) + this.toastService.showError( + $localize`Error restoring document "${document.title}"`, + err + ) }, }) } diff --git a/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts b/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts index ab739aa54..559b03f51 100644 --- a/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts +++ b/src-ui/src/app/components/admin/users-groups/users-groups.component.spec.ts @@ -134,7 +134,7 @@ describe('UsersAndGroupsComponent', () => { deleteSpy.mockReturnValueOnce(of(true)) deleteDialog.confirm() expect(listAllSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user') + expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user "user1"') }) it('should logout current user if password changed, after delay', fakeAsync(() => { @@ -178,7 +178,7 @@ describe('UsersAndGroupsComponent', () => { completeSetup() let modal: NgbModalRef modalService.activeInstances.subscribe((refs) => (modal = refs[0])) - component.deleteGroup(users[0]) + component.deleteGroup(groups[0]) const deleteDialog = modal.componentInstance as ConfirmDialogComponent const deleteSpy = jest.spyOn(groupService, 'delete') const toastErrorSpy = jest.spyOn(toastService, 'showError') @@ -192,7 +192,7 @@ describe('UsersAndGroupsComponent', () => { deleteSpy.mockReturnValueOnce(of(true)) deleteDialog.confirm() expect(listAllSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group') + expect(toastInfoSpy).toHaveBeenCalledWith('Deleted group "group1"') }) it('should get group name', () => { diff --git a/src-ui/src/app/components/admin/users-groups/users-groups.component.ts b/src-ui/src/app/components/admin/users-groups/users-groups.component.ts index 41ed58dac..9ed73cde4 100644 --- a/src-ui/src/app/components/admin/users-groups/users-groups.component.ts +++ b/src-ui/src/app/components/admin/users-groups/users-groups.component.ts @@ -129,13 +129,16 @@ export class UsersAndGroupsComponent this.usersService.delete(user).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted user`) + this.toastService.showInfo($localize`Deleted user "${user.username}"`) this.usersService.listAll().subscribe((r) => { this.users = r.results }) }, error: (e) => { - this.toastService.showError($localize`Error deleting user.`, e) + this.toastService.showError( + $localize`Error deleting user "${user.username}".`, + e + ) }, }) }) @@ -179,13 +182,16 @@ export class UsersAndGroupsComponent this.groupsService.delete(group).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted group`) + this.toastService.showInfo($localize`Deleted group "${group.name}"`) this.groupsService.listAll().subscribe((r) => { this.groups = r.results }) }, error: (e) => { - this.toastService.showError($localize`Error deleting group.`, e) + this.toastService.showError( + $localize`Error deleting group "${group.name}".`, + e + ) }, }) }) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index 442f9f366..b3d515274 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -30,12 +30,13 @@
    + diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.scss b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.scss new file mode 100644 index 000000000..2332e710d --- /dev/null +++ b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.scss @@ -0,0 +1,22 @@ +.dropdown-menu { + width: var(--pngx-toast-max-width); +} + +.dropdown-menu .scroll-list { + max-height: 500px; + overflow-y: auto; +} + +.dropdown-toggle::after { + display: none; +} + +.dropdown-item { + white-space: initial; +} + +@media screen and (max-width: 400px) { + :host ::ng-deep .dropdown-menu-end { + right: -3rem; + } +} diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts new file mode 100644 index 000000000..33b948f30 --- /dev/null +++ b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.spec.ts @@ -0,0 +1,112 @@ +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { provideHttpClientTesting } from '@angular/common/http/testing' +import { + ComponentFixture, + TestBed, + discardPeriodicTasks, + fakeAsync, + flush, +} from '@angular/core/testing' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { Subject } from 'rxjs' +import { Toast, ToastService } from 'src/app/services/toast.service' +import { ToastsDropdownComponent } from './toasts-dropdown.component' + +const toasts = [ + { + id: 'abc-123', + content: 'foo bar', + delay: 5000, + }, + { + id: 'def-123', + content: 'Error 1 content', + delay: 5000, + error: 'Error 1 string', + }, + { + id: 'ghi-123', + content: 'Error 2 content', + delay: 5000, + error: { + url: 'https://example.com', + status: 500, + statusText: 'Internal Server Error', + message: 'Internal server error 500 message', + error: { detail: 'Error 2 message details' }, + }, + }, +] + +describe('ToastsDropdownComponent', () => { + let component: ToastsDropdownComponent + let fixture: ComponentFixture + let toastService: ToastService + let toastsSubject: Subject = new Subject() + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [ + ToastsDropdownComponent, + NgxBootstrapIconsModule.pick(allIcons), + ], + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }).compileComponents() + + fixture = TestBed.createComponent(ToastsDropdownComponent) + toastService = TestBed.inject(ToastService) + jest.spyOn(toastService, 'getToasts').mockReturnValue(toastsSubject) + + component = fixture.componentInstance + + fixture.detectChanges() + }) + + it('should call getToasts and return toasts', fakeAsync(() => { + const spy = jest.spyOn(toastService, 'getToasts') + + component.ngOnInit() + toastsSubject.next(toasts) + fixture.detectChanges() + + expect(spy).toHaveBeenCalled() + expect(component.toasts).toContainEqual({ + id: 'abc-123', + content: 'foo bar', + delay: 5000, + }) + + component.ngOnDestroy() + flush() + discardPeriodicTasks() + })) + + it('should show a toast', fakeAsync(() => { + component.ngOnInit() + toastsSubject.next(toasts) + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toContain('foo bar') + + component.ngOnDestroy() + flush() + discardPeriodicTasks() + })) + + it('should toggle suppressPopupToasts', fakeAsync((finish) => { + component.ngOnInit() + fixture.detectChanges() + toastsSubject.next(toasts) + + const spy = jest.spyOn(toastService, 'suppressPopupToasts', 'set') + component.onOpenChange(true) + expect(spy).toHaveBeenCalledWith(true) + + component.ngOnDestroy() + flush() + discardPeriodicTasks() + })) +}) diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.ts b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.ts new file mode 100644 index 000000000..c04d758af --- /dev/null +++ b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.ts @@ -0,0 +1,42 @@ +import { Component, OnDestroy, OnInit } from '@angular/core' +import { + NgbDropdownModule, + NgbProgressbarModule, +} from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subscription } from 'rxjs' +import { Toast, ToastService } from 'src/app/services/toast.service' +import { ToastComponent } from '../../common/toast/toast.component' + +@Component({ + selector: 'pngx-toasts-dropdown', + templateUrl: './toasts-dropdown.component.html', + styleUrls: ['./toasts-dropdown.component.scss'], + imports: [ + ToastComponent, + NgbDropdownModule, + NgbProgressbarModule, + NgxBootstrapIconsModule, + ], +}) +export class ToastsDropdownComponent implements OnInit, OnDestroy { + constructor(public toastService: ToastService) {} + + private subscription: Subscription + + public toasts: Toast[] = [] + + ngOnDestroy(): void { + this.subscription?.unsubscribe() + } + + ngOnInit(): void { + this.subscription = this.toastService.getToasts().subscribe((toasts) => { + this.toasts = [...toasts] + }) + } + + onOpenChange(open: boolean): void { + this.toastService.suppressPopupToasts = open + } +} diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html index 2f119b074..742dd8e8a 100644 --- a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html @@ -29,10 +29,17 @@ + #d="ngbDatepicker" + [footerTemplate]="datePickerFooterTemplate" /> + +
    + + +
    +
    } @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Float || getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Integer) { } @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Boolean) { diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.scss b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.scss index f38bb4002..2f9e2c45e 100644 --- a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.scss +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.scss @@ -41,3 +41,9 @@ min-width: 140px; } } + +.btn-group-xs { + > .btn { + border-radius: 0.15rem; + } +} diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts index 7afb5fc1c..4dcbceb13 100644 --- a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts @@ -113,6 +113,9 @@ describe('CustomFieldsQueryDropdownComponent', () => { ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[ CustomFieldQueryOperatorGroups.Basic ], + ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[ + CustomFieldQueryOperatorGroups.Exact + ], ...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[ CustomFieldQueryOperatorGroups.String ], diff --git a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts index 0fa7fe536..b31ba6c09 100644 --- a/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts +++ b/src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts @@ -37,6 +37,7 @@ import { import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options' import { LoadingComponentWithPermissions } from '../../loading-component/loading.component' import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component' +import { DocumentLinkComponent } from '../input/document-link/document-link.component' export class CustomFieldQueriesModel { public queries: CustomFieldQueryElement[] = [] @@ -167,6 +168,7 @@ export class CustomFieldQueriesModel { imports: [ ClearableBadgeComponent, FormsModule, + DocumentLinkComponent, ReactiveFormsModule, NgbDatepickerModule, NgTemplateOutlet, @@ -241,6 +243,8 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm customFields: CustomField[] = [] + public readonly today: string = new Date().toISOString().split('T')[0] + constructor(protected customFieldsService: CustomFieldsService) { super() this.selectionModel = new CustomFieldQueriesModel() diff --git a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html index dcab4606d..c3ff61ba8 100644 --- a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html +++ b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.html @@ -1,5 +1,5 @@
    - + +
    + + +
    +
@@ -95,40 +107,52 @@ diff --git a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.scss b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.scss index e101a131d..ebd34b29a 100644 --- a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.scss +++ b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.scss @@ -41,3 +41,9 @@ } } } + +.btn-group-xs { + > .btn { + border-radius: 0.15rem; + } +} diff --git a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.spec.ts b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.spec.ts index 10762264a..1f6ee909e 100644 --- a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.spec.ts @@ -61,7 +61,7 @@ describe('DatesDropdownComponent', () => { it('should support date input, emit change', fakeAsync(() => { let result: string - component.createdDateAfterChange.subscribe((date) => (result = date)) + component.createdDateFromChange.subscribe((date) => (result = date)) const input: HTMLInputElement = fixture.nativeElement.querySelector('input') input.value = '5/30/2023' input.dispatchEvent(new Event('change')) @@ -83,68 +83,68 @@ describe('DatesDropdownComponent', () => { let result: DateSelection component.datesSet.subscribe((date) => (result = date)) component.setCreatedRelativeDate(null) - component.setCreatedRelativeDate(RelativeDate.LAST_7_DAYS) + component.setCreatedRelativeDate(RelativeDate.WITHIN_1_WEEK) component.setAddedRelativeDate(null) - component.setAddedRelativeDate(RelativeDate.LAST_7_DAYS) + component.setAddedRelativeDate(RelativeDate.WITHIN_1_WEEK) tick(500) expect(result).toEqual({ - createdAfter: null, - createdBefore: null, - createdRelativeDateID: RelativeDate.LAST_7_DAYS, - addedAfter: null, - addedBefore: null, - addedRelativeDateID: RelativeDate.LAST_7_DAYS, + createdFrom: null, + createdTo: null, + createdRelativeDateID: RelativeDate.WITHIN_1_WEEK, + addedFrom: null, + addedTo: null, + addedRelativeDateID: RelativeDate.WITHIN_1_WEEK, }) })) it('should support report if active', () => { - component.createdRelativeDate = RelativeDate.LAST_7_DAYS + component.createdRelativeDate = RelativeDate.WITHIN_1_WEEK expect(component.isActive).toBeTruthy() component.createdRelativeDate = null - component.createdDateAfter = '2023-05-30' + component.createdDateFrom = '2023-05-30' expect(component.isActive).toBeTruthy() - component.createdDateAfter = null - component.createdDateBefore = '2023-05-30' + component.createdDateFrom = null + component.createdDateTo = '2023-05-30' expect(component.isActive).toBeTruthy() - component.createdDateBefore = null + component.createdDateTo = null - component.addedRelativeDate = RelativeDate.LAST_7_DAYS + component.addedRelativeDate = RelativeDate.WITHIN_1_WEEK expect(component.isActive).toBeTruthy() component.addedRelativeDate = null - component.addedDateAfter = '2023-05-30' + component.addedDateFrom = '2023-05-30' expect(component.isActive).toBeTruthy() - component.addedDateAfter = null - component.addedDateBefore = '2023-05-30' + component.addedDateFrom = null + component.addedDateTo = '2023-05-30' expect(component.isActive).toBeTruthy() - component.addedDateBefore = null + component.addedDateTo = null expect(component.isActive).toBeFalsy() }) it('should support reset', () => { - component.createdDateAfter = '2023-05-30' + component.createdDateFrom = '2023-05-30' component.reset() - expect(component.createdDateAfter).toBeNull() + expect(component.createdDateFrom).toBeNull() }) - it('should support clearAfter', () => { - component.createdDateAfter = '2023-05-30' - component.clearCreatedAfter() - expect(component.createdDateAfter).toBeNull() + it('should support clearFrom', () => { + component.createdDateFrom = '2023-05-30' + component.clearCreatedFrom() + expect(component.createdDateFrom).toBeNull() - component.addedDateAfter = '2023-05-30' - component.clearAddedAfter() - expect(component.addedDateAfter).toBeNull() + component.addedDateFrom = '2023-05-30' + component.clearAddedFrom() + expect(component.addedDateFrom).toBeNull() }) - it('should support clearBefore', () => { - component.createdDateBefore = '2023-05-30' - component.clearCreatedBefore() - expect(component.createdDateBefore).toBeNull() + it('should support clearTo', () => { + component.createdDateTo = '2023-05-30' + component.clearCreatedTo() + expect(component.createdDateTo).toBeNull() - component.addedDateBefore = '2023-05-30' - component.clearAddedBefore() - expect(component.addedDateBefore).toBeNull() + component.addedDateTo = '2023-05-30' + component.clearAddedTo() + expect(component.addedDateTo).toBeNull() }) it('should limit keyboard events', () => { diff --git a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.ts b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.ts index 356ba510a..e7d506d18 100644 --- a/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.ts +++ b/src-ui/src/app/components/common/dates-dropdown/dates-dropdown.component.ts @@ -23,19 +23,19 @@ import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-optio import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component' export interface DateSelection { - createdBefore?: string - createdAfter?: string + createdTo?: string + createdFrom?: string createdRelativeDateID?: number - addedBefore?: string - addedAfter?: string + addedTo?: string + addedFrom?: string addedRelativeDateID?: number } export enum RelativeDate { - LAST_7_DAYS = 0, - LAST_MONTH = 1, - LAST_3_MONTHS = 2, - LAST_YEAR = 3, + WITHIN_1_WEEK = 0, + WITHIN_1_MONTH = 1, + WITHIN_3_MONTHS = 2, + WITHIN_1_YEAR = 3, } @Component({ @@ -63,23 +63,23 @@ export class DatesDropdownComponent implements OnInit, OnDestroy { relativeDates = [ { - id: RelativeDate.LAST_7_DAYS, - name: $localize`Last 7 days`, + id: RelativeDate.WITHIN_1_WEEK, + name: $localize`Within 1 week`, date: new Date().setDate(new Date().getDate() - 7), }, { - id: RelativeDate.LAST_MONTH, - name: $localize`Last month`, + id: RelativeDate.WITHIN_1_MONTH, + name: $localize`Within 1 month`, date: new Date().setMonth(new Date().getMonth() - 1), }, { - id: RelativeDate.LAST_3_MONTHS, - name: $localize`Last 3 months`, + id: RelativeDate.WITHIN_3_MONTHS, + name: $localize`Within 3 months`, date: new Date().setMonth(new Date().getMonth() - 3), }, { - id: RelativeDate.LAST_YEAR, - name: $localize`Last year`, + id: RelativeDate.WITHIN_1_YEAR, + name: $localize`Within 1 year`, date: new Date().setFullYear(new Date().getFullYear() - 1), }, ] @@ -88,16 +88,16 @@ export class DatesDropdownComponent implements OnInit, OnDestroy { // created @Input() - createdDateBefore: string + createdDateTo: string @Output() - createdDateBeforeChange = new EventEmitter() + createdDateToChange = new EventEmitter() @Input() - createdDateAfter: string + createdDateFrom: string @Output() - createdDateAfterChange = new EventEmitter() + createdDateFromChange = new EventEmitter() @Input() createdRelativeDate: RelativeDate @@ -107,16 +107,16 @@ export class DatesDropdownComponent implements OnInit, OnDestroy { // added @Input() - addedDateBefore: string + addedDateTo: string @Output() - addedDateBeforeChange = new EventEmitter() + addedDateToChange = new EventEmitter() @Input() - addedDateAfter: string + addedDateFrom: string @Output() - addedDateAfterChange = new EventEmitter() + addedDateFromChange = new EventEmitter() @Input() addedRelativeDate: RelativeDate @@ -133,14 +133,16 @@ export class DatesDropdownComponent implements OnInit, OnDestroy { @Input() disabled: boolean = false + public readonly today: string = new Date().toISOString().split('T')[0] + get isActive(): boolean { return ( this.createdRelativeDate !== null || - this.createdDateAfter?.length > 0 || - this.createdDateBefore?.length > 0 || + this.createdDateFrom?.length > 0 || + this.createdDateTo?.length > 0 || this.addedRelativeDate !== null || - this.addedDateAfter?.length > 0 || - this.addedDateBefore?.length > 0 + this.addedDateFrom?.length > 0 || + this.addedDateTo?.length > 0 ) } @@ -161,42 +163,42 @@ export class DatesDropdownComponent implements OnInit, OnDestroy { } reset() { - this.createdDateBefore = null - this.createdDateAfter = null + this.createdDateTo = null + this.createdDateFrom = null this.createdRelativeDate = null - this.addedDateBefore = null - this.addedDateAfter = null + this.addedDateTo = null + this.addedDateFrom = null this.addedRelativeDate = null this.onChange() } setCreatedRelativeDate(rd: RelativeDate) { - this.createdDateBefore = null - this.createdDateAfter = null + this.createdDateTo = null + this.createdDateFrom = null this.createdRelativeDate = this.createdRelativeDate == rd ? null : rd this.onChange() } setAddedRelativeDate(rd: RelativeDate) { - this.addedDateBefore = null - this.addedDateAfter = null + this.addedDateTo = null + this.addedDateFrom = null this.addedRelativeDate = this.addedRelativeDate == rd ? null : rd this.onChange() } onChange() { - this.createdDateBeforeChange.emit(this.createdDateBefore) - this.createdDateAfterChange.emit(this.createdDateAfter) + this.createdDateToChange.emit(this.createdDateTo) + this.createdDateFromChange.emit(this.createdDateFrom) this.createdRelativeDateChange.emit(this.createdRelativeDate) - this.addedDateBeforeChange.emit(this.addedDateBefore) - this.addedDateAfterChange.emit(this.addedDateAfter) + this.addedDateToChange.emit(this.addedDateTo) + this.addedDateFromChange.emit(this.addedDateFrom) this.addedRelativeDateChange.emit(this.addedRelativeDate) this.datesSet.emit({ - createdAfter: this.createdDateAfter, - createdBefore: this.createdDateBefore, + createdFrom: this.createdDateFrom, + createdTo: this.createdDateTo, createdRelativeDateID: this.createdRelativeDate, - addedAfter: this.addedDateAfter, - addedBefore: this.addedDateBefore, + addedFrom: this.addedDateFrom, + addedTo: this.addedDateTo, addedRelativeDateID: this.addedRelativeDate, }) } @@ -205,30 +207,30 @@ export class DatesDropdownComponent implements OnInit, OnDestroy { this.createdRelativeDate = null this.addedRelativeDate = null this.datesSetDebounce$.next({ - createdAfter: this.createdDateAfter, - createdBefore: this.createdDateBefore, - addedAfter: this.addedDateAfter, - addedBefore: this.addedDateBefore, + createdAfter: this.createdDateFrom, + createdBefore: this.createdDateTo, + addedAfter: this.addedDateFrom, + addedBefore: this.addedDateTo, }) } - clearCreatedBefore() { - this.createdDateBefore = null + clearCreatedTo() { + this.createdDateTo = null this.onChange() } - clearCreatedAfter() { - this.createdDateAfter = null + clearCreatedFrom() { + this.createdDateFrom = null this.onChange() } - clearAddedBefore() { - this.addedDateBefore = null + clearAddedTo() { + this.addedDateTo = null this.onChange() } - clearAddedAfter() { - this.addedDateAfter = null + clearAddedFrom() { + this.addedDateFrom = null this.onChange() } diff --git a/src-ui/src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html index a9ad3040b..afe6c2ab9 100644 --- a/src-ui/src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html @@ -41,6 +41,7 @@
+
diff --git a/src-ui/src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts index f2d8236bc..3d4924c0b 100644 --- a/src-ui/src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts @@ -18,6 +18,7 @@ import { MailMetadataTitleOption, MailRule, MailRuleConsumptionScope, + MailRulePdfLayout, } from 'src/app/data/mail-rule' import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service' @@ -58,6 +59,29 @@ const CONSUMPTION_SCOPE_OPTIONS = [ }, ] +const PDF_LAYOUT_OPTIONS = [ + { + id: MailRulePdfLayout.Default, + name: $localize`System default`, + }, + { + id: MailRulePdfLayout.TextHtml, + name: $localize`Text, then HTML`, + }, + { + id: MailRulePdfLayout.HtmlText, + name: $localize`HTML, then text`, + }, + { + id: MailRulePdfLayout.HtmlOnly, + name: $localize`HTML only`, + }, + { + id: MailRulePdfLayout.TextOnly, + name: $localize`Text only`, + }, +] + const ACTION_OPTIONS = [ { id: MailAction.Delete, @@ -184,6 +208,7 @@ export class MailRuleEditDialogComponent extends EditDialogComponent { filter_attachment_filename_exclude: new FormControl(null), maximum_age: new FormControl(null), attachment_type: new FormControl(MailFilterAttachmentType.Attachments), + pdf_layout: new FormControl(MailRulePdfLayout.Default), consumption_scope: new FormControl(MailRuleConsumptionScope.Attachments), order: new FormControl(null), action: new FormControl(MailAction.MarkRead), @@ -232,4 +257,8 @@ export class MailRuleEditDialogComponent extends EditDialogComponent { get consumptionScopeOptions() { return CONSUMPTION_SCOPE_OPTIONS } + + get pdfLayoutOptions() { + return PDF_LAYOUT_OPTIONS + } } diff --git a/src-ui/src/app/components/common/input/date/date.component.html b/src-ui/src/app/components/common/input/date/date.component.html index 8f386e2c8..3221677fc 100644 --- a/src-ui/src/app/components/common/input/date/date.component.html +++ b/src-ui/src/app/components/common/input/date/date.component.html @@ -12,10 +12,16 @@
+ name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled" [footerTemplate]="datePickerFooterTemplate"> + +
+ + +
+
@if (showFilter) { +
+
+
+ + } + @if (toast.action) { +

+ } + + + + diff --git a/src-ui/src/app/components/common/toast/toast.component.scss b/src-ui/src/app/components/common/toast/toast.component.scss new file mode 100644 index 000000000..3783445de --- /dev/null +++ b/src-ui/src/app/components/common/toast/toast.component.scss @@ -0,0 +1,20 @@ +::ng-deep .toast-body { + position: relative; +} + +::ng-deep .toast.error { + border-color: hsla(350, 79%, 40%, 0.4); // bg-danger +} + +::ng-deep .toast.error .toast-body { + background-color: hsla(350, 79%, 40%, 0.8); // bg-danger + border-top-left-radius: inherit; + border-top-right-radius: inherit; + border-bottom-left-radius: inherit; + border-bottom-right-radius: inherit; +} + +.progress { + background-color: var(--pngx-primary); + opacity: .07; +} diff --git a/src-ui/src/app/components/common/toast/toast.component.spec.ts b/src-ui/src/app/components/common/toast/toast.component.spec.ts new file mode 100644 index 000000000..c5d52a28f --- /dev/null +++ b/src-ui/src/app/components/common/toast/toast.component.spec.ts @@ -0,0 +1,104 @@ +import { + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + flush, + TestBed, + tick, +} from '@angular/core/testing' + +import { Clipboard } from '@angular/cdk/clipboard' +import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { ToastComponent } from './toast.component' + +const toast1 = { + content: 'Error 1 content', + delay: 5000, + error: 'Error 1 string', +} + +const toast2 = { + content: 'Error 2 content', + delay: 5000, + error: { + url: 'https://example.com', + status: 500, + statusText: 'Internal Server Error', + message: 'Internal server error 500 message', + error: { detail: 'Error 2 message details' }, + }, +} + +describe('ToastComponent', () => { + let component: ToastComponent + let fixture: ComponentFixture + let clipboard: Clipboard + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ToastComponent, NgxBootstrapIconsModule.pick(allIcons)], + }).compileComponents() + + fixture = TestBed.createComponent(ToastComponent) + clipboard = TestBed.inject(Clipboard) + component = fixture.componentInstance + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should countdown toast', fakeAsync(() => { + component.toast = toast2 + fixture.detectChanges() + component.onShown(toast2) + tick(5000) + expect(component.toast.delayRemaining).toEqual(0) + flush() + discardPeriodicTasks() + })) + + it('should show an error if given with toast', fakeAsync(() => { + component.toast = toast1 + fixture.detectChanges() + + expect(fixture.nativeElement.querySelector('details')).not.toBeNull() + expect(fixture.nativeElement.textContent).toContain('Error 1 content') + + flush() + discardPeriodicTasks() + })) + + it('should show error details, support copy', fakeAsync(() => { + component.toast = toast2 + fixture.detectChanges() + + expect(fixture.nativeElement.querySelector('details')).not.toBeNull() + expect(fixture.nativeElement.textContent).toContain( + 'Error 2 message details' + ) + + const copySpy = jest.spyOn(clipboard, 'copy') + component.copyError(toast2.error) + expect(copySpy).toHaveBeenCalled() + + flush() + discardPeriodicTasks() + })) + + it('should parse error text, add ellipsis', () => { + expect(component.getErrorText(toast2.error)).toEqual( + 'Error 2 message details' + ) + expect(component.getErrorText({ error: 'Error string no detail' })).toEqual( + 'Error string no detail' + ) + expect(component.getErrorText('Error string')).toEqual('') + expect( + component.getErrorText({ error: { message: 'foo error bar' } }) + ).toContain('{"message":"foo error bar"}') + expect( + component.getErrorText({ error: new Array(205).join('a') }) + ).toContain('...') + }) +}) diff --git a/src-ui/src/app/components/common/toast/toast.component.ts b/src-ui/src/app/components/common/toast/toast.component.ts new file mode 100644 index 000000000..5ebfdbe82 --- /dev/null +++ b/src-ui/src/app/components/common/toast/toast.component.ts @@ -0,0 +1,76 @@ +import { Clipboard } from '@angular/cdk/clipboard' +import { DecimalPipe } from '@angular/common' +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { + NgbProgressbarModule, + NgbToastModule, +} from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { interval, take } from 'rxjs' +import { Toast } from 'src/app/services/toast.service' + +@Component({ + selector: 'pngx-toast', + imports: [ + DecimalPipe, + NgbToastModule, + NgbProgressbarModule, + NgxBootstrapIconsModule, + ], + templateUrl: './toast.component.html', + styleUrl: './toast.component.scss', +}) +export class ToastComponent { + @Input() toast: Toast + + @Input() autohide: boolean = true + + @Output() hidden: EventEmitter = new EventEmitter() + + @Output() close: EventEmitter = new EventEmitter() + + public copied: boolean = false + + constructor(private clipboard: Clipboard) {} + + onShown(toast: Toast) { + if (!this.autohide) return + + const refreshInterval = 150 + const delay = toast.delay - 500 // for fade animation + + interval(refreshInterval) + .pipe(take(Math.round(delay / refreshInterval))) + .subscribe((count) => { + toast.delayRemaining = Math.max( + 0, + delay - refreshInterval * (count + 1) + ) + }) + } + + public isDetailedError(error: any): boolean { + return ( + typeof error === 'object' && + 'status' in error && + 'statusText' in error && + 'url' in error && + 'message' in error && + 'error' in error + ) + } + + public copyError(error: any) { + this.clipboard.copy(JSON.stringify(error)) + this.copied = true + setTimeout(() => { + this.copied = false + }, 3000) + } + + getErrorText(error: any) { + let text: string = error.error?.detail ?? error.error ?? '' + if (typeof text === 'object') text = JSON.stringify(text) + return `${text.slice(0, 200)}${text.length > 200 ? '...' : ''}` + } +} diff --git a/src-ui/src/app/components/common/toasts/toasts.component.html b/src-ui/src/app/components/common/toasts/toasts.component.html index 36623161b..2178a2023 100644 --- a/src-ui/src/app/components/common/toasts/toasts.component.html +++ b/src-ui/src/app/components/common/toasts/toasts.component.html @@ -1,55 +1,3 @@ -@for (toast of toasts; track toast) { - - - {{ toast.delayRemaining / 1000 | number: '1.0-0' }} seconds -
- @if (!toast.error) { - - } - @if (toast.error) { - - } -
-

{{toast.content}}

- @if (toast.error) { -
-
- @if (isDetailedError(toast.error)) { -
-
URL
-
{{ toast.error.url }}
-
Status
-
{{ toast.error.status }} {{ toast.error.statusText }}
-
Error
-
{{ getErrorText(toast.error) }}
-
- } -
-
- -
-
-
-
- } - @if (toast.action) { -

- } -
- -
-
+@for (toast of toasts; track toast.id) { + } diff --git a/src-ui/src/app/components/common/toasts/toasts.component.scss b/src-ui/src/app/components/common/toasts/toasts.component.scss index 463f96495..e0a069dda 100644 --- a/src-ui/src/app/components/common/toasts/toasts.component.scss +++ b/src-ui/src/app/components/common/toasts/toasts.component.scss @@ -1,7 +1,7 @@ :host { position: fixed; top: 0; - right: 0; + right: calc(50% - (var(--pngx-toast-max-width) / 2)); margin: 0.3em; z-index: 1200; } @@ -9,24 +9,3 @@ .toast:not(.show) { display: block; // this corrects an ng-bootstrap bug that prevented animations } - -::ng-deep .toast-body { - position: relative; -} - -::ng-deep .toast.error { - border-color: hsla(350, 79%, 40%, 0.4); // bg-danger -} - -::ng-deep .toast.error .toast-body { - background-color: hsla(350, 79%, 40%, 0.8); // bg-danger - border-top-left-radius: inherit; - border-top-right-radius: inherit; - border-bottom-left-radius: inherit; - border-bottom-right-radius: inherit; -} - -.progress { - background-color: var(--pngx-primary); - opacity: .07; -} diff --git a/src-ui/src/app/components/common/toasts/toasts.component.spec.ts b/src-ui/src/app/components/common/toasts/toasts.component.spec.ts index 449396134..bbea04c9c 100644 --- a/src-ui/src/app/components/common/toasts/toasts.component.spec.ts +++ b/src-ui/src/app/components/common/toasts/toasts.component.spec.ts @@ -1,58 +1,33 @@ -import { Clipboard } from '@angular/cdk/clipboard' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { provideHttpClientTesting } from '@angular/common/http/testing' -import { - ComponentFixture, - TestBed, - discardPeriodicTasks, - fakeAsync, - flush, - tick, -} from '@angular/core/testing' +import { ComponentFixture, TestBed } from '@angular/core/testing' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' -import { of } from 'rxjs' -import { ToastService } from 'src/app/services/toast.service' +import { Subject } from 'rxjs' +import { Toast, ToastService } from 'src/app/services/toast.service' import { ToastsComponent } from './toasts.component' -const toasts = [ - { - content: 'foo bar', - delay: 5000, +const toast = { + content: 'Error 2 content', + delay: 5000, + error: { + url: 'https://example.com', + status: 500, + statusText: 'Internal Server Error', + message: 'Internal server error 500 message', + error: { detail: 'Error 2 message details' }, }, - { - content: 'Error 1 content', - delay: 5000, - error: 'Error 1 string', - }, - { - content: 'Error 2 content', - delay: 5000, - error: { - url: 'https://example.com', - status: 500, - statusText: 'Internal Server Error', - message: 'Internal server error 500 message', - error: { detail: 'Error 2 message details' }, - }, - }, -] +} describe('ToastsComponent', () => { let component: ToastsComponent let fixture: ComponentFixture let toastService: ToastService - let clipboard: Clipboard + let toastSubject: Subject = new Subject() beforeEach(async () => { TestBed.configureTestingModule({ imports: [ToastsComponent, NgxBootstrapIconsModule.pick(allIcons)], providers: [ - { - provide: ToastService, - useValue: { - getToasts: () => of(toasts), - }, - }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), ], @@ -60,95 +35,37 @@ describe('ToastsComponent', () => { fixture = TestBed.createComponent(ToastsComponent) toastService = TestBed.inject(ToastService) - clipboard = TestBed.inject(Clipboard) + jest.replaceProperty(toastService, 'showToast', toastSubject) component = fixture.componentInstance fixture.detectChanges() }) - it('should call getToasts and return toasts', fakeAsync(() => { - const spy = jest.spyOn(toastService, 'getToasts') + it('should create', () => { + expect(component).toBeTruthy() + }) - component.ngOnInit() - fixture.detectChanges() + it('should close toast', () => { + component.toasts = [toast] + const closeToastSpy = jest.spyOn(toastService, 'closeToast') + component.closeToast() + expect(component.toasts).toEqual([]) + expect(closeToastSpy).toHaveBeenCalledWith(toast) + }) - expect(spy).toHaveBeenCalled() - expect(component.toasts).toContainEqual({ - content: 'foo bar', - delay: 5000, - }) - - component.ngOnDestroy() - flush() - discardPeriodicTasks() - })) - - it('should show a toast', fakeAsync(() => { - component.ngOnInit() - fixture.detectChanges() - - expect(fixture.nativeElement.textContent).toContain('foo bar') - - component.ngOnDestroy() - flush() - discardPeriodicTasks() - })) - - it('should countdown toast', fakeAsync(() => { - component.ngOnInit() - fixture.detectChanges() - component.onShow(toasts[0]) - tick(5000) - expect(component.toasts[0].delayRemaining).toEqual(0) - component.ngOnDestroy() - flush() - discardPeriodicTasks() - })) - - it('should show an error if given with toast', fakeAsync(() => { - component.ngOnInit() - fixture.detectChanges() - - expect(fixture.nativeElement.querySelector('details')).not.toBeNull() - expect(fixture.nativeElement.textContent).toContain('Error 1 content') - - component.ngOnDestroy() - flush() - discardPeriodicTasks() - })) - - it('should show error details, support copy', fakeAsync(() => { - component.ngOnInit() - fixture.detectChanges() - - expect(fixture.nativeElement.querySelector('details')).not.toBeNull() - expect(fixture.nativeElement.textContent).toContain( - 'Error 2 message details' + it('should unsubscribe', () => { + const unsubscribeSpy = jest.spyOn( + (component as any).subscription, + 'unsubscribe' ) - - const copySpy = jest.spyOn(clipboard, 'copy') - component.copyError(toasts[2].error) - expect(copySpy).toHaveBeenCalled() - component.ngOnDestroy() - flush() - discardPeriodicTasks() - })) + expect(unsubscribeSpy).toHaveBeenCalled() + }) - it('should parse error text, add ellipsis', () => { - expect(component.getErrorText(toasts[2].error)).toEqual( - 'Error 2 message details' - ) - expect(component.getErrorText({ error: 'Error string no detail' })).toEqual( - 'Error string no detail' - ) - expect(component.getErrorText('Error string')).toEqual('') - expect( - component.getErrorText({ error: { message: 'foo error bar' } }) - ).toContain('{"message":"foo error bar"}') - expect( - component.getErrorText({ error: new Array(205).join('a') }) - ).toContain('...') + it('should subscribe to toastService', () => { + component.ngOnInit() + toastSubject.next(toast) + expect(component.toasts).toEqual([toast]) }) }) diff --git a/src-ui/src/app/components/common/toasts/toasts.component.ts b/src-ui/src/app/components/common/toasts/toasts.component.ts index bb791de11..53b6e1895 100644 --- a/src-ui/src/app/components/common/toasts/toasts.component.ts +++ b/src-ui/src/app/components/common/toasts/toasts.component.ts @@ -1,92 +1,43 @@ -import { Clipboard } from '@angular/cdk/clipboard' -import { DecimalPipe } from '@angular/common' import { Component, OnDestroy, OnInit } from '@angular/core' import { + NgbAccordionModule, NgbProgressbarModule, - NgbToastModule, } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' -import { Subscription, interval, take } from 'rxjs' +import { Subscription } from 'rxjs' import { Toast, ToastService } from 'src/app/services/toast.service' +import { ToastComponent } from '../toast/toast.component' @Component({ selector: 'pngx-toasts', templateUrl: './toasts.component.html', styleUrls: ['./toasts.component.scss'], imports: [ - DecimalPipe, - NgbToastModule, + ToastComponent, + NgbAccordionModule, NgbProgressbarModule, NgxBootstrapIconsModule, ], }) export class ToastsComponent implements OnInit, OnDestroy { - constructor( - public toastService: ToastService, - private clipboard: Clipboard - ) {} + constructor(public toastService: ToastService) {} private subscription: Subscription - public toasts: Toast[] = [] - - public copied: boolean = false - - public seconds: number = 0 + public toasts: Toast[] = [] // array to force change detection ngOnDestroy(): void { this.subscription?.unsubscribe() } ngOnInit(): void { - this.subscription = this.toastService.getToasts().subscribe((toasts) => { - this.toasts = toasts - this.toasts.forEach((t) => { - if (typeof t.error === 'string') { - try { - t.error = JSON.parse(t.error) - } catch (e) {} - } - }) + this.subscription = this.toastService.showToast.subscribe((toast) => { + this.toasts = toast ? [toast] : [] }) } - onShow(toast: Toast) { - const refreshInterval = 150 - const delay = toast.delay - 500 // for fade animation - - interval(refreshInterval) - .pipe(take(delay / refreshInterval)) - .subscribe((count) => { - toast.delayRemaining = Math.max( - 0, - delay - refreshInterval * (count + 1) - ) - }) - } - - public isDetailedError(error: any): boolean { - return ( - typeof error === 'object' && - 'status' in error && - 'statusText' in error && - 'url' in error && - 'message' in error && - 'error' in error - ) - } - - public copyError(error: any) { - this.clipboard.copy(JSON.stringify(error)) - this.copied = true - setTimeout(() => { - this.copied = false - }, 3000) - } - - getErrorText(error: any) { - let text: string = error.error?.detail ?? error.error ?? '' - if (typeof text === 'object') text = JSON.stringify(text) - return `${text.slice(0, 200)}${text.length > 200 ? '...' : ''}` + closeToast() { + this.toastService.closeToast(this.toasts[0]) + this.toasts = [] } } diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts index 5f66c68d6..621a90491 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts @@ -33,14 +33,14 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' -import { - ConsumerStatusService, - FileStatus, -} from 'src/app/services/consumer-status.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { PermissionsService } from 'src/app/services/permissions.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { DocumentService } from 'src/app/services/rest/document.service' +import { + FileStatus, + WebsocketStatusService, +} from 'src/app/services/websocket-status.service' import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' import { SavedViewWidgetComponent } from './saved-view-widget.component' @@ -112,7 +112,7 @@ describe('SavedViewWidgetComponent', () => { let component: SavedViewWidgetComponent let fixture: ComponentFixture let documentService: DocumentService - let consumerStatusService: ConsumerStatusService + let websocketStatusService: WebsocketStatusService let documentListViewService: DocumentListViewService let router: Router @@ -176,7 +176,7 @@ describe('SavedViewWidgetComponent', () => { }).compileComponents() documentService = TestBed.inject(DocumentService) - consumerStatusService = TestBed.inject(ConsumerStatusService) + websocketStatusService = TestBed.inject(WebsocketStatusService) documentListViewService = TestBed.inject(DocumentListViewService) router = TestBed.inject(Router) fixture = TestBed.createComponent(SavedViewWidgetComponent) @@ -235,7 +235,7 @@ describe('SavedViewWidgetComponent', () => { it('should reload on document consumption finished', () => { const fileStatusSubject = new Subject() jest - .spyOn(consumerStatusService, 'onDocumentConsumptionFinished') + .spyOn(websocketStatusService, 'onDocumentConsumptionFinished') .mockReturnValue(fileStatusSubject) const reloadSpy = jest.spyOn(component, 'reload') component.ngOnInit() diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts index 7f6c5755b..32bf7a004 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts @@ -42,7 +42,6 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe' import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe' import { UsernamePipe } from 'src/app/pipes/username.pipe' -import { ConsumerStatusService } from 'src/app/services/consumer-status.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { @@ -53,6 +52,7 @@ import { import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { DocumentService } from 'src/app/services/rest/document.service' import { SettingsService } from 'src/app/services/settings.service' +import { WebsocketStatusService } from 'src/app/services/websocket-status.service' import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' @Component({ @@ -94,7 +94,7 @@ export class SavedViewWidgetComponent private documentService: DocumentService, private router: Router, private list: DocumentListViewService, - private consumerStatusService: ConsumerStatusService, + private websocketStatusService: WebsocketStatusService, public openDocumentsService: OpenDocumentsService, public documentListViewService: DocumentListViewService, public permissionsService: PermissionsService, @@ -124,7 +124,7 @@ export class SavedViewWidgetComponent ngOnInit(): void { this.reload() this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE - this.consumerStatusService + this.websocketStatusService .onDocumentConsumptionFinished() .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe(() => { diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html index 718edf4ea..ef2b47b02 100644 --- a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html @@ -56,6 +56,7 @@ [ngbPopover]="getFileTypeName(filetype)" i18n-ngbPopover triggers="mouseenter:mouseleave" + (click)="filterByFileType(filetype)" [attr.aria-label]="getFileTypeName(filetype)" [class.me-1px]="!last" [style.width]="getFileTypePercent(filetype) + '%'" @@ -70,7 +71,7 @@
@for (filetype of statistics?.document_file_type_counts; track filetype; let i = $index) {
-
+
{{ getFileTypeExtension(filetype) }} ({{getFileTypePercent(filetype) | number: '1.0-1'}}%)
diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts index da0c2c083..f5f930190 100644 --- a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts +++ b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts @@ -9,12 +9,14 @@ import { RouterTestingModule } from '@angular/router/testing' import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { Subject } from 'rxjs' import { routes } from 'src/app/app-routing.module' +import { FILTER_MIME_TYPE } from 'src/app/data/filter-rule-type' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { PermissionsGuard } from 'src/app/guards/permissions.guard' +import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { - ConsumerStatusService, FileStatus, -} from 'src/app/services/consumer-status.service' + WebsocketStatusService, +} from 'src/app/services/websocket-status.service' import { environment } from 'src/environments/environment' import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' import { StatisticsWidgetComponent } from './statistics-widget.component' @@ -23,7 +25,8 @@ describe('StatisticsWidgetComponent', () => { let component: StatisticsWidgetComponent let fixture: ComponentFixture let httpTestingController: HttpTestingController - let consumerStatusService: ConsumerStatusService + let websocketStatusService: WebsocketStatusService + let documentListViewService: DocumentListViewService const fileStatusSubject = new Subject() beforeEach(async () => { @@ -44,10 +47,11 @@ describe('StatisticsWidgetComponent', () => { }).compileComponents() fixture = TestBed.createComponent(StatisticsWidgetComponent) - consumerStatusService = TestBed.inject(ConsumerStatusService) + websocketStatusService = TestBed.inject(WebsocketStatusService) jest - .spyOn(consumerStatusService, 'onDocumentConsumptionFinished') + .spyOn(websocketStatusService, 'onDocumentConsumptionFinished') .mockReturnValue(fileStatusSubject) + documentListViewService = TestBed.inject(DocumentListViewService) component = fixture.componentInstance httpTestingController = TestBed.inject(HttpTestingController) @@ -231,4 +235,26 @@ describe('StatisticsWidgetComponent', () => { 'CurrentASN:' ) }) + + it('should support quick filter by mime type', () => { + const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') + component.filterByFileType({ + mime_type: 'application/pdf', + mime_type_count: 160, + }) + expect(qfSpy).toHaveBeenCalledWith([ + { + rule_type: FILTER_MIME_TYPE, + value: 'application/pdf', + }, + ]) + + qfSpy.mockClear() + component.filterByFileType({ + mime_type: 'Other', + mime_type_count: 160, + is_other: true, + }) + expect(qfSpy).not.toHaveBeenCalled() + }) }) diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts index f54852429..95bd4e6ce 100644 --- a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.ts @@ -6,10 +6,13 @@ import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' import * as mimeTypeNames from 'mime-names' import { first, Subject, Subscription, takeUntil } from 'rxjs' import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component' -import { FILTER_HAS_TAGS_ANY } from 'src/app/data/filter-rule-type' +import { + FILTER_HAS_TAGS_ANY, + FILTER_MIME_TYPE, +} from 'src/app/data/filter-rule-type' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' -import { ConsumerStatusService } from 'src/app/services/consumer-status.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service' +import { WebsocketStatusService } from 'src/app/services/websocket-status.service' import { environment } from 'src/environments/environment' import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' @@ -29,6 +32,7 @@ export interface Statistics { interface DocumentFileType { mime_type: string mime_type_count: number + is_other?: boolean } @Component({ @@ -51,7 +55,7 @@ export class StatisticsWidgetComponent constructor( private http: HttpClient, - private consumerStatusService: ConsumerStatusService, + private websocketConnectionService: WebsocketStatusService, private documentListViewService: DocumentListViewService ) { super() @@ -77,6 +81,7 @@ export class StatisticsWidgetComponent statistics.document_file_type_counts.slice(0, fileTypeMax) statistics.document_file_type_counts.push({ mime_type: $localize`Other`, + is_other: true, mime_type_count: others.reduce( (currentValue, documentFileType) => documentFileType.mime_type_count + currentValue, @@ -109,7 +114,7 @@ export class StatisticsWidgetComponent ngOnInit(): void { this.reload() - this.subscription = this.consumerStatusService + this.subscription = this.websocketConnectionService .onDocumentConsumptionFinished() .subscribe(() => { this.reload() @@ -132,4 +137,14 @@ export class StatisticsWidgetComponent }, ]) } + + filterByFileType(filetype: DocumentFileType) { + if (filetype.is_other) return + this.documentListViewService.quickFilter([ + { + rule_type: FILTER_MIME_TYPE, + value: filetype.mime_type, + }, + ]) + } } diff --git a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts index cc1591966..45ac9217a 100644 --- a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts +++ b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts @@ -12,13 +12,13 @@ import { NgbAlert, NgbCollapse } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { routes } from 'src/app/app-routing.module' import { PermissionsGuard } from 'src/app/guards/permissions.guard' -import { - ConsumerStatusService, - FileStatus, - FileStatusPhase, -} from 'src/app/services/consumer-status.service' import { PermissionsService } from 'src/app/services/permissions.service' import { UploadDocumentsService } from 'src/app/services/upload-documents.service' +import { + FileStatus, + FileStatusPhase, + WebsocketStatusService, +} from 'src/app/services/websocket-status.service' import { UploadFileWidgetComponent } from './upload-file-widget.component' const FAILED_STATUSES = [new FileStatus()] @@ -42,7 +42,7 @@ const DEFAULT_STATUSES = [ describe('UploadFileWidgetComponent', () => { let component: UploadFileWidgetComponent let fixture: ComponentFixture - let consumerStatusService: ConsumerStatusService + let websocketStatusService: WebsocketStatusService let uploadDocumentsService: UploadDocumentsService beforeEach(async () => { @@ -65,7 +65,7 @@ describe('UploadFileWidgetComponent', () => { ], }).compileComponents() - consumerStatusService = TestBed.inject(ConsumerStatusService) + websocketStatusService = TestBed.inject(WebsocketStatusService) uploadDocumentsService = TestBed.inject(UploadDocumentsService) fixture = TestBed.createComponent(UploadFileWidgetComponent) component = fixture.componentInstance @@ -91,14 +91,14 @@ describe('UploadFileWidgetComponent', () => { }) it('should generate stats summary', () => { - mockConsumerStatuses(consumerStatusService) + mockConsumerStatuses(websocketStatusService) expect(component.getStatusSummary()).toEqual( 'Processing: 6, Failed: 1, Added: 4' ) }) it('should report an upload progress summary', () => { - mockConsumerStatuses(consumerStatusService) + mockConsumerStatuses(websocketStatusService) expect(component.getTotalUploadProgress()).toEqual(0.75) }) @@ -117,7 +117,7 @@ describe('UploadFileWidgetComponent', () => { }) it('should enforce a maximum number of alerts', () => { - mockConsumerStatuses(consumerStatusService) + mockConsumerStatuses(websocketStatusService) fixture.detectChanges() // 5 total, 1 hidden expect(fixture.debugElement.queryAll(By.directive(NgbAlert))).toHaveLength( @@ -131,19 +131,19 @@ describe('UploadFileWidgetComponent', () => { }) it('should allow dismissing an alert', () => { - const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss') + const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss') component.dismiss(new FileStatus()) expect(dismissSpy).toHaveBeenCalled() }) it('should allow dismissing completed alerts', fakeAsync(() => { - mockConsumerStatuses(consumerStatusService) + mockConsumerStatuses(websocketStatusService) component.alertsExpanded = true fixture.detectChanges() jest .spyOn(component, 'getStatusCompleted') .mockImplementation(() => SUCCESS_STATUSES) - const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss') + const dismissSpy = jest.spyOn(websocketStatusService, 'dismiss') component.dismissCompleted() tick(1000) fixture.detectChanges() diff --git a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts index f237ab7aa..f60cdce60 100644 --- a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.ts @@ -12,13 +12,13 @@ import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap' import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' -import { - ConsumerStatusService, - FileStatus, - FileStatusPhase, -} from 'src/app/services/consumer-status.service' import { SettingsService } from 'src/app/services/settings.service' import { UploadDocumentsService } from 'src/app/services/upload-documents.service' +import { + FileStatus, + FileStatusPhase, + WebsocketStatusService, +} from 'src/app/services/websocket-status.service' import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' const MAX_ALERTS = 5 @@ -46,7 +46,7 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions { @ViewChildren(NgbAlert) alerts: QueryList constructor( - private consumerStatusService: ConsumerStatusService, + private websocketStatusService: WebsocketStatusService, private uploadDocumentsService: UploadDocumentsService, public settingsService: SettingsService ) { @@ -54,13 +54,13 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions { } getStatus() { - return this.consumerStatusService.getConsumerStatus().slice(0, MAX_ALERTS) + return this.websocketStatusService.getConsumerStatus().slice(0, MAX_ALERTS) } getStatusSummary() { let strings = [] let countUploadingAndProcessing = - this.consumerStatusService.getConsumerStatusNotCompleted().length + this.websocketStatusService.getConsumerStatusNotCompleted().length let countFailed = this.getStatusFailed().length let countSuccess = this.getStatusSuccess().length if (countUploadingAndProcessing > 0) { @@ -78,27 +78,30 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions { } getStatusHidden() { - if (this.consumerStatusService.getConsumerStatus().length < MAX_ALERTS) + if (this.websocketStatusService.getConsumerStatus().length < MAX_ALERTS) return [] - else return this.consumerStatusService.getConsumerStatus().slice(MAX_ALERTS) + else + return this.websocketStatusService.getConsumerStatus().slice(MAX_ALERTS) } getStatusUploading() { - return this.consumerStatusService.getConsumerStatus( + return this.websocketStatusService.getConsumerStatus( FileStatusPhase.UPLOADING ) } getStatusFailed() { - return this.consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED) + return this.websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED) } getStatusSuccess() { - return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS) + return this.websocketStatusService.getConsumerStatus( + FileStatusPhase.SUCCESS + ) } getStatusCompleted() { - return this.consumerStatusService.getConsumerStatusCompleted() + return this.websocketStatusService.getConsumerStatusCompleted() } getTotalUploadProgress() { @@ -134,12 +137,12 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions { } dismiss(status: FileStatus) { - this.consumerStatusService.dismiss(status) + this.websocketStatusService.dismiss(status) } dismissCompleted() { this.getStatusCompleted().forEach((status) => - this.consumerStatusService.dismiss(status) + this.websocketStatusService.dismiss(status) ) } diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index bc0bf41fd..a8e14c51d 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -9,9 +9,9 @@ }
- @for (setting of zoomSettings; track setting) { - } @@ -25,15 +25,20 @@
- - Download - + @if (metadata?.has_archive_version) {
- +
} @@ -351,9 +356,9 @@ -
+
@if (showThumbnailOverlay) { - Document loading... + Document loading... }
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.scss b/src-ui/src/app/components/document-detail/document-detail.component.scss index c00f7655e..3fc009020 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.scss +++ b/src-ui/src/app/components/document-detail/document-detail.component.scss @@ -85,5 +85,8 @@ textarea.rtl { > img { filter: blur(1px); + max-width: 100%; + object-fit: contain; + object-position: top; } } diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index 0a2e5605f..349e213aa 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -24,6 +24,7 @@ import { NgbModalRef, } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { DeviceDetectorService } from 'ngx-device-detector' import { of, throwError } from 'rxjs' import { routes } from 'src/app/app-routing.module' import { Correspondent } from 'src/app/data/correspondent' @@ -61,7 +62,10 @@ import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component' -import { DocumentDetailComponent } from './document-detail.component' +import { + DocumentDetailComponent, + ZoomSetting, +} from './document-detail.component' const doc: Document = { id: 3, @@ -127,6 +131,7 @@ describe('DocumentDetailComponent', () => { let documentListViewService: DocumentListViewService let settingsService: SettingsService let customFieldsService: CustomFieldsService + let deviceDetectorService: DeviceDetectorService let httpTestingController: HttpTestingController let componentRouterService: ComponentRouterService @@ -264,6 +269,7 @@ describe('DocumentDetailComponent', () => { settingsService = TestBed.inject(SettingsService) settingsService.currentUser = { id: 1 } customFieldsService = TestBed.inject(CustomFieldsService) + deviceDetectorService = TestBed.inject(DeviceDetectorService) fixture = TestBed.createComponent(DocumentDetailComponent) httpTestingController = TestBed.inject(HttpTestingController) componentRouterService = TestBed.inject(ComponentRouterService) @@ -451,7 +457,9 @@ describe('DocumentDetailComponent', () => { component.save(true) expect(updateSpy).toHaveBeenCalled() expect(closeSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.') + expect(toastSpy).toHaveBeenCalledWith( + 'Document "Doc 3" saved successfully.' + ) }) it('should support save without close and show success toast', () => { @@ -464,7 +472,9 @@ describe('DocumentDetailComponent', () => { component.save() expect(updateSpy).toHaveBeenCalled() expect(closeSpy).not.toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.') + expect(toastSpy).toHaveBeenCalledWith( + 'Document "Doc 3" saved successfully.' + ) }) it('should show toast error on save if error occurs', () => { @@ -479,7 +489,10 @@ describe('DocumentDetailComponent', () => { component.save() expect(updateSpy).toHaveBeenCalled() expect(closeSpy).not.toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith('Error saving document', error) + expect(toastSpy).toHaveBeenCalledWith( + 'Error saving document "Doc 3"', + error + ) }) it('should show error toast on save but close if user can no longer edit', () => { @@ -495,7 +508,9 @@ describe('DocumentDetailComponent', () => { component.save(true) expect(updateSpy).toHaveBeenCalled() expect(closeSpy).toHaveBeenCalled() - expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.') + expect(toastSpy).toHaveBeenCalledWith( + 'Document "Doc 3" saved successfully.' + ) }) it('should allow save and next', () => { @@ -741,7 +756,7 @@ describe('DocumentDetailComponent', () => { it('should support zoom controls', () => { initNormally() - component.onZoomSelect({ target: { value: '1' } } as any) // from select + component.setZoom(ZoomSetting.One) // from select expect(component.previewZoomSetting).toEqual('1') component.increaseZoom() expect(component.previewZoomSetting).toEqual('1.5') @@ -749,18 +764,18 @@ describe('DocumentDetailComponent', () => { expect(component.previewZoomSetting).toEqual('2') component.decreaseZoom() expect(component.previewZoomSetting).toEqual('1.5') - component.onZoomSelect({ target: { value: '1' } } as any) // from select + component.setZoom(ZoomSetting.One) // from select component.decreaseZoom() expect(component.previewZoomSetting).toEqual('.75') - component.onZoomSelect({ target: { value: 'page-fit' } } as any) // from select + component.setZoom(ZoomSetting.PageFit) // from select expect(component.previewZoomScale).toEqual('page-fit') expect(component.previewZoomSetting).toEqual('1') component.increaseZoom() expect(component.previewZoomSetting).toEqual('1.5') expect(component.previewZoomScale).toEqual('page-width') - component.onZoomSelect({ target: { value: 'page-fit' } } as any) // from select + component.setZoom(ZoomSetting.PageFit) // from select expect(component.previewZoomScale).toEqual('page-fit') expect(component.previewZoomSetting).toEqual('1') component.decreaseZoom() @@ -768,6 +783,19 @@ describe('DocumentDetailComponent', () => { expect(component.previewZoomScale).toEqual('page-width') }) + it('should select correct zoom setting in dropdown', () => { + initNormally() + component.setZoom(ZoomSetting.PageFit) + expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeTruthy() + expect(component.isZoomSelected(ZoomSetting.One)).toBeFalsy() + component.setZoom(ZoomSetting.PageWidth) + expect(component.isZoomSelected(ZoomSetting.One)).toBeTruthy() + expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeFalsy() + component.setZoom(ZoomSetting.Quarter) + expect(component.isZoomSelected(ZoomSetting.Quarter)).toBeTruthy() + expect(component.isZoomSelected(ZoomSetting.PageFit)).toBeFalsy() + }) + it('should support updating notes dynamically', () => { const notes = [ { @@ -1268,4 +1296,38 @@ describe('DocumentDetailComponent', () => { .error(new ErrorEvent('failed')) expect(component.tiffError).not.toBeUndefined() }) + + it('should support download using share sheet on mobile, direct download otherwise', () => { + const shareSpy = jest.spyOn(navigator, 'share') + const createSpy = jest.spyOn(document, 'createElement') + const urlRevokeSpy = jest.spyOn(URL, 'revokeObjectURL') + initNormally() + + // Mobile + jest.spyOn(deviceDetectorService, 'isDesktop').mockReturnValue(false) + component.download() + httpTestingController + .expectOne(`${environment.apiBaseUrl}documents/${doc.id}/download/`) + .error(new ProgressEvent('failed')) + expect(shareSpy).not.toHaveBeenCalled() + + component.download(true) + httpTestingController + .expectOne( + `${environment.apiBaseUrl}documents/${doc.id}/download/?original=true` + ) + .flush(new ArrayBuffer(100)) + expect(shareSpy).toHaveBeenCalled() + + // Desktop + shareSpy.mockClear() + jest.spyOn(deviceDetectorService, 'isDesktop').mockReturnValue(true) + component.download() + httpTestingController + .expectOne(`${environment.apiBaseUrl}documents/${doc.id}/download/`) + .flush(new ArrayBuffer(100)) + expect(shareSpy).not.toHaveBeenCalled() + expect(createSpy).toHaveBeenCalledWith('a') + expect(urlRevokeSpy).toHaveBeenCalled() + }) }) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 8d1b35071..30e34d9cf 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -20,6 +20,7 @@ import { import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { DeviceDetectorService } from 'ngx-device-detector' import { BehaviorSubject, Observable, Subject } from 'rxjs' import { debounceTime, @@ -123,7 +124,7 @@ enum ContentRenderType { TIFF = 'tiff', } -enum ZoomSetting { +export enum ZoomSetting { PageFit = 'page-fit', PageWidth = 'page-width', Quarter = '.25', @@ -195,8 +196,6 @@ export class DocumentDetailComponent previewUrl: string thumbUrl: string previewText: string - downloadUrl: string - downloadOriginalUrl: string previewLoaded: boolean = false tiffURL: string tiffError: string @@ -234,6 +233,9 @@ export class DocumentDetailComponent ogDate: Date customFields: CustomField[] + + public downloading: boolean = false + public readonly CustomFieldDataType = CustomFieldDataType public readonly ContentRenderType = ContentRenderType @@ -274,7 +276,8 @@ export class DocumentDetailComponent private customFieldsService: CustomFieldsService, private http: HttpClient, private hotKeyService: HotKeyService, - private componentRouterService: ComponentRouterService + private componentRouterService: ComponentRouterService, + private deviceDetectorService: DeviceDetectorService ) { super() } @@ -325,6 +328,7 @@ export class DocumentDetailComponent } ngOnInit(): void { + this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING)) this.documentForm.valueChanges .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe(() => { @@ -417,13 +421,6 @@ export class DocumentDetailComponent .pipe( switchMap((doc) => { this.documentId = doc.id - this.downloadUrl = this.documentsService.getDownloadUrl( - this.documentId - ) - this.downloadOriginalUrl = this.documentsService.getDownloadUrl( - this.documentId, - true - ) this.suggestions = null const openDocument = this.openDocumentService.getOpenDocument( this.documentId @@ -812,7 +809,9 @@ export class DocumentDetailComponent this.store.next(newValues) this.openDocumentService.setDirty(this.document, false) this.openDocumentService.save() - this.toastService.showInfo($localize`Document saved successfully.`) + this.toastService.showInfo( + $localize`Document "${newValues.title}" saved successfully.` + ) this.networkActive = false this.error = null if (close) { @@ -826,11 +825,16 @@ export class DocumentDetailComponent error: (error) => { this.networkActive = false if (!this.userCanEdit) { - this.toastService.showInfo($localize`Document saved successfully.`) + this.toastService.showInfo( + $localize`Document "${this.document.title}" saved successfully.` + ) close && this.close() } else { this.error = error.error - this.toastService.showError($localize`Error saving document`, error) + this.toastService.showError( + $localize`Error saving document "${this.document.title}"`, + error + ) } }, }) @@ -959,7 +963,7 @@ export class DocumentDetailComponent .subscribe({ next: () => { this.toastService.showInfo( - $localize`Reprocess operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.` + $localize`Reprocess operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.` ) if (modal) { modal.close() @@ -978,6 +982,52 @@ export class DocumentDetailComponent }) } + download(original: boolean = false) { + this.downloading = true + const downloadUrl = this.documentsService.getDownloadUrl( + this.documentId, + original + ) + this.http.get(downloadUrl, { responseType: 'blob' }).subscribe({ + next: (blob) => { + this.downloading = false + const blobParts = [blob] + const file = new File( + blobParts, + original + ? this.document.original_file_name + : this.document.archived_file_name, + { + type: original ? this.document.mime_type : 'application/pdf', + } + ) + if ( + !this.deviceDetectorService.isDesktop() && + navigator.canShare && + navigator.canShare({ files: [file] }) + ) { + navigator.share({ + files: [file], + }) + } else { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = this.document.title + a.click() + URL.revokeObjectURL(url) + } + }, + error: (error) => { + this.downloading = false + this.toastService.showError( + $localize`Error downloading document`, + error + ) + }, + }) + } + hasNext() { return this.documentListViewService.hasNext(this.documentId) } @@ -1023,14 +1073,13 @@ export class DocumentDetailComponent } } - onZoomSelect(event: Event) { - const setting = (event.target as HTMLSelectElement)?.value as ZoomSetting - if (ZoomSetting.PageFit === setting) { - this.previewZoomSetting = ZoomSetting.One + setZoom(setting: ZoomSetting) { + if (ZoomSetting.PageFit === setting || ZoomSetting.PageWidth === setting) { this.previewZoomScale = setting + this.previewZoomSetting = ZoomSetting.One } else { - this.previewZoomScale = ZoomSetting.PageWidth this.previewZoomSetting = setting + this.previewZoomScale = ZoomSetting.PageWidth } } @@ -1040,6 +1089,14 @@ export class DocumentDetailComponent ) } + isZoomSelected(setting: ZoomSetting): boolean { + if (this.previewZoomScale === ZoomSetting.PageFit) { + return setting === ZoomSetting.PageFit + } + + return this.previewZoomSetting === setting + } + getZoomSettingTitle(setting: ZoomSetting): string { switch (setting) { case ZoomSetting.PageFit: @@ -1273,7 +1330,7 @@ export class DocumentDetailComponent .subscribe({ next: () => { this.toastService.showInfo( - $localize`Split operation will begin in the background.` + $localize`Split operation for "${this.document.title}" will begin in the background.` ) modal.close() }, @@ -1312,7 +1369,7 @@ export class DocumentDetailComponent .subscribe({ next: () => { this.toastService.show({ - content: $localize`Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes.`, + content: $localize`Rotation of "${this.document.title}" will begin in the background. Close and re-open the document after the operation has completed to see the changes.`, delay: 8000, action: this.close.bind(this), actionName: $localize`Close`, @@ -1352,7 +1409,7 @@ export class DocumentDetailComponent .subscribe({ next: () => { this.toastService.showInfo( - $localize`Delete pages operation will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.` + $localize`Delete pages operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.` ) modal.close() }, diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts index 21b8f4175..aa4a07d12 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts @@ -1039,6 +1039,7 @@ describe('BulkEditorComponent', () => { httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id` ) // listAllFilteredIds + expect(documentListViewService.selected.size).toEqual(0) }) it('should support bulk download with archive, originals or both and file formatting', () => { diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 5750c4b2f..9864761fa 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -268,6 +268,9 @@ export class BulkEditorComponent .pipe(first()) .subscribe({ next: () => { + if (args['delete_originals']) { + this.list.selected.clear() + } this.list.reload() this.list.reduceSelectionToFilter() this.list.selected.forEach((id) => { diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index 6ebbd6055..84e415815 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -32,7 +32,7 @@ {{document.title | documentTitle}} } @if (displayFields.includes(DisplayField.TAGS)) { - @for (tagID of document.tags; track t) { + @for (tagID of document.tags; track tagID) { } } diff --git a/src-ui/src/app/components/document-list/document-list.component.spec.ts b/src-ui/src/app/components/document-list/document-list.component.spec.ts index 805a65846..13a938f59 100644 --- a/src-ui/src/app/components/document-list/document-list.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-list.component.spec.ts @@ -38,16 +38,16 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { FilterPipe } from 'src/app/pipes/filter.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { UsernamePipe } from 'src/app/pipes/username.pipe' -import { - ConsumerStatusService, - FileStatus, -} from 'src/app/services/consumer-status.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { PermissionsService } from 'src/app/services/permissions.service' import { DocumentService } from 'src/app/services/rest/document.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' +import { + FileStatus, + WebsocketStatusService, +} from 'src/app/services/websocket-status.service' import { DocumentCardLargeComponent } from './document-card-large/document-card-large.component' import { DocumentCardSmallComponent } from './document-card-small/document-card-small.component' import { DocumentListComponent } from './document-list.component' @@ -81,7 +81,7 @@ describe('DocumentListComponent', () => { let fixture: ComponentFixture let documentListService: DocumentListViewService let documentService: DocumentService - let consumerStatusService: ConsumerStatusService + let websocketStatusService: WebsocketStatusService let savedViewService: SavedViewService let router: Router let activatedRoute: ActivatedRoute @@ -112,7 +112,7 @@ describe('DocumentListComponent', () => { documentListService = TestBed.inject(DocumentListViewService) documentService = TestBed.inject(DocumentService) - consumerStatusService = TestBed.inject(ConsumerStatusService) + websocketStatusService = TestBed.inject(WebsocketStatusService) savedViewService = TestBed.inject(SavedViewService) router = TestBed.inject(Router) activatedRoute = TestBed.inject(ActivatedRoute) @@ -128,13 +128,24 @@ describe('DocumentListComponent', () => { const reloadSpy = jest.spyOn(documentListService, 'reload') const fileStatusSubject = new Subject() jest - .spyOn(consumerStatusService, 'onDocumentConsumptionFinished') + .spyOn(websocketStatusService, 'onDocumentConsumptionFinished') .mockReturnValue(fileStatusSubject) fixture.detectChanges() fileStatusSubject.next(new FileStatus()) expect(reloadSpy).toHaveBeenCalled() }) + it('should reload on document deleted', () => { + const reloadSpy = jest.spyOn(documentListService, 'reload') + const documentDeletedSubject = new Subject() + jest + .spyOn(websocketStatusService, 'onDocumentDeleted') + .mockReturnValue(documentDeletedSubject) + fixture.detectChanges() + documentDeletedSubject.next(true) + expect(reloadSpy).toHaveBeenCalled() + }) + it('should show score sort fields on fulltext queries', () => { documentListService.filterRules = [ { diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index b845a524a..e1f71edbc 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -43,7 +43,6 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { DocumentTypeNamePipe } from 'src/app/pipes/document-type-name.pipe' import { StoragePathNamePipe } from 'src/app/pipes/storage-path-name.pipe' import { UsernamePipe } from 'src/app/pipes/username.pipe' -import { ConsumerStatusService } from 'src/app/services/consumer-status.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { HotKeyService } from 'src/app/services/hot-key.service' import { OpenDocumentsService } from 'src/app/services/open-documents.service' @@ -51,6 +50,7 @@ import { PermissionsService } from 'src/app/services/permissions.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' +import { WebsocketStatusService } from 'src/app/services/websocket-status.service' import { filterRulesDiffer, isFullTextFilterRule, @@ -113,7 +113,7 @@ export class DocumentListComponent private router: Router, private toastService: ToastService, private modalService: NgbModal, - private consumerStatusService: ConsumerStatusService, + private websocketStatusService: WebsocketStatusService, public openDocumentsService: OpenDocumentsService, public settingsService: SettingsService, private hotKeyService: HotKeyService, @@ -234,13 +234,17 @@ export class DocumentListComponent } ngOnInit(): void { - this.consumerStatusService + this.websocketStatusService .onDocumentConsumptionFinished() .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe(() => { this.list.reload() }) + this.websocketStatusService.onDocumentDeleted().subscribe(() => { + this.list.reload() + }) + this.route.paramMap .pipe( filter((params) => params.has('id')), // only on saved view e.g. /view/id diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html index 32c2a1908..fcb7bed8f 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html @@ -94,11 +94,11 @@ { expect(component.textFilterModifier).toEqual('less') // TEXT_FILTER_MODIFIER_LT })) + it('should ingest text filter rules for mime type', fakeAsync(() => { + expect(component.textFilter).toEqual(null) + component.filterRules = [ + { + rule_type: FILTER_MIME_TYPE, + value: 'pdf', + }, + ] + expect(component.textFilter).toEqual('pdf') + expect(component.textFilterTarget).toEqual('mime-type') // TEXT_FILTER_TARGET_MIME_TYPE + })) + it('should ingest text filter rules for fulltext query', fakeAsync(() => { expect(component.textFilter).toEqual(null) component.filterRules = [ @@ -465,48 +482,92 @@ describe('FilterEditorComponent', () => { ]) })) - it('should ingest filter rules for date created after', fakeAsync(() => { - expect(component.dateCreatedAfter).toBeNull() + it('should ingest filter rules for date created after and adjust date by 1 day', fakeAsync(() => { + expect(component.dateCreatedFrom).toBeNull() component.filterRules = [ { rule_type: FILTER_CREATED_AFTER, value: '2023-05-14', }, ] - expect(component.dateCreatedAfter).toEqual('2023-05-14') + expect(component.dateCreatedFrom).toEqual('2023-05-15') })) - it('should ingest filter rules for date created before', fakeAsync(() => { - expect(component.dateCreatedBefore).toBeNull() + it('should ingest filter rules for date created from', fakeAsync(() => { + expect(component.dateCreatedFrom).toBeNull() + component.filterRules = [ + { + rule_type: FILTER_CREATED_FROM, + value: '2023-05-14', + }, + ] + expect(component.dateCreatedFrom).toEqual('2023-05-14') + })) + + it('should ingest filter rules for date created before and adjust date by 1 day', fakeAsync(() => { + expect(component.dateCreatedTo).toBeNull() component.filterRules = [ { rule_type: FILTER_CREATED_BEFORE, value: '2023-05-14', }, ] - expect(component.dateCreatedBefore).toEqual('2023-05-14') + expect(component.dateCreatedTo).toEqual('2023-05-13') })) - it('should ingest filter rules for date added after', fakeAsync(() => { - expect(component.dateAddedAfter).toBeNull() + it('should ingest filter rules for date created to', fakeAsync(() => { + expect(component.dateCreatedTo).toBeNull() + component.filterRules = [ + { + rule_type: FILTER_CREATED_TO, + value: '2023-05-14', + }, + ] + expect(component.dateCreatedTo).toEqual('2023-05-14') + })) + + it('should ingest filter rules for date added after and adjust date by 1 day', fakeAsync(() => { + expect(component.dateAddedFrom).toBeNull() component.filterRules = [ { rule_type: FILTER_ADDED_AFTER, value: '2023-05-14', }, ] - expect(component.dateAddedAfter).toEqual('2023-05-14') + expect(component.dateAddedFrom).toEqual('2023-05-15') })) - it('should ingest filter rules for date added before', fakeAsync(() => { - expect(component.dateAddedBefore).toBeNull() + it('should ingest filter rules for date added from', fakeAsync(() => { + expect(component.dateAddedFrom).toBeNull() + component.filterRules = [ + { + rule_type: FILTER_ADDED_FROM, + value: '2023-05-14', + }, + ] + expect(component.dateAddedFrom).toEqual('2023-05-14') + })) + + it('should ingest filter rules for date added before and adjust date by 1 day', fakeAsync(() => { + expect(component.dateAddedTo).toBeNull() component.filterRules = [ { rule_type: FILTER_ADDED_BEFORE, value: '2023-05-14', }, ] - expect(component.dateAddedBefore).toEqual('2023-05-14') + expect(component.dateAddedTo).toEqual('2023-05-13') + })) + + it('should ingest filter rules for date added to', fakeAsync(() => { + expect(component.dateAddedTo).toBeNull() + component.filterRules = [ + { + rule_type: FILTER_ADDED_TO, + value: '2023-05-14', + }, + ] + expect(component.dateAddedTo).toEqual('2023-05-14') })) it('should ingest filter rules for has all tags', fakeAsync(() => { @@ -1174,12 +1235,30 @@ describe('FilterEditorComponent', () => { ]) })) + it('should convert user input to correct filter rules on mime type', fakeAsync(() => { + component.textFilterInput.nativeElement.value = 'pdf' + component.textFilterInput.nativeElement.dispatchEvent(new Event('input')) + const textFieldTargetDropdown = fixture.debugElement.queryAll( + By.directive(NgbDropdownItem) + )[4] + textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_MIME_TYPE + fixture.detectChanges() + tick(400) + expect(component.textFilterTarget).toEqual('mime-type') + expect(component.filterRules).toEqual([ + { + rule_type: FILTER_MIME_TYPE, + value: 'pdf', + }, + ]) + })) + it('should convert user input to correct filter rules on full text query', fakeAsync(() => { component.textFilterInput.nativeElement.value = 'foo' component.textFilterInput.nativeElement.dispatchEvent(new Event('input')) const textFieldTargetDropdown = fixture.debugElement.queryAll( By.directive(NgbDropdownItem) - )[4] + )[5] textFieldTargetDropdown.triggerEventHandler('click') // TEXT_FILTER_TARGET_ASN fixture.detectChanges() tick(400) @@ -1464,7 +1543,7 @@ describe('FilterEditorComponent', () => { ]) })) - it('should convert user input to correct filter rules on date created after', fakeAsync(() => { + it('should convert user input to correct filter rules on date created from', fakeAsync(() => { const dateCreatedDropdown = fixture.debugElement.queryAll( By.directive(DatesDropdownComponent) )[0] @@ -1473,18 +1552,18 @@ describe('FilterEditorComponent', () => { dateCreatedAfter.nativeElement.value = '05/14/2023' // dateCreatedAfter.triggerEventHandler('change') // TODO: why isn't ngModel triggering this on change? - component.dateCreatedAfter = '2023-05-14' + component.dateCreatedFrom = '2023-05-14' fixture.detectChanges() tick(400) expect(component.filterRules).toEqual([ { - rule_type: FILTER_CREATED_AFTER, + rule_type: FILTER_CREATED_FROM, value: '2023-05-14', }, ]) })) - it('should convert user input to correct filter rules on date created before', fakeAsync(() => { + it('should convert user input to correct filter rules on date created to', fakeAsync(() => { const dateCreatedDropdown = fixture.debugElement.queryAll( By.directive(DatesDropdownComponent) )[0] @@ -1493,12 +1572,12 @@ describe('FilterEditorComponent', () => { dateCreatedBefore.nativeElement.value = '05/14/2023' // dateCreatedBefore.triggerEventHandler('change') // TODO: why isn't ngModel triggering this on change? - component.dateCreatedBefore = '2023-05-14' + component.dateCreatedTo = '2023-05-14' fixture.detectChanges() tick(400) expect(component.filterRules).toEqual([ { - rule_type: FILTER_CREATED_BEFORE, + rule_type: FILTER_CREATED_TO, value: '2023-05-14', }, ]) @@ -1546,7 +1625,7 @@ describe('FilterEditorComponent', () => { component.textFilterInput.nativeElement.dispatchEvent(new Event('input')) const textFieldTargetDropdown = fixture.debugElement.queryAll( By.directive(NgbDropdownItem) - )[4] + )[5] textFieldTargetDropdown.triggerEventHandler('click') fixture.detectChanges() tick(400) @@ -1578,12 +1657,12 @@ describe('FilterEditorComponent', () => { dateAddedAfter.nativeElement.value = '05/14/2023' // dateAddedAfter.triggerEventHandler('change') // TODO: why isn't ngModel triggering this on change? - component.dateAddedAfter = '2023-05-14' + component.dateAddedFrom = '2023-05-14' fixture.detectChanges() tick(400) expect(component.filterRules).toEqual([ { - rule_type: FILTER_ADDED_AFTER, + rule_type: FILTER_ADDED_FROM, value: '2023-05-14', }, ]) @@ -1598,12 +1677,12 @@ describe('FilterEditorComponent', () => { dateAddedBefore.nativeElement.value = '05/14/2023' // dateAddedBefore.triggerEventHandler('change') // TODO: why isn't ngModel triggering this on change? - component.dateAddedBefore = '2023-05-14' + component.dateAddedTo = '2023-05-14' fixture.detectChanges() tick(400) expect(component.filterRules).toEqual([ { - rule_type: FILTER_ADDED_BEFORE, + rule_type: FILTER_ADDED_TO, value: '2023-05-14', }, ]) diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts index 6e9a3fb7d..0916d9c0d 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -38,6 +38,8 @@ import { FilterRule } from 'src/app/data/filter-rule' import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, + FILTER_ADDED_FROM, + FILTER_ADDED_TO, FILTER_ASN, FILTER_ASN_GT, FILTER_ASN_ISNULL, @@ -45,6 +47,8 @@ import { FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, + FILTER_CREATED_FROM, + FILTER_CREATED_TO, FILTER_CUSTOM_FIELDS_QUERY, FILTER_CUSTOM_FIELDS_TEXT, FILTER_DOCUMENT_TYPE, @@ -62,6 +66,7 @@ import { FILTER_HAS_STORAGE_PATH_ANY, FILTER_HAS_TAGS_ALL, FILTER_HAS_TAGS_ANY, + FILTER_MIME_TYPE, FILTER_OWNER, FILTER_OWNER_ANY, FILTER_OWNER_DOES_NOT_INCLUDE, @@ -122,6 +127,7 @@ const TEXT_FILTER_TARGET_ASN = 'asn' const TEXT_FILTER_TARGET_FULLTEXT_QUERY = 'fulltext-query' const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = 'fulltext-morelike' const TEXT_FILTER_TARGET_CUSTOM_FIELDS = 'custom-fields' +const TEXT_FILTER_TARGET_MIME_TYPE = 'mime-type' const TEXT_FILTER_MODIFIER_EQUALS = 'equals' const TEXT_FILTER_MODIFIER_NULL = 'is null' @@ -133,19 +139,19 @@ const RELATIVE_DATE_QUERY_REGEXP_CREATED = /created:\[([^\]]+)\]/g const RELATIVE_DATE_QUERY_REGEXP_ADDED = /added:\[([^\]]+)\]/g const RELATIVE_DATE_QUERYSTRINGS = [ { - relativeDate: RelativeDate.LAST_7_DAYS, + relativeDate: RelativeDate.WITHIN_1_WEEK, dateQuery: '-1 week to now', }, { - relativeDate: RelativeDate.LAST_MONTH, + relativeDate: RelativeDate.WITHIN_1_MONTH, dateQuery: '-1 month to now', }, { - relativeDate: RelativeDate.LAST_3_MONTHS, + relativeDate: RelativeDate.WITHIN_3_MONTHS, dateQuery: '-3 month to now', }, { - relativeDate: RelativeDate.LAST_YEAR, + relativeDate: RelativeDate.WITHIN_1_YEAR, dateQuery: '-1 year to now', }, ] @@ -161,6 +167,7 @@ const DEFAULT_TEXT_FILTER_TARGET_OPTIONS = [ id: TEXT_FILTER_TARGET_CUSTOM_FIELDS, name: $localize`Custom fields`, }, + { id: TEXT_FILTER_TARGET_MIME_TYPE, name: $localize`File type` }, { id: TEXT_FILTER_TARGET_FULLTEXT_QUERY, name: $localize`Advanced search`, @@ -349,10 +356,10 @@ export class FilterEditorComponent storagePathSelectionModel = new FilterableDropdownSelectionModel() customFieldQueriesModel = new CustomFieldQueriesModel() - dateCreatedBefore: string - dateCreatedAfter: string - dateAddedBefore: string - dateAddedAfter: string + dateCreatedTo: string + dateCreatedFrom: string + dateAddedTo: string + dateAddedFrom: string dateCreatedRelativeDate: RelativeDate dateAddedRelativeDate: RelativeDate @@ -385,10 +392,10 @@ export class FilterEditorComponent this.customFieldQueriesModel.clear(false) this._textFilter = null this._moreLikeId = null - this.dateAddedBefore = null - this.dateAddedAfter = null - this.dateCreatedBefore = null - this.dateCreatedAfter = null + this.dateAddedTo = null + this.dateAddedFrom = null + this.dateCreatedTo = null + this.dateCreatedFrom = null this.dateCreatedRelativeDate = null this.dateAddedRelativeDate = null this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS @@ -412,6 +419,10 @@ export class FilterEditorComponent this._textFilter = rule.value this.textFilterTarget = TEXT_FILTER_TARGET_CUSTOM_FIELDS break + case FILTER_MIME_TYPE: + this.textFilterTarget = TEXT_FILTER_TARGET_MIME_TYPE + this._textFilter = rule.value + break case FILTER_FULLTEXT_QUERY: let allQueryArgs = rule.value.split(',') let textQueryArgs = [] @@ -458,16 +469,40 @@ export class FilterEditorComponent }) break case FILTER_CREATED_AFTER: - this.dateCreatedAfter = rule.value + // Old rules require adjusting date by a day + const createdAfter = new Date(rule.value) + createdAfter.setDate(createdAfter.getDate() + 1) + this.dateCreatedFrom = createdAfter.toISOString().split('T')[0] break case FILTER_CREATED_BEFORE: - this.dateCreatedBefore = rule.value + // Old rules require adjusting date by a day + const createdBefore = new Date(rule.value) + createdBefore.setDate(createdBefore.getDate() - 1) + this.dateCreatedTo = createdBefore.toISOString().split('T')[0] break case FILTER_ADDED_AFTER: - this.dateAddedAfter = rule.value + // Old rules require adjusting date by a day + const addedAfter = new Date(rule.value) + addedAfter.setDate(addedAfter.getDate() + 1) + this.dateAddedFrom = addedAfter.toISOString().split('T')[0] break case FILTER_ADDED_BEFORE: - this.dateAddedBefore = rule.value + // Old rules require adjusting date by a day + const addedBefore = new Date(rule.value) + addedBefore.setDate(addedBefore.getDate() - 1) + this.dateAddedTo = addedBefore.toISOString().split('T')[0] + break + case FILTER_CREATED_FROM: + this.dateCreatedFrom = rule.value + break + case FILTER_CREATED_TO: + this.dateCreatedTo = rule.value + break + case FILTER_ADDED_FROM: + this.dateAddedFrom = rule.value + break + case FILTER_ADDED_TO: + this.dateAddedTo = rule.value break case FILTER_HAS_TAGS_ALL: this.tagSelectionModel.logicalOperator = LogicalOperator.And @@ -701,6 +736,15 @@ export class FilterEditorComponent value: this._textFilter, }) } + if ( + this._textFilter && + this.textFilterTarget == TEXT_FILTER_TARGET_MIME_TYPE + ) { + filterRules.push({ + rule_type: FILTER_MIME_TYPE, + value: this._textFilter, + }) + } if ( this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_QUERY @@ -814,28 +858,28 @@ export class FilterEditorComponent value: JSON.stringify(queries[0]), }) } - if (this.dateCreatedBefore) { + if (this.dateCreatedTo) { filterRules.push({ - rule_type: FILTER_CREATED_BEFORE, - value: this.dateCreatedBefore, + rule_type: FILTER_CREATED_TO, + value: this.dateCreatedTo, }) } - if (this.dateCreatedAfter) { + if (this.dateCreatedFrom) { filterRules.push({ - rule_type: FILTER_CREATED_AFTER, - value: this.dateCreatedAfter, + rule_type: FILTER_CREATED_FROM, + value: this.dateCreatedFrom, }) } - if (this.dateAddedBefore) { + if (this.dateAddedTo) { filterRules.push({ - rule_type: FILTER_ADDED_BEFORE, - value: this.dateAddedBefore, + rule_type: FILTER_ADDED_TO, + value: this.dateAddedTo, }) } - if (this.dateAddedAfter) { + if (this.dateAddedFrom) { filterRules.push({ - rule_type: FILTER_ADDED_AFTER, - value: this.dateAddedAfter, + rule_type: FILTER_ADDED_FROM, + value: this.dateAddedFrom, }) } if ( diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts index 476b09106..9a17a4528 100644 --- a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts +++ b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts @@ -108,13 +108,16 @@ export class CustomFieldsComponent this.customFieldsService.delete(field).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted field`) + this.toastService.showInfo($localize`Deleted field "${field.name}"`) this.customFieldsService.clearCache() this.settingsService.initializeDisplayFields() this.reload() }, error: (e) => { - this.toastService.showError($localize`Error deleting field.`, e) + this.toastService.showError( + $localize`Error deleting field "${field.name}".`, + e + ) }, }) }) diff --git a/src-ui/src/app/components/manage/mail/mail.component.spec.ts b/src-ui/src/app/components/manage/mail/mail.component.spec.ts index 3ece18061..b9f02343d 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.spec.ts +++ b/src-ui/src/app/components/manage/mail/mail.component.spec.ts @@ -214,7 +214,7 @@ describe('MailComponent', () => { deleteSpy.mockReturnValueOnce(of(true)) deleteDialog.confirm() expect(listAllSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account') + expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail account "account1"') }) it('should support process mail account, show error if needed', () => { @@ -231,7 +231,9 @@ describe('MailComponent', () => { expect(toastErrorSpy).toHaveBeenCalled() processSpy.mockReturnValueOnce(of(true)) component.processAccount(mailAccounts[0] as MailAccount) - expect(toastInfoSpy).toHaveBeenCalledWith('Processing mail account') + expect(toastInfoSpy).toHaveBeenCalledWith( + 'Processing mail account "account1"' + ) }) it('should support edit / create mail rule, show error if needed', () => { @@ -274,14 +276,14 @@ describe('MailComponent', () => { const toastInfoSpy = jest.spyOn(toastService, 'showInfo') const listAllSpy = jest.spyOn(mailRuleService, 'listAll') deleteSpy.mockReturnValueOnce( - throwError(() => new Error('error deleting mail rule')) + throwError(() => new Error('error deleting mail rule "rule1"')) ) deleteDialog.confirm() expect(toastErrorSpy).toBeCalled() deleteSpy.mockReturnValueOnce(of(true)) deleteDialog.confirm() expect(listAllSpy).toHaveBeenCalled() - expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule') + expect(toastInfoSpy).toHaveBeenCalledWith('Deleted mail rule "rule1"') }) it('should support edit permissions on mail rule objects', () => { diff --git a/src-ui/src/app/components/manage/mail/mail.component.ts b/src-ui/src/app/components/manage/mail/mail.component.ts index b606a33c9..8d4222516 100644 --- a/src-ui/src/app/components/manage/mail/mail.component.ts +++ b/src-ui/src/app/components/manage/mail/mail.component.ts @@ -200,7 +200,9 @@ export class MailComponent this.mailAccountService.delete(account).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted mail account`) + this.toastService.showInfo( + $localize`Deleted mail account "${account.name}"` + ) this.mailAccountService.clearCache() this.mailAccountService .listAll(null, null, { full_perms: true }) @@ -210,7 +212,7 @@ export class MailComponent }, error: (e) => { this.toastService.showError( - $localize`Error deleting mail account.`, + $localize`Error deleting mail account "${account.name}".`, e ) }, @@ -221,10 +223,15 @@ export class MailComponent processAccount(account: MailAccount) { this.mailAccountService.processAccount(account).subscribe({ next: () => { - this.toastService.showInfo($localize`Processing mail account`) + this.toastService.showInfo( + $localize`Processing mail account "${account.name}"` + ) }, error: (e) => { - this.toastService.showError($localize`Error processing mail account`, e) + this.toastService.showError( + $localize`Error processing mail account "${account.name}"`, + e + ) }, }) } @@ -272,7 +279,10 @@ export class MailComponent ) }, error: (e) => { - this.toastService.showError($localize`Error toggling rule.`, e) + this.toastService.showError( + $localize`Error toggling rule "${rule.name}".`, + e + ) }, }) } @@ -291,7 +301,9 @@ export class MailComponent this.mailRuleService.delete(rule).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted mail rule`) + this.toastService.showInfo( + $localize`Deleted mail rule "${rule.name}"` + ) this.mailRuleService.clearCache() this.mailRuleService .listAll(null, null, { full_perms: true }) @@ -300,7 +312,10 @@ export class MailComponent }) }, error: (e) => { - this.toastService.showError($localize`Error deleting mail rule.`, e) + this.toastService.showError( + $localize`Error deleting mail rule "${rule.name}".`, + e + ) }, }) }) diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.html b/src-ui/src/app/components/manage/management-list/management-list.component.html index ca4e93095..82fd8502c 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.html +++ b/src-ui/src/app/components/manage/management-list/management-list.component.html @@ -73,35 +73,37 @@ } -
-
- -
- - - @if (object.document_count > 0) { - - } +
+
+
+ +
+ + + @if (object.document_count > 0) { + + } +
-
-
- - -
- @if (object.document_count > 0) { -
- +
- } + @if (object.document_count > 0) { +
+ +
+ } +
} diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index ecb3e2519..7f7721485 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -21,7 +21,6 @@ import { MATCHING_ALGORITHMS, MatchingModel, } from 'src/app/data/matching-model' -import { ObjectWithId } from 'src/app/data/object-with-id' import { ObjectWithPermissions } from 'src/app/data/object-with-permissions' import { SortableDirective, @@ -56,7 +55,7 @@ export interface ManagementListColumn { } @Directive() -export abstract class ManagementListComponent +export abstract class ManagementListComponent extends LoadingComponentWithPermissions implements OnInit, OnDestroy { @@ -195,7 +194,7 @@ export abstract class ManagementListComponent activeModal.componentInstance.succeeded.subscribe(() => { this.reloadData() this.toastService.showInfo( - $localize`Successfully updated ${this.typeName}.` + $localize`Successfully updated ${this.typeName} "${object.name}".` ) }) activeModal.componentInstance.failed.subscribe((e) => { @@ -208,7 +207,7 @@ export abstract class ManagementListComponent abstract getDeleteMessage(object: T) - filterDocuments(object: ObjectWithId) { + filterDocuments(object: MatchingModel) { this.documentListViewService.quickFilter([ { rule_type: this.filterRuleType, value: object.id.toString() }, ]) diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.ts b/src-ui/src/app/components/manage/workflows/workflows.component.ts index b1f9ff6d0..edbca44c8 100644 --- a/src-ui/src/app/components/manage/workflows/workflows.component.ts +++ b/src-ui/src/app/components/manage/workflows/workflows.component.ts @@ -142,12 +142,17 @@ export class WorkflowsComponent this.workflowService.delete(workflow).subscribe({ next: () => { modal.close() - this.toastService.showInfo($localize`Deleted workflow`) + this.toastService.showInfo( + $localize`Deleted workflow "${workflow.name}".` + ) this.workflowService.clearCache() this.reload() }, error: (e) => { - this.toastService.showError($localize`Error deleting workflow.`, e) + this.toastService.showError( + $localize`Error deleting workflow "${workflow.name}".`, + e + ) }, }) }) @@ -158,14 +163,17 @@ export class WorkflowsComponent next: () => { this.toastService.showInfo( workflow.enabled - ? $localize`Enabled workflow` - : $localize`Disabled workflow` + ? $localize`Enabled workflow "${workflow.name}"` + : $localize`Disabled workflow "${workflow.name}"` ) this.workflowService.clearCache() this.reload() }, error: (e) => { - this.toastService.showError($localize`Error toggling workflow.`, e) + this.toastService.showError( + $localize`Error toggling workflow "${workflow.name}".`, + e + ) }, }) } diff --git a/src-ui/src/app/data/custom-field-query.ts b/src-ui/src/app/data/custom-field-query.ts index 226a10605..084b7a330 100644 --- a/src-ui/src/app/data/custom-field-query.ts +++ b/src-ui/src/app/data/custom-field-query.ts @@ -36,6 +36,7 @@ export const CUSTOM_FIELD_QUERY_OPERATOR_LABELS = { export enum CustomFieldQueryOperatorGroups { Basic = 'basic', + Exact = 'exact', String = 'string', Arithmetic = 'arithmetic', Containment = 'containment', @@ -48,8 +49,8 @@ export const CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP = { [CustomFieldQueryOperatorGroups.Basic]: [ CustomFieldQueryOperator.Exists, CustomFieldQueryOperator.IsNull, - CustomFieldQueryOperator.Exact, ], + [CustomFieldQueryOperatorGroups.Exact]: [CustomFieldQueryOperator.Exact], [CustomFieldQueryOperatorGroups.String]: [CustomFieldQueryOperator.IContains], [CustomFieldQueryOperatorGroups.Arithmetic]: [ CustomFieldQueryOperator.GreaterThan, @@ -71,27 +72,33 @@ export const CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP = { export const CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE = { [CustomFieldDataType.String]: [ CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Exact, CustomFieldQueryOperatorGroups.String, ], [CustomFieldDataType.Url]: [ CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Exact, CustomFieldQueryOperatorGroups.String, ], [CustomFieldDataType.Date]: [ CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Exact, CustomFieldQueryOperatorGroups.Date, ], [CustomFieldDataType.Boolean]: [CustomFieldQueryOperatorGroups.Basic], [CustomFieldDataType.Integer]: [ CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Exact, CustomFieldQueryOperatorGroups.Arithmetic, ], [CustomFieldDataType.Float]: [ CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Exact, CustomFieldQueryOperatorGroups.Arithmetic, ], [CustomFieldDataType.Monetary]: [ CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Exact, CustomFieldQueryOperatorGroups.String, CustomFieldQueryOperatorGroups.Arithmetic, ], @@ -101,6 +108,7 @@ export const CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE = { ], [CustomFieldDataType.Select]: [ CustomFieldQueryOperatorGroups.Basic, + CustomFieldQueryOperatorGroups.Exact, CustomFieldQueryOperatorGroups.Subset, ], } diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts index 1c6b1cdf8..bb2bf762c 100644 --- a/src-ui/src/app/data/filter-rule-type.ts +++ b/src-ui/src/app/data/filter-rule-type.ts @@ -36,6 +36,11 @@ export const FILTER_CREATED_DAY = 12 export const FILTER_ADDED_BEFORE = 13 export const FILTER_ADDED_AFTER = 14 +export const FILTER_CREATED_TO = 43 +export const FILTER_CREATED_FROM = 44 +export const FILTER_ADDED_TO = 45 +export const FILTER_ADDED_FROM = 46 + export const FILTER_MODIFIED_BEFORE = 15 export const FILTER_MODIFIED_AFTER = 16 @@ -57,6 +62,8 @@ export const FILTER_HAS_ANY_CUSTOM_FIELDS = 41 export const FILTER_CUSTOM_FIELDS_QUERY = 42 +export const FILTER_MIME_TYPE = 47 + export const FILTER_RULE_TYPES: FilterRuleType[] = [ { id: FILTER_TITLE, @@ -179,6 +186,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ datatype: 'date', multi: false, }, + { + id: FILTER_CREATED_TO, + filtervar: 'created__date__lte', + datatype: 'date', + multi: false, + }, + { + id: FILTER_CREATED_FROM, + filtervar: 'created__date__gte', + datatype: 'date', + multi: false, + }, { id: FILTER_CREATED_YEAR, filtervar: 'created__year', @@ -210,6 +229,18 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ datatype: 'date', multi: false, }, + { + id: FILTER_ADDED_TO, + filtervar: 'added__date__lte', + datatype: 'date', + multi: false, + }, + { + id: FILTER_ADDED_FROM, + filtervar: 'added__date__gte', + datatype: 'date', + multi: false, + }, { id: FILTER_MODIFIED_BEFORE, filtervar: 'modified__date__lt', @@ -325,6 +356,12 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ datatype: 'string', multi: false, }, + { + id: FILTER_MIME_TYPE, + filtervar: 'mime_type', + datatype: 'string', + multi: false, + }, ] export interface FilterRuleType { diff --git a/src-ui/src/app/data/mail-rule.ts b/src-ui/src/app/data/mail-rule.ts index 6e2c468a2..4c47b6500 100644 --- a/src-ui/src/app/data/mail-rule.ts +++ b/src-ui/src/app/data/mail-rule.ts @@ -11,6 +11,14 @@ export enum MailRuleConsumptionScope { Everything = 3, } +export enum MailRulePdfLayout { + Default = 0, + TextHtml = 1, + HtmlText = 2, + HtmlOnly = 3, + TextOnly = 4, +} + export enum MailAction { Delete = 1, Move = 2, @@ -59,6 +67,8 @@ export interface MailRule extends ObjectWithPermissions { attachment_type: MailFilterAttachmentType + pdf_layout: MailRulePdfLayout + action: MailAction action_parameter?: string diff --git a/src-ui/src/app/data/ui-settings.ts b/src-ui/src/app/data/ui-settings.ts index b8a319d9b..c5164d6e1 100644 --- a/src-ui/src/app/data/ui-settings.ts +++ b/src-ui/src/app/data/ui-settings.ts @@ -33,6 +33,8 @@ export const SETTINGS_KEYS = { DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted', THEME_COLOR: 'general-settings:theme:color', USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer', + PDF_VIEWER_ZOOM_SETTING: + 'general-settings:document-details:pdf-viewer-zoom-setting', DATE_LOCALE: 'general-settings:date-display:date-locale', DATE_FORMAT: 'general-settings:date-display:date-format', NOTIFICATIONS_CONSUMER_NEW_DOCUMENT: @@ -269,4 +271,9 @@ export const SETTINGS: UiSetting[] = [ type: 'boolean', default: false, }, + { + key: SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING, + type: 'string', + default: 'page-width', // ZoomSetting from 'document-detail.component' + }, ] diff --git a/src-ui/src/app/data/websocket-documents-deleted-message.ts b/src-ui/src/app/data/websocket-documents-deleted-message.ts new file mode 100644 index 000000000..11ded3781 --- /dev/null +++ b/src-ui/src/app/data/websocket-documents-deleted-message.ts @@ -0,0 +1,3 @@ +export interface WebsocketDocumentsDeletedMessage { + documents: number[] +} diff --git a/src-ui/src/app/data/websocket-consumer-status-message.ts b/src-ui/src/app/data/websocket-progress-message.ts similarity index 77% rename from src-ui/src/app/data/websocket-consumer-status-message.ts rename to src-ui/src/app/data/websocket-progress-message.ts index d1ac590b1..c8e37e232 100644 --- a/src-ui/src/app/data/websocket-consumer-status-message.ts +++ b/src-ui/src/app/data/websocket-progress-message.ts @@ -1,4 +1,4 @@ -export interface WebsocketConsumerStatusMessage { +export interface WebsocketProgressMessage { filename?: string task_id?: string current_progress?: number diff --git a/src-ui/src/app/services/component-router.service.ts b/src-ui/src/app/services/component-router.service.ts index 3f97584b7..0589ef61f 100644 --- a/src-ui/src/app/services/component-router.service.ts +++ b/src-ui/src/app/services/component-router.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@angular/core' import { ActivationStart, Event, Router } from '@angular/router' import { filter } from 'rxjs' +const EXCLUDE_COMPONENTS = ['AppFrameComponent'] + @Injectable({ providedIn: 'root', }) @@ -15,7 +17,8 @@ export class ComponentRouterService { .subscribe((event: ActivationStart) => { if ( this.componentHistory[this.componentHistory.length - 1] !== - event.snapshot.component.name + event.snapshot.component.name && + !EXCLUDE_COMPONENTS.includes(event.snapshot.component.name) ) { this.history.push(event.snapshot.url.toString()) this.componentHistory.push(event.snapshot.component.name) diff --git a/src-ui/src/app/services/consumer-status.service.spec.ts b/src-ui/src/app/services/consumer-status.service.spec.ts deleted file mode 100644 index b699f8772..000000000 --- a/src-ui/src/app/services/consumer-status.service.spec.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { - HttpEventType, - HttpResponse, - provideHttpClient, - withInterceptorsFromDi, -} from '@angular/common/http' -import { - HttpTestingController, - provideHttpClientTesting, -} from '@angular/common/http/testing' -import { TestBed } from '@angular/core/testing' -import WS from 'jest-websocket-mock' -import { environment } from 'src/environments/environment' -import { - ConsumerStatusService, - FILE_STATUS_MESSAGES, - FileStatusPhase, -} from './consumer-status.service' -import { DocumentService } from './rest/document.service' -import { SettingsService } from './settings.service' - -describe('ConsumerStatusService', () => { - let httpTestingController: HttpTestingController - let consumerStatusService: ConsumerStatusService - let documentService: DocumentService - let settingsService: SettingsService - - const server = new WS( - `${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`, - { jsonProtocol: true } - ) - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - providers: [ - ConsumerStatusService, - DocumentService, - SettingsService, - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - ], - }) - - httpTestingController = TestBed.inject(HttpTestingController) - settingsService = TestBed.inject(SettingsService) - settingsService.currentUser = { - id: 1, - username: 'testuser', - is_superuser: false, - } - consumerStatusService = TestBed.inject(ConsumerStatusService) - documentService = TestBed.inject(DocumentService) - }) - - afterEach(() => { - httpTestingController.verify() - }) - - it('should update status on websocket processing progress', () => { - const task_id = '1234' - const status = consumerStatusService.newFileUpload('file.pdf') - expect(status.getProgress()).toEqual(0) - - consumerStatusService.connect() - - consumerStatusService - .onDocumentConsumptionFinished() - .subscribe((filestatus) => { - expect(filestatus.phase).toEqual(FileStatusPhase.SUCCESS) - }) - - consumerStatusService.onDocumentDetected().subscribe((filestatus) => { - expect(filestatus.phase).toEqual(FileStatusPhase.STARTED) - }) - - server.send({ - task_id, - filename: 'file.pdf', - current_progress: 50, - max_progress: 100, - document_id: 12, - status: 'WORKING', - }) - - expect(status.getProgress()).toBeCloseTo(0.6) // (0.8 * 50/100) + .2 - expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([ - status, - ]) - - server.send({ - task_id, - filename: 'file.pdf', - current_progress: 100, - max_progress: 100, - document_id: 12, - status: 'SUCCESS', - message: FILE_STATUS_MESSAGES.finished, - }) - - expect(status.getProgress()).toEqual(1) - expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength( - 0 - ) - expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1) - - consumerStatusService.disconnect() - }) - - it('should update status on websocket failed progress', () => { - const task_id = '1234' - const status = consumerStatusService.newFileUpload('file.pdf') - status.taskId = task_id - consumerStatusService.connect() - - consumerStatusService - .onDocumentConsumptionFailed() - .subscribe((filestatus) => { - expect(filestatus.phase).toEqual(FileStatusPhase.FAILED) - }) - - server.send({ - task_id, - filename: 'file.pdf', - current_progress: 50, - max_progress: 100, - document_id: 12, - }) - - expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([ - status, - ]) - - server.send({ - task_id, - filename: 'file.pdf', - current_progress: 50, - max_progress: 100, - document_id: 12, - status: 'FAILED', - message: FILE_STATUS_MESSAGES.document_already_exists, - }) - - expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength( - 0 - ) - expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1) - }) - - it('should update status on upload progress', () => { - const task_id = '1234' - const status = consumerStatusService.newFileUpload('file.pdf') - - documentService.uploadDocument({}).subscribe((event) => { - if (event.type === HttpEventType.Response) { - status.taskId = event.body['task_id'] - status.message = $localize`Upload complete, waiting...` - } else if (event.type === HttpEventType.UploadProgress) { - status.updateProgress( - FileStatusPhase.UPLOADING, - event.loaded, - event.total - ) - } - }) - - const req = httpTestingController.expectOne( - `${environment.apiBaseUrl}documents/post_document/` - ) - - req.event( - new HttpResponse({ - body: { - task_id, - }, - }) - ) - - req.event({ - type: HttpEventType.UploadProgress, - loaded: 100, - total: 300, - }) - - expect( - consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING) - ).toEqual([status]) - expect(consumerStatusService.getConsumerStatus()).toEqual([status]) - expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([ - status, - ]) - - req.event({ - type: HttpEventType.UploadProgress, - loaded: 300, - total: 300, - }) - - expect(status.getProgress()).toEqual(0.2) // 0.2 * 300/300 - }) - - it('should support dismiss completed', () => { - consumerStatusService.connect() - server.send({ - task_id: '1234', - filename: 'file.pdf', - current_progress: 100, - max_progress: 100, - document_id: 12, - status: 'SUCCESS', - message: 'finished', - }) - - expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1) - consumerStatusService.dismissCompleted() - expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0) - consumerStatusService.disconnect() - }) - - it('should support dismiss', () => { - const task_id = '1234' - const status = consumerStatusService.newFileUpload('file.pdf') - status.taskId = task_id - status.updateProgress(FileStatusPhase.UPLOADING, 50, 100) - - const status2 = consumerStatusService.newFileUpload('file2.pdf') - status2.updateProgress(FileStatusPhase.UPLOADING, 50, 100) - - expect( - consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING) - ).toEqual([status, status2]) - expect(consumerStatusService.getConsumerStatus()).toEqual([status, status2]) - expect(consumerStatusService.getConsumerStatusNotCompleted()).toEqual([ - status, - status2, - ]) - - consumerStatusService.dismiss(status) - expect(consumerStatusService.getConsumerStatus()).toEqual([status2]) - - consumerStatusService.dismiss(status2) - expect(consumerStatusService.getConsumerStatus()).toHaveLength(0) - }) - - it('should support fail', () => { - const task_id = '1234' - const status = consumerStatusService.newFileUpload('file.pdf') - status.taskId = task_id - status.updateProgress(FileStatusPhase.UPLOADING, 50, 100) - expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength( - 1 - ) - expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(0) - consumerStatusService.fail(status, 'fail') - expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength( - 0 - ) - expect(consumerStatusService.getConsumerStatusCompleted()).toHaveLength(1) - }) - - it('should notify of document created on status message without upload', () => { - let detected = false - consumerStatusService.onDocumentDetected().subscribe((filestatus) => { - expect(filestatus.phase).toEqual(FileStatusPhase.STARTED) - detected = true - }) - - consumerStatusService.connect() - server.send({ - task_id: '1234', - filename: 'file.pdf', - current_progress: 0, - max_progress: 100, - message: 'new_file', - status: 'STARTED', - }) - - consumerStatusService.disconnect() - expect(detected).toBeTruthy() - }) - - it('should notify of document in progress without upload', () => { - consumerStatusService.connect() - server.send({ - task_id: '1234', - filename: 'file.pdf', - current_progress: 50, - max_progress: 100, - docuement_id: 12, - status: 'WORKING', - }) - - consumerStatusService.disconnect() - expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength( - 1 - ) - }) - - it('should not notify current user if document has different expected owner', () => { - consumerStatusService.connect() - server.send({ - task_id: '1234', - filename: 'file1.pdf', - current_progress: 50, - max_progress: 100, - docuement_id: 12, - owner_id: 1, - status: 'WORKING', - }) - - server.send({ - task_id: '5678', - filename: 'file2.pdf', - current_progress: 50, - max_progress: 100, - docuement_id: 13, - owner_id: 2, - status: 'WORKING', - }) - - consumerStatusService.disconnect() - expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength( - 1 - ) - }) -}) diff --git a/src-ui/src/app/services/toast.service.spec.ts b/src-ui/src/app/services/toast.service.spec.ts index 274ea9db6..ce50b165e 100644 --- a/src-ui/src/app/services/toast.service.spec.ts +++ b/src-ui/src/app/services/toast.service.spec.ts @@ -25,6 +25,33 @@ describe('ToastService', () => { }) }) + it('adds a unique id to toast on show', () => { + const toast = { + title: 'Title', + content: 'content', + delay: 5000, + } + toastService.show(toast) + + toastService.getToasts().subscribe((toasts) => { + expect(toasts[0].id).toBeDefined() + }) + }) + + it('parses error string to object on show', () => { + const toast = { + title: 'Title', + content: 'content', + delay: 5000, + error: 'Error string', + } + toastService.show(toast) + + toastService.getToasts().subscribe((toasts) => { + expect(toasts[0].error).toEqual('Error string') + }) + }) + it('creates toasts with defaults on showInfo and showError', () => { toastService.showInfo('Info toast') toastService.showError('Error toast') @@ -54,4 +81,29 @@ describe('ToastService', () => { expect(toasts).toHaveLength(0) }) }) + + it('clears all toasts on clearToasts', () => { + toastService.showInfo('Info toast') + toastService.showError('Error toast') + toastService.clearToasts() + + toastService.getToasts().subscribe((toasts) => { + expect(toasts).toHaveLength(0) + }) + }) + + it('suppresses popup toasts if suppressPopupToasts is true', (finish) => { + toastService.showToast.subscribe((toast) => { + expect(toast).not.toBeNull() + }) + toastService.showInfo('Info toast') + + toastService.showToast.subscribe((toast) => { + expect(toast).toBeNull() + finish() + }) + + toastService.suppressPopupToasts = true + toastService.showInfo('Info toast') + }) }) diff --git a/src-ui/src/app/services/toast.service.ts b/src-ui/src/app/services/toast.service.ts index 16c534b5c..b917bf94b 100644 --- a/src-ui/src/app/services/toast.service.ts +++ b/src-ui/src/app/services/toast.service.ts @@ -1,7 +1,10 @@ import { Injectable } from '@angular/core' import { Subject } from 'rxjs' +import { v4 as uuidv4 } from 'uuid' export interface Toast { + id?: string + content: string delay: number @@ -22,13 +25,32 @@ export interface Toast { }) export class ToastService { constructor() {} + _suppressPopupToasts: boolean + + set suppressPopupToasts(value: boolean) { + this._suppressPopupToasts = value + this.showToast.next(null) + } private toasts: Toast[] = [] private toastsSubject: Subject = new Subject() + public showToast: Subject = new Subject() + show(toast: Toast) { - this.toasts.push(toast) + if (!toast.id) { + toast.id = uuidv4() + } + if (typeof toast.error === 'string') { + try { + toast.error = JSON.parse(toast.error) + } catch (e) {} + } + this.toasts.unshift(toast) + if (!this._suppressPopupToasts) { + this.showToast.next(toast) + } this.toastsSubject.next(this.toasts) } @@ -46,7 +68,7 @@ export class ToastService { } closeToast(toast: Toast) { - let index = this.toasts.findIndex((t) => t == toast) + let index = this.toasts.findIndex((t) => t.id == toast.id) if (index > -1) { this.toasts.splice(index, 1) this.toastsSubject.next(this.toasts) @@ -56,4 +78,10 @@ export class ToastService { getToasts() { return this.toastsSubject } + + clearToasts() { + this.toasts = [] + this.toastsSubject.next(this.toasts) + this.showToast.next(null) + } } diff --git a/src-ui/src/app/services/upload-documents.service.spec.ts b/src-ui/src/app/services/upload-documents.service.spec.ts index cf0812306..28fb5b2e0 100644 --- a/src-ui/src/app/services/upload-documents.service.spec.ts +++ b/src-ui/src/app/services/upload-documents.service.spec.ts @@ -9,11 +9,11 @@ import { } from '@angular/common/http/testing' import { TestBed } from '@angular/core/testing' import { environment } from 'src/environments/environment' -import { - ConsumerStatusService, - FileStatusPhase, -} from './consumer-status.service' import { UploadDocumentsService } from './upload-documents.service' +import { + FileStatusPhase, + WebsocketStatusService, +} from './websocket-status.service' const files = [ { @@ -45,14 +45,14 @@ const fileList = { describe('UploadDocumentsService', () => { let httpTestingController: HttpTestingController let uploadDocumentsService: UploadDocumentsService - let consumerStatusService: ConsumerStatusService + let websocketStatusService: WebsocketStatusService beforeEach(() => { TestBed.configureTestingModule({ imports: [], providers: [ UploadDocumentsService, - ConsumerStatusService, + WebsocketStatusService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), ], @@ -60,7 +60,7 @@ describe('UploadDocumentsService', () => { httpTestingController = TestBed.inject(HttpTestingController) uploadDocumentsService = TestBed.inject(UploadDocumentsService) - consumerStatusService = TestBed.inject(ConsumerStatusService) + websocketStatusService = TestBed.inject(WebsocketStatusService) }) afterEach(() => { @@ -80,11 +80,11 @@ describe('UploadDocumentsService', () => { it('updates progress during upload and failure', () => { uploadDocumentsService.uploadFiles(fileList) - expect(consumerStatusService.getConsumerStatusNotCompleted()).toHaveLength( + expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength( 2 ) expect( - consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING) + websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING) ).toHaveLength(0) const req = httpTestingController.match( @@ -98,7 +98,7 @@ describe('UploadDocumentsService', () => { }) expect( - consumerStatusService.getConsumerStatus(FileStatusPhase.UPLOADING) + websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING) ).toHaveLength(1) }) @@ -110,7 +110,7 @@ describe('UploadDocumentsService', () => { ) expect( - consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED) + websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED) ).toHaveLength(0) req[0].flush( @@ -122,7 +122,7 @@ describe('UploadDocumentsService', () => { ) expect( - consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED) + websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED) ).toHaveLength(1) uploadDocumentsService.uploadFiles(fileList) @@ -140,7 +140,7 @@ describe('UploadDocumentsService', () => { ) expect( - consumerStatusService.getConsumerStatus(FileStatusPhase.FAILED) + websocketStatusService.getConsumerStatus(FileStatusPhase.FAILED) ).toHaveLength(2) }) diff --git a/src-ui/src/app/services/upload-documents.service.ts b/src-ui/src/app/services/upload-documents.service.ts index 8a5e42b47..602e6d8ae 100644 --- a/src-ui/src/app/services/upload-documents.service.ts +++ b/src-ui/src/app/services/upload-documents.service.ts @@ -2,11 +2,11 @@ import { HttpEventType } from '@angular/common/http' import { Injectable } from '@angular/core' import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop' import { Subscription } from 'rxjs' -import { - ConsumerStatusService, - FileStatusPhase, -} from './consumer-status.service' import { DocumentService } from './rest/document.service' +import { + FileStatusPhase, + WebsocketStatusService, +} from './websocket-status.service' @Injectable({ providedIn: 'root', @@ -16,7 +16,7 @@ export class UploadDocumentsService { constructor( private documentService: DocumentService, - private consumerStatusService: ConsumerStatusService + private websocketStatusService: WebsocketStatusService ) {} onNgxFileDrop(files: NgxFileDropEntry[]) { @@ -37,7 +37,7 @@ export class UploadDocumentsService { private uploadFile(file: File) { let formData = new FormData() formData.append('document', file, file.name) - let status = this.consumerStatusService.newFileUpload(file.name) + let status = this.websocketStatusService.newFileUpload(file.name) status.message = $localize`Connecting...` @@ -61,11 +61,11 @@ export class UploadDocumentsService { error: (error) => { switch (error.status) { case 400: { - this.consumerStatusService.fail(status, error.error.document) + this.websocketStatusService.fail(status, error.error.document) break } default: { - this.consumerStatusService.fail( + this.websocketStatusService.fail( status, $localize`HTTP error: ${error.status} ${error.statusText}` ) diff --git a/src-ui/src/app/services/websocket-status.service.spec.ts b/src-ui/src/app/services/websocket-status.service.spec.ts new file mode 100644 index 000000000..d3bf71f7e --- /dev/null +++ b/src-ui/src/app/services/websocket-status.service.spec.ts @@ -0,0 +1,375 @@ +import { + HttpEventType, + HttpResponse, + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http' +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing' +import { TestBed } from '@angular/core/testing' +import WS from 'jest-websocket-mock' +import { environment } from 'src/environments/environment' +import { DocumentService } from './rest/document.service' +import { SettingsService } from './settings.service' +import { + FILE_STATUS_MESSAGES, + FileStatusPhase, + WebsocketStatusService, + WebsocketStatusType, +} from './websocket-status.service' + +describe('ConsumerStatusService', () => { + let httpTestingController: HttpTestingController + let websocketStatusService: WebsocketStatusService + let documentService: DocumentService + let settingsService: SettingsService + + const server = new WS( + `${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/`, + { jsonProtocol: true } + ) + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + WebsocketStatusService, + DocumentService, + SettingsService, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }) + + httpTestingController = TestBed.inject(HttpTestingController) + settingsService = TestBed.inject(SettingsService) + settingsService.currentUser = { + id: 1, + username: 'testuser', + is_superuser: false, + } + websocketStatusService = TestBed.inject(WebsocketStatusService) + documentService = TestBed.inject(DocumentService) + }) + + afterEach(() => { + httpTestingController.verify() + }) + + it('should update status on websocket processing progress', () => { + const task_id = '1234' + const status = websocketStatusService.newFileUpload('file.pdf') + expect(status.getProgress()).toEqual(0) + + websocketStatusService.connect() + + websocketStatusService + .onDocumentConsumptionFinished() + .subscribe((filestatus) => { + expect(filestatus.phase).toEqual(FileStatusPhase.SUCCESS) + }) + + websocketStatusService.onDocumentDetected().subscribe((filestatus) => { + expect(filestatus.phase).toEqual(FileStatusPhase.STARTED) + }) + + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id, + filename: 'file.pdf', + current_progress: 50, + max_progress: 100, + document_id: 12, + status: 'WORKING', + }, + }) + + expect(status.getProgress()).toBeCloseTo(0.6) // (0.8 * 50/100) + .2 + expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([ + status, + ]) + + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id, + filename: 'file.pdf', + current_progress: 100, + max_progress: 100, + document_id: 12, + status: 'SUCCESS', + message: FILE_STATUS_MESSAGES.finished, + }, + }) + + expect(status.getProgress()).toEqual(1) + expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength( + 0 + ) + expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1) + + websocketStatusService.disconnect() + }) + + it('should update status on websocket failed progress', () => { + const task_id = '1234' + const status = websocketStatusService.newFileUpload('file.pdf') + status.taskId = task_id + websocketStatusService.connect() + + websocketStatusService + .onDocumentConsumptionFailed() + .subscribe((filestatus) => { + expect(filestatus.phase).toEqual(FileStatusPhase.FAILED) + }) + + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id, + filename: 'file.pdf', + current_progress: 50, + max_progress: 100, + document_id: 12, + }, + }) + + expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([ + status, + ]) + + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id, + filename: 'file.pdf', + current_progress: 50, + max_progress: 100, + document_id: 12, + status: 'FAILED', + message: FILE_STATUS_MESSAGES.document_already_exists, + }, + }) + + expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength( + 0 + ) + expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1) + }) + + it('should update status on upload progress', () => { + const task_id = '1234' + const status = websocketStatusService.newFileUpload('file.pdf') + + documentService.uploadDocument({}).subscribe((event) => { + if (event.type === HttpEventType.Response) { + status.taskId = event.body['task_id'] + status.message = $localize`Upload complete, waiting...` + } else if (event.type === HttpEventType.UploadProgress) { + status.updateProgress( + FileStatusPhase.UPLOADING, + event.loaded, + event.total + ) + } + }) + + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/post_document/` + ) + + req.event( + new HttpResponse({ + body: { + task_id, + }, + }) + ) + + req.event({ + type: HttpEventType.UploadProgress, + loaded: 100, + total: 300, + }) + + expect( + websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING) + ).toEqual([status]) + expect(websocketStatusService.getConsumerStatus()).toEqual([status]) + expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([ + status, + ]) + + req.event({ + type: HttpEventType.UploadProgress, + loaded: 300, + total: 300, + }) + + expect(status.getProgress()).toEqual(0.2) // 0.2 * 300/300 + }) + + it('should support dismiss completed', () => { + websocketStatusService.connect() + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id: '1234', + filename: 'file.pdf', + current_progress: 100, + max_progress: 100, + document_id: 12, + status: 'SUCCESS', + message: 'finished', + }, + }) + + expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1) + websocketStatusService.dismissCompleted() + expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(0) + websocketStatusService.disconnect() + }) + + it('should support dismiss', () => { + const task_id = '1234' + const status = websocketStatusService.newFileUpload('file.pdf') + status.taskId = task_id + status.updateProgress(FileStatusPhase.UPLOADING, 50, 100) + + const status2 = websocketStatusService.newFileUpload('file2.pdf') + status2.updateProgress(FileStatusPhase.UPLOADING, 50, 100) + + expect( + websocketStatusService.getConsumerStatus(FileStatusPhase.UPLOADING) + ).toEqual([status, status2]) + expect(websocketStatusService.getConsumerStatus()).toEqual([ + status, + status2, + ]) + expect(websocketStatusService.getConsumerStatusNotCompleted()).toEqual([ + status, + status2, + ]) + + websocketStatusService.dismiss(status) + expect(websocketStatusService.getConsumerStatus()).toEqual([status2]) + + websocketStatusService.dismiss(status2) + expect(websocketStatusService.getConsumerStatus()).toHaveLength(0) + }) + + it('should support fail', () => { + const task_id = '1234' + const status = websocketStatusService.newFileUpload('file.pdf') + status.taskId = task_id + status.updateProgress(FileStatusPhase.UPLOADING, 50, 100) + expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength( + 1 + ) + expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(0) + websocketStatusService.fail(status, 'fail') + expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength( + 0 + ) + expect(websocketStatusService.getConsumerStatusCompleted()).toHaveLength(1) + }) + + it('should notify of document created on status message without upload', () => { + let detected = false + websocketStatusService.onDocumentDetected().subscribe((filestatus) => { + expect(filestatus.phase).toEqual(FileStatusPhase.STARTED) + detected = true + }) + + websocketStatusService.connect() + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id: '1234', + filename: 'file.pdf', + current_progress: 0, + max_progress: 100, + message: 'new_file', + status: 'STARTED', + }, + }) + + websocketStatusService.disconnect() + expect(detected).toBeTruthy() + }) + + it('should notify of document in progress without upload', () => { + websocketStatusService.connect() + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id: '1234', + filename: 'file.pdf', + current_progress: 50, + max_progress: 100, + docuement_id: 12, + status: 'WORKING', + }, + }) + + websocketStatusService.disconnect() + expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength( + 1 + ) + }) + + it('should not notify current user if document has different expected owner', () => { + websocketStatusService.connect() + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id: '1234', + filename: 'file1.pdf', + current_progress: 50, + max_progress: 100, + docuement_id: 12, + owner_id: 1, + status: 'WORKING', + }, + }) + + server.send({ + type: WebsocketStatusType.STATUS_UPDATE, + data: { + task_id: '5678', + filename: 'file2.pdf', + current_progress: 50, + max_progress: 100, + docuement_id: 13, + owner_id: 2, + status: 'WORKING', + }, + }) + + websocketStatusService.disconnect() + expect(websocketStatusService.getConsumerStatusNotCompleted()).toHaveLength( + 1 + ) + }) + + it('should trigger deleted subject on document deleted', () => { + let deleted = false + websocketStatusService.onDocumentDeleted().subscribe(() => { + deleted = true + }) + + websocketStatusService.connect() + server.send({ + type: WebsocketStatusType.DOCUMENTS_DELETED, + data: { + documents: [1, 2, 3], + }, + }) + + websocketStatusService.disconnect() + expect(deleted).toBeTruthy() + }) +}) diff --git a/src-ui/src/app/services/consumer-status.service.ts b/src-ui/src/app/services/websocket-status.service.ts similarity index 71% rename from src-ui/src/app/services/consumer-status.service.ts rename to src-ui/src/app/services/websocket-status.service.ts index 40641ff81..13f82412f 100644 --- a/src-ui/src/app/services/consumer-status.service.ts +++ b/src-ui/src/app/services/websocket-status.service.ts @@ -1,9 +1,15 @@ import { Injectable } from '@angular/core' import { Subject } from 'rxjs' import { environment } from 'src/environments/environment' -import { WebsocketConsumerStatusMessage } from '../data/websocket-consumer-status-message' +import { WebsocketDocumentsDeletedMessage } from '../data/websocket-documents-deleted-message' +import { WebsocketProgressMessage } from '../data/websocket-progress-message' import { SettingsService } from './settings.service' +export enum WebsocketStatusType { + STATUS_UPDATE = 'status_update', + DOCUMENTS_DELETED = 'documents_deleted', +} + // see ProgressStatusOptions in src/documents/plugins/helpers.py export enum FileStatusPhase { STARTED = 0, @@ -85,7 +91,7 @@ export class FileStatus { @Injectable({ providedIn: 'root', }) -export class ConsumerStatusService { +export class WebsocketStatusService { constructor(private settingsService: SettingsService) {} private statusWebSocket: WebSocket @@ -95,6 +101,7 @@ export class ConsumerStatusService { private documentDetectedSubject = new Subject() private documentConsumptionFinishedSubject = new Subject() private documentConsumptionFailedSubject = new Subject() + private documentDeletedSubject = new Subject() private get(taskId: string, filename?: string) { let status = @@ -145,63 +152,75 @@ export class ConsumerStatusService { this.statusWebSocket = new WebSocket( `${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/` ) - this.statusWebSocket.onmessage = (ev) => { - let statusMessage: WebsocketConsumerStatusMessage = JSON.parse(ev['data']) + this.statusWebSocket.onmessage = (ev: MessageEvent) => { + const { + type, + data: messageData, + }: { + type: WebsocketStatusType + data: WebsocketProgressMessage | WebsocketDocumentsDeletedMessage + } = JSON.parse(ev.data) - // fallback if backend didn't restrict message - if ( - statusMessage.owner_id && - statusMessage.owner_id !== this.settingsService.currentUser?.id && - !this.settingsService.currentUser?.is_superuser - ) { - return - } - - let statusMessageGet = this.get( - statusMessage.task_id, - statusMessage.filename - ) - let status = statusMessageGet.status - let created = statusMessageGet.created - - status.updateProgress( - FileStatusPhase.WORKING, - statusMessage.current_progress, - statusMessage.max_progress - ) - if ( - statusMessage.message && - statusMessage.message in FILE_STATUS_MESSAGES - ) { - status.message = FILE_STATUS_MESSAGES[statusMessage.message] - } else if (statusMessage.message) { - status.message = statusMessage.message - } - status.documentId = statusMessage.document_id - - if (statusMessage.status in FileStatusPhase) { - status.phase = FileStatusPhase[statusMessage.status] - } - - switch (status.phase) { - case FileStatusPhase.STARTED: - if (created) this.documentDetectedSubject.next(status) + switch (type) { + case WebsocketStatusType.DOCUMENTS_DELETED: + this.documentDeletedSubject.next(true) break - case FileStatusPhase.SUCCESS: - this.documentConsumptionFinishedSubject.next(status) - break - - case FileStatusPhase.FAILED: - this.documentConsumptionFailedSubject.next(status) - break - - default: + case WebsocketStatusType.STATUS_UPDATE: + this.handleProgressUpdate(messageData as WebsocketProgressMessage) break } } } + handleProgressUpdate(messageData: WebsocketProgressMessage) { + // fallback if backend didn't restrict message + if ( + messageData.owner_id && + messageData.owner_id !== this.settingsService.currentUser?.id && + !this.settingsService.currentUser?.is_superuser + ) { + return + } + + let statusMessageGet = this.get(messageData.task_id, messageData.filename) + let status = statusMessageGet.status + let created = statusMessageGet.created + + status.updateProgress( + FileStatusPhase.WORKING, + messageData.current_progress, + messageData.max_progress + ) + if (messageData.message && messageData.message in FILE_STATUS_MESSAGES) { + status.message = FILE_STATUS_MESSAGES[messageData.message] + } else if (messageData.message) { + status.message = messageData.message + } + status.documentId = messageData.document_id + + if (messageData.status in FileStatusPhase) { + status.phase = FileStatusPhase[messageData.status] + } + + switch (status.phase) { + case FileStatusPhase.STARTED: + if (created) this.documentDetectedSubject.next(status) + break + + case FileStatusPhase.SUCCESS: + this.documentConsumptionFinishedSubject.next(status) + break + + case FileStatusPhase.FAILED: + this.documentConsumptionFailedSubject.next(status) + break + + default: + break + } + } + fail(status: FileStatus, message: string) { status.message = message status.phase = FileStatusPhase.FAILED @@ -250,4 +269,8 @@ export class ConsumerStatusService { onDocumentDetected() { return this.documentDetectedSubject } + + onDocumentDeleted() { + return this.documentDeletedSubject + } } diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 0fbbdae0a..9db14f6c3 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -5,7 +5,7 @@ export const environment = { apiBaseUrl: document.baseURI + 'api/', apiVersion: '7', appTitle: 'Paperless-ngx', - version: '2.14.6', + version: '2.14.7', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', diff --git a/src-ui/src/main.ts b/src-ui/src/main.ts index 83aa12dc2..484a77c82 100644 --- a/src-ui/src/main.ts +++ b/src-ui/src/main.ts @@ -34,6 +34,7 @@ import { arrowRightShort, arrowUpRight, asterisk, + bell, bodyText, boxArrowUp, boxArrowUpRight, @@ -235,6 +236,7 @@ const icons = { arrowRightShort, arrowUpRight, asterisk, + bell, braces, bodyText, boxArrowUp, diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index 1257798b9..589356566 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -570,6 +570,10 @@ table.table { color: var(--bs-body-color); } +.toast { + --bs-toast-max-width: var(--pngx-toast-max-width); +} + .alert-primary { --bs-alert-color: var(--bs-primary); --bs-alert-bg: var(--pngx-primary-faded); diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss index 9f3c9cbe9..fc8c13d3b 100644 --- a/src-ui/src/theme.scss +++ b/src-ui/src/theme.scss @@ -24,6 +24,10 @@ --pngx-bg-alt2: var(--bs-gray-200); --pngx-bg-disabled: #f7f7f7; --pngx-focus-alpha: 0.3; + --pngx-toast-max-width: 360px; + @media screen and (min-width: 1024px) { + --pngx-toast-max-width: 450px; + } } // Dark text colors allow for maintain contrast with theme color changes diff --git a/src/documents/bulk_download.py b/src/documents/bulk_download.py index 25dfb5a14..5bdc3e74a 100644 --- a/src/documents/bulk_download.py +++ b/src/documents/bulk_download.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: class BulkArchiveStrategy: - def __init__(self, zipf: ZipFile, follow_formatting: bool = False) -> None: + def __init__(self, zipf: ZipFile, *, follow_formatting: bool = False) -> None: self.zipf: ZipFile = zipf if follow_formatting: self.make_unique_filename: Callable[..., Path | str] = ( @@ -22,6 +22,7 @@ class BulkArchiveStrategy: def _filename_only( self, doc: Document, + *, archive: bool = False, folder: str = "", ) -> str: @@ -33,7 +34,10 @@ class BulkArchiveStrategy: """ counter = 0 while True: - filename: str = folder + doc.get_public_filename(archive, counter) + filename: str = folder + doc.get_public_filename( + archive=archive, + counter=counter, + ) if filename in self.zipf.namelist(): counter += 1 else: @@ -42,6 +46,7 @@ class BulkArchiveStrategy: def _formatted_filepath( self, doc: Document, + *, archive: bool = False, folder: str = "", ) -> Path: diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index de43aed87..f6adfc8a9 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -12,6 +12,7 @@ from celery import shared_task from django.conf import settings from django.contrib.auth.models import User from django.db.models import Q +from django.utils import timezone from documents.data_models import ConsumableDocument from documents.data_models import DocumentMetadataOverrides @@ -23,6 +24,7 @@ from documents.models import Document from documents.models import DocumentType from documents.models import StoragePath from documents.permissions import set_permissions_for_object +from documents.plugins.helpers import DocumentsStatusManager from documents.tasks import bulk_update_documents from documents.tasks import consume_file from documents.tasks import update_document_content_maybe_archive_file @@ -177,6 +179,27 @@ def modify_custom_fields( field_id=field_id, defaults=defaults, ) + if custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK: + doc = Document.objects.get(id=doc_id) + reflect_doclinks(doc, custom_field, value) + + # For doc link fields that are being removed, remove symmetrical links + for doclink_being_removed_instance in CustomFieldInstance.objects.filter( + document_id__in=affected_docs, + field__id__in=remove_custom_fields, + field__data_type=CustomField.FieldDataType.DOCUMENTLINK, + value_document_ids__isnull=False, + ): + for target_doc_id in doclink_being_removed_instance.value: + remove_doclink( + document=Document.objects.get( + id=doclink_being_removed_instance.document.id, + ), + field=doclink_being_removed_instance.field, + target_doc_id=target_doc_id, + ) + + # Finally, remove the custom fields CustomFieldInstance.objects.filter( document_id__in=affected_docs, field_id__in=remove_custom_fields, @@ -197,6 +220,9 @@ def delete(doc_ids: list[int]) -> Literal["OK"]: with index.open_index_writer() as writer: for id in doc_ids: index.remove_document_by_id(writer, id) + + status_mgr = DocumentsStatusManager() + status_mgr.send_documents_deleted(doc_ids) except Exception as e: if "Data too long for column" in str(e): logger.warning( @@ -219,6 +245,7 @@ def reprocess(doc_ids: list[int]) -> Literal["OK"]: def set_permissions( doc_ids: list[int], set_permissions, + *, owner=None, merge=False, ) -> Literal["OK"]: @@ -283,6 +310,7 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]: def merge( doc_ids: list[int], + *, metadata_document_id: int | None = None, delete_originals: bool = False, user: User | None = None, @@ -361,6 +389,7 @@ def merge( def split( doc_ids: list[int], pages: list[list[int]], + *, delete_originals: bool = False, user: User | None = None, ) -> Literal["OK"]: @@ -447,3 +476,87 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]: logger.exception(f"Error deleting pages from document {doc.id}: {e}") return "OK" + + +def reflect_doclinks( + document: Document, + field: CustomField, + target_doc_ids: list[int], +): + """ + Add or remove 'symmetrical' links to `document` on all `target_doc_ids` + """ + + if target_doc_ids is None: + target_doc_ids = [] + + # Check if any documents are going to be removed from the current list of links and remove the symmetrical links + current_field_instance = CustomFieldInstance.objects.filter( + field=field, + document=document, + ).first() + if current_field_instance is not None and current_field_instance.value is not None: + for doc_id in current_field_instance.value: + if doc_id not in target_doc_ids: + remove_doclink( + document=document, + field=field, + target_doc_id=doc_id, + ) + + # Create an instance if target doc doesn't have this field or append it to an existing one + existing_custom_field_instances = { + custom_field.document_id: custom_field + for custom_field in CustomFieldInstance.objects.filter( + field=field, + document_id__in=target_doc_ids, + ) + } + custom_field_instances_to_create = [] + custom_field_instances_to_update = [] + for target_doc_id in target_doc_ids: + target_doc_field_instance = existing_custom_field_instances.get( + target_doc_id, + ) + if target_doc_field_instance is None: + custom_field_instances_to_create.append( + CustomFieldInstance( + document_id=target_doc_id, + field=field, + value_document_ids=[document.id], + ), + ) + elif target_doc_field_instance.value is None: + target_doc_field_instance.value_document_ids = [document.id] + custom_field_instances_to_update.append(target_doc_field_instance) + elif document.id not in target_doc_field_instance.value: + target_doc_field_instance.value_document_ids.append(document.id) + custom_field_instances_to_update.append(target_doc_field_instance) + + CustomFieldInstance.objects.bulk_create(custom_field_instances_to_create) + CustomFieldInstance.objects.bulk_update( + custom_field_instances_to_update, + ["value_document_ids"], + ) + Document.objects.filter(id__in=target_doc_ids).update(modified=timezone.now()) + + +def remove_doclink( + document: Document, + field: CustomField, + target_doc_id: int, +): + """ + Removes a 'symmetrical' link to `document` from the target document's existing custom field instance + """ + target_doc_field_instance = CustomFieldInstance.objects.filter( + document_id=target_doc_id, + field=field, + ).first() + if ( + target_doc_field_instance is not None + and document.id in target_doc_field_instance.value + ): + target_doc_field_instance.value.remove(document.id) + target_doc_field_instance.save() + Document.objects.filter(id=target_doc_id).update(modified=timezone.now()) diff --git a/src/documents/classifier.py b/src/documents/classifier.py index 72bf1f16c..5bc8be2c6 100644 --- a/src/documents/classifier.py +++ b/src/documents/classifier.py @@ -1,6 +1,7 @@ import logging import pickle import re +import time import warnings from collections.abc import Iterator from hashlib import sha256 @@ -141,6 +142,19 @@ class DocumentClassifier: ): raise IncompatibleClassifierVersionError("sklearn version update") + def set_last_checked(self) -> None: + # save a timestamp of the last time we checked for retraining to a file + with Path(settings.MODEL_FILE.with_suffix(".last_checked")).open("w") as f: + f.write(str(time.time())) + + def get_last_checked(self) -> float | None: + # load the timestamp of the last time we checked for retraining + try: + with Path(settings.MODEL_FILE.with_suffix(".last_checked")).open("r") as f: + return float(f.read()) + except FileNotFoundError: # pragma: no cover + return None + def save(self) -> None: target_file: Path = settings.MODEL_FILE target_file_temp: Path = target_file.with_suffix(".pickle.part") @@ -161,6 +175,7 @@ class DocumentClassifier: pickle.dump(self.storage_path_classifier, f) target_file_temp.rename(target_file) + self.set_last_checked() def train(self) -> bool: # Get non-inbox documents @@ -229,6 +244,7 @@ class DocumentClassifier: and self.last_doc_change_time >= latest_doc_change ) and self.last_auto_type_hash == hasher.digest(): logger.info("No updates since last training") + self.set_last_checked() # Set the classifier information into the cache # Caching for 50 minutes, so slightly less than the normal retrain time cache.set( diff --git a/src/documents/consumer.py b/src/documents/consumer.py index ec92ddba8..81739fa7a 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -4,6 +4,7 @@ import os import tempfile from enum import Enum from pathlib import Path +from typing import TYPE_CHECKING import magic from django.conf import settings @@ -47,6 +48,7 @@ from documents.templating.workflows import parse_w_workflow_placeholders from documents.utils import copy_basic_file_stats from documents.utils import copy_file_with_basic_stats from documents.utils import run_subprocess +from paperless_mail.parsers import MailDocumentParser class WorkflowTriggerPlugin( @@ -154,7 +156,11 @@ class ConsumerPlugin( """ Confirm the input file still exists where it should """ - if not os.path.isfile(self.input_doc.original_file): + if TYPE_CHECKING: + assert isinstance(self.input_doc.original_file, Path), ( + self.input_doc.original_file + ) + if not self.input_doc.original_file.is_file(): self._fail( ConsumerStatusShortMessage.FILE_NOT_FOUND, f"Cannot consume {self.input_doc.original_file}: File not found.", @@ -164,7 +170,7 @@ class ConsumerPlugin( """ Using the MD5 of the file, check this exact file doesn't already exist """ - with open(self.input_doc.original_file, "rb") as f: + with Path(self.input_doc.original_file).open("rb") as f: checksum = hashlib.md5(f.read()).hexdigest() existing_doc = Document.global_objects.filter( Q(checksum=checksum) | Q(archive_checksum=checksum), @@ -178,7 +184,7 @@ class ConsumerPlugin( log_msg += " Note: existing document is in the trash." if settings.CONSUMER_DELETE_DUPLICATES: - os.unlink(self.input_doc.original_file) + Path(self.input_doc.original_file).unlink() self._fail( msg, log_msg, @@ -237,7 +243,7 @@ class ConsumerPlugin( if not settings.PRE_CONSUME_SCRIPT: return - if not os.path.isfile(settings.PRE_CONSUME_SCRIPT): + if not Path(settings.PRE_CONSUME_SCRIPT).is_file(): self._fail( ConsumerStatusShortMessage.PRE_CONSUME_SCRIPT_NOT_FOUND, f"Configured pre-consume script " @@ -280,7 +286,7 @@ class ConsumerPlugin( if not settings.POST_CONSUME_SCRIPT: return - if not os.path.isfile(settings.POST_CONSUME_SCRIPT): + if not Path(settings.POST_CONSUME_SCRIPT).is_file(): self._fail( ConsumerStatusShortMessage.POST_CONSUME_SCRIPT_NOT_FOUND, f"Configured post-consume script " @@ -474,7 +480,18 @@ class ConsumerPlugin( ConsumerStatusShortMessage.PARSING_DOCUMENT, ) self.log.debug(f"Parsing {self.filename}...") - document_parser.parse(self.working_copy, mime_type, self.filename) + if ( + isinstance(document_parser, MailDocumentParser) + and self.input_doc.mailrule_id + ): + document_parser.parse( + self.working_copy, + mime_type, + self.filename, + self.input_doc.mailrule_id, + ) + else: + document_parser.parse(self.working_copy, mime_type, self.filename) self.log.debug(f"Generating thumbnail for {self.filename}...") self._send_progress( @@ -582,7 +599,7 @@ class ConsumerPlugin( document.thumbnail_path, ) - if archive_path and os.path.isfile(archive_path): + if archive_path and Path(archive_path).is_file(): document.archive_filename = generate_unique_filename( document, archive_filename=True, @@ -594,7 +611,7 @@ class ConsumerPlugin( document.archive_path, ) - with open(archive_path, "rb") as f: + with Path(archive_path).open("rb") as f: document.archive_checksum = hashlib.md5( f.read(), ).hexdigest() @@ -612,14 +629,14 @@ class ConsumerPlugin( self.unmodified_original.unlink() # https://github.com/jonaswinkler/paperless-ng/discussions/1037 - shadow_file = os.path.join( - os.path.dirname(self.input_doc.original_file), - "._" + os.path.basename(self.input_doc.original_file), + shadow_file = ( + Path(self.input_doc.original_file).parent + / f"._{Path(self.input_doc.original_file).name}" ) - if os.path.isfile(shadow_file): + if Path(shadow_file).is_file(): self.log.debug(f"Deleting file {shadow_file}") - os.unlink(shadow_file) + Path(shadow_file).unlink() except Exception as e: self._fail( @@ -704,7 +721,7 @@ class ConsumerPlugin( create_date = date self.log.debug(f"Creation date from parse_date: {create_date}") else: - stats = os.stat(self.input_doc.original_file) + stats = Path(self.input_doc.original_file).stat() create_date = timezone.make_aware( datetime.datetime.fromtimestamp(stats.st_mtime), ) @@ -800,7 +817,10 @@ class ConsumerPlugin( ) # adds to document def _write(self, storage_type, source, target): - with open(source, "rb") as read_file, open(target, "wb") as write_file: + with ( + Path(source).open("rb") as read_file, + Path(target).open("wb") as write_file, + ): write_file.write(read_file.read()) # Attempt to copy file's original stats, but it's ok if we can't diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index 4198ecabb..3d1a643df 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -43,7 +43,7 @@ def delete_empty_directories(directory, root): directory = os.path.normpath(os.path.dirname(directory)) -def generate_unique_filename(doc, archive_filename=False): +def generate_unique_filename(doc, *, archive_filename=False): """ Generates a unique filename for doc in settings.ORIGINALS_DIR. @@ -77,7 +77,7 @@ def generate_unique_filename(doc, archive_filename=False): while True: new_filename = generate_filename( doc, - counter, + counter=counter, archive_filename=archive_filename, ) if new_filename == old_filename: @@ -92,6 +92,7 @@ def generate_unique_filename(doc, archive_filename=False): def generate_filename( doc: Document, + *, counter=0, append_gpg=True, archive_filename=False, diff --git a/src/documents/filters.py b/src/documents/filters.py index 0ef7770fd..1ce782ee6 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -42,7 +42,19 @@ from documents.models import Tag CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"] ID_KWARGS = ["in", "exact"] INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"] -DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"] +DATE_KWARGS = [ + "year", + "month", + "day", + "date__gt", + "date__gte", + "gt", + "gte", + "date__lt", + "date__lte", + "lt", + "lte", +] CUSTOM_FIELD_QUERY_MAX_DEPTH = 10 CUSTOM_FIELD_QUERY_MAX_ATOMS = 20 @@ -86,7 +98,7 @@ class StoragePathFilterSet(FilterSet): class ObjectFilter(Filter): - def __init__(self, exclude=False, in_list=False, field_name=""): + def __init__(self, *, exclude=False, in_list=False, field_name=""): super().__init__() self.exclude = exclude self.in_list = in_list @@ -208,6 +220,14 @@ class CustomFieldsFilter(Filter): return qs +class MimeTypeFilter(Filter): + def filter(self, qs, value): + if value: + return qs.filter(mime_type__icontains=value) + else: + return qs + + class SelectField(serializers.CharField): def __init__(self, custom_field: CustomField): self._options = custom_field.extra_data["select_options"] @@ -704,6 +724,8 @@ class DocumentFilterSet(FilterSet): shared_by__id = SharedByUser() + mime_type = MimeTypeFilter() + class Meta: model = Document fields = { diff --git a/src/documents/index.py b/src/documents/index.py index 4c5afb505..4b11325ff 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -85,7 +85,7 @@ def get_schema() -> Schema: ) -def open_index(recreate=False) -> FileIndex: +def open_index(*, recreate=False) -> FileIndex: try: if exists_in(settings.INDEX_DIR) and not recreate: return open_dir(settings.INDEX_DIR, schema=get_schema()) @@ -101,7 +101,7 @@ def open_index(recreate=False) -> FileIndex: @contextmanager -def open_index_writer(optimize=False) -> AsyncWriter: +def open_index_writer(*, optimize=False) -> AsyncWriter: writer = AsyncWriter(open_index()) try: @@ -425,7 +425,7 @@ def autocomplete( def get_permissions_criterias(user: User | None = None) -> list: - user_criterias = [query.Term("has_owner", False)] + user_criterias = [query.Term("has_owner", text=False)] if user is not None: if user.is_superuser: # superusers see all docs user_criterias = [] diff --git a/src/documents/management/commands/convert_mariadb_uuid.py b/src/documents/management/commands/convert_mariadb_uuid.py index 4000e67cb..76ccf9e76 100644 --- a/src/documents/management/commands/convert_mariadb_uuid.py +++ b/src/documents/management/commands/convert_mariadb_uuid.py @@ -9,7 +9,7 @@ class Command(BaseCommand): # This code is taken almost entirely from https://github.com/wagtail/wagtail/pull/11912 with all credit to the original author. help = "Converts UUID columns from char type to the native UUID type used in MariaDB 10.7+ and Django 5.0+." - def convert_field(self, model, field_name, null=False): + def convert_field(self, model, field_name, *, null=False): if model._meta.get_field(field_name).model != model: # pragma: no cover # Field is inherited from a parent model return diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index 6b2706733..36dcc7706 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -248,15 +248,15 @@ class Command(BaseCommand): return if settings.CONSUMER_POLLING == 0 and INotify: - self.handle_inotify(directory, recursive, options["testing"]) + self.handle_inotify(directory, recursive, is_testing=options["testing"]) else: if INotify is None and settings.CONSUMER_POLLING == 0: # pragma: no cover logger.warning("Using polling as INotify import failed") - self.handle_polling(directory, recursive, options["testing"]) + self.handle_polling(directory, recursive, is_testing=options["testing"]) logger.debug("Consumer exiting.") - def handle_polling(self, directory, recursive, is_testing: bool): + def handle_polling(self, directory, recursive, *, is_testing: bool): logger.info(f"Polling directory for changes: {directory}") timeout = None @@ -283,7 +283,7 @@ class Command(BaseCommand): observer.stop() observer.join() - def handle_inotify(self, directory, recursive, is_testing: bool): + def handle_inotify(self, directory, recursive, *, is_testing: bool): logger.info(f"Using inotify to watch directory for changes: {directory}") timeout_ms = None diff --git a/src/documents/migrations/1012_fix_archive_files.py b/src/documents/migrations/1012_fix_archive_files.py index 1d12c439b..46951471e 100644 --- a/src/documents/migrations/1012_fix_archive_files.py +++ b/src/documents/migrations/1012_fix_archive_files.py @@ -84,7 +84,7 @@ def source_path(doc): return os.path.join(settings.ORIGINALS_DIR, fname) -def generate_unique_filename(doc, archive_filename=False): +def generate_unique_filename(doc, *, archive_filename=False): if archive_filename: old_filename = doc.archive_filename root = settings.ARCHIVE_DIR @@ -97,7 +97,7 @@ def generate_unique_filename(doc, archive_filename=False): while True: new_filename = generate_filename( doc, - counter, + counter=counter, archive_filename=archive_filename, ) if new_filename == old_filename: @@ -110,7 +110,7 @@ def generate_unique_filename(doc, archive_filename=False): return new_filename -def generate_filename(doc, counter=0, append_gpg=True, archive_filename=False): +def generate_filename(doc, *, counter=0, append_gpg=True, archive_filename=False): path = "" try: diff --git a/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py new file mode 100644 index 000000000..c5a6bb90e --- /dev/null +++ b/src/documents/migrations/1062_alter_savedviewfilterrule_rule_type.py @@ -0,0 +1,70 @@ +# Generated by Django 5.1.4 on 2025-02-06 05:54 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1061_workflowactionwebhook_as_json"), + ] + + operations = [ + migrations.AlterField( + model_name="savedviewfilterrule", + name="rule_type", + field=models.PositiveIntegerField( + choices=[ + (0, "title contains"), + (1, "content contains"), + (2, "ASN is"), + (3, "correspondent is"), + (4, "document type is"), + (5, "is in inbox"), + (6, "has tag"), + (7, "has any tag"), + (8, "created before"), + (9, "created after"), + (10, "created year is"), + (11, "created month is"), + (12, "created day is"), + (13, "added before"), + (14, "added after"), + (15, "modified before"), + (16, "modified after"), + (17, "does not have tag"), + (18, "does not have ASN"), + (19, "title or content contains"), + (20, "fulltext query"), + (21, "more like this"), + (22, "has tags in"), + (23, "ASN greater than"), + (24, "ASN less than"), + (25, "storage path is"), + (26, "has correspondent in"), + (27, "does not have correspondent in"), + (28, "has document type in"), + (29, "does not have document type in"), + (30, "has storage path in"), + (31, "does not have storage path in"), + (32, "owner is"), + (33, "has owner in"), + (34, "does not have owner"), + (35, "does not have owner in"), + (36, "has custom field value"), + (37, "is shared by me"), + (38, "has custom fields"), + (39, "has custom field in"), + (40, "does not have custom field in"), + (41, "does not have custom field"), + (42, "custom fields query"), + (43, "created to"), + (44, "created from"), + (45, "added to"), + (46, "added from"), + (47, "mime type is"), + ], + verbose_name="rule type", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 79856b837..4c644c14c 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -337,7 +337,7 @@ class Document(SoftDeleteModel, ModelWithOwner): def archive_file(self): return open(self.archive_path, "rb") - def get_public_filename(self, archive=False, counter=0, suffix=None) -> str: + def get_public_filename(self, *, archive=False, counter=0, suffix=None) -> str: """ Returns a sanitized filename for the document, not including any paths. """ @@ -522,6 +522,11 @@ class SavedViewFilterRule(models.Model): (40, _("does not have custom field in")), (41, _("does not have custom field")), (42, _("custom fields query")), + (43, _("created to")), + (44, _("created from")), + (45, _("added to")), + (46, _("added from")), + (47, _("mime type is")), ] saved_view = models.ForeignKey( diff --git a/src/documents/parsers.py b/src/documents/parsers.py index 2d73dc63f..28d903fdd 100644 --- a/src/documents/parsers.py +++ b/src/documents/parsers.py @@ -41,7 +41,7 @@ DATE_REGEX = re.compile( r"(\b|(?!=([_-])))(\d{1,2}[\. ]+[a-zéûäëčžúřěáíóńźçŞğü]{3,9} \d{4}|[a-zéûäëčžúřěáíóńźçŞğü]{3,9} \d{1,2}, \d{4})(\b|(?=([_-])))|" r"(\b|(?!=([_-])))([^\W\d_]{3,9} \d{1,2}, (\d{4}))(\b|(?=([_-])))|" r"(\b|(?!=([_-])))([^\W\d_]{3,9} \d{4})(\b|(?=([_-])))|" - r"(\b|(?!=([_-])))(\d{1,2}[^ ]{2}[\. ]+[^ ]{3,9}[ \.\/-]\d{4})(\b|(?=([_-])))|" + r"(\b|(?!=([_-])))(\d{1,2}[^ 0-9]{2}[\. ]+[^ ]{3,9}[ \.\/-]\d{4})(\b|(?=([_-])))|" r"(\b|(?!=([_-])))(\b\d{1,2}[ \.\/-][a-zéûäëčžúřěáíóńźçŞğü]{3}[ \.\/-]\d{4})(\b|(?=([_-])))", re.IGNORECASE, ) @@ -133,6 +133,7 @@ def get_parser_class_for_mime_type(mime_type: str) -> type["DocumentParser"] | N def run_convert( input_file, output_file, + *, density=None, scale=None, alpha=None, diff --git a/src/documents/permissions.py b/src/documents/permissions.py index 464916ad4..4380c6994 100644 --- a/src/documents/permissions.py +++ b/src/documents/permissions.py @@ -58,7 +58,7 @@ def get_groups_with_only_permission(obj, codename): return Group.objects.filter(id__in=group_object_perm_group_ids).distinct() -def set_permissions_for_object(permissions: list[str], object, merge: bool = False): +def set_permissions_for_object(permissions: list[str], object, *, merge: bool = False): """ Set permissions for an object. The permissions are given as a list of strings in the format "action_modelname", e.g. "view_document". diff --git a/src/documents/plugins/helpers.py b/src/documents/plugins/helpers.py index 20380b852..3315ec60e 100644 --- a/src/documents/plugins/helpers.py +++ b/src/documents/plugins/helpers.py @@ -15,16 +15,14 @@ class ProgressStatusOptions(str, enum.Enum): FAILED = "FAILED" -class ProgressManager: +class BaseStatusManager: """ Handles sending of progress information via the channel layer, with proper management of the open/close of the layer to ensure messages go out and everything is cleaned up """ - def __init__(self, filename: str, task_id: str | None = None) -> None: - self.filename = filename + def __init__(self) -> None: self._channel: RedisPubSubChannelLayer | None = None - self.task_id = task_id def __enter__(self): self.open() @@ -49,6 +47,24 @@ class ProgressManager: async_to_sync(self._channel.flush) self._channel = None + def send(self, payload: dict[str, str | int | None]) -> None: + # Ensure the layer is open + self.open() + + # Just for IDEs + if TYPE_CHECKING: + assert self._channel is not None + + # Construct and send the update + async_to_sync(self._channel.group_send)("status_updates", payload) + + +class ProgressManager(BaseStatusManager): + def __init__(self, filename: str | None = None, task_id: str | None = None) -> None: + super().__init__() + self.filename = filename + self.task_id = task_id + def send_progress( self, status: ProgressStatusOptions, @@ -57,13 +73,6 @@ class ProgressManager: max_progress: int, extra_args: dict[str, str | int | None] | None = None, ) -> None: - # Ensure the layer is open - self.open() - - # Just for IDEs - if TYPE_CHECKING: - assert self._channel is not None - payload = { "type": "status_update", "data": { @@ -78,5 +87,16 @@ class ProgressManager: if extra_args is not None: payload["data"].update(extra_args) - # Construct and send the update - async_to_sync(self._channel.group_send)("status_updates", payload) + self.send(payload) + + +class DocumentsStatusManager(BaseStatusManager): + def send_documents_deleted(self, documents: list[int]) -> None: + payload = { + "type": "documents_deleted", + "data": { + "documents": documents, + }, + } + + self.send(payload) diff --git a/src/documents/sanity_checker.py b/src/documents/sanity_checker.py index 9d44ff345..28d2024e7 100644 --- a/src/documents/sanity_checker.py +++ b/src/documents/sanity_checker.py @@ -57,7 +57,7 @@ class SanityCheckFailedException(Exception): pass -def check_sanity(progress=False) -> SanityCheckMessages: +def check_sanity(*, progress=False) -> SanityCheckMessages: messages = SanityCheckMessages() present_files = { diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 9587514f5..6a0a1eec1 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -16,7 +16,6 @@ from django.core.validators import DecimalValidator from django.core.validators import MaxLengthValidator from django.core.validators import RegexValidator from django.core.validators import integer_validator -from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.text import slugify from django.utils.translation import gettext as _ @@ -727,7 +726,7 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer): if custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK: # prior to update so we can look for any docs that are going to be removed - self.reflect_doclinks(document, custom_field, validated_data["value"]) + bulk_edit.reflect_doclinks(document, custom_field, validated_data["value"]) # Actually update or create the instance, providing the value # to fill in the correct attribute based on the type @@ -847,89 +846,6 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer): return ret - def reflect_doclinks( - self, - document: Document, - field: CustomField, - target_doc_ids: list[int], - ): - """ - Add or remove 'symmetrical' links to `document` on all `target_doc_ids` - """ - - if target_doc_ids is None: - target_doc_ids = [] - - # Check if any documents are going to be removed from the current list of links and remove the symmetrical links - current_field_instance = CustomFieldInstance.objects.filter( - field=field, - document=document, - ).first() - if ( - current_field_instance is not None - and current_field_instance.value is not None - ): - for doc_id in current_field_instance.value: - if doc_id not in target_doc_ids: - self.remove_doclink(document, field, doc_id) - - # Create an instance if target doc doesn't have this field or append it to an existing one - existing_custom_field_instances = { - custom_field.document_id: custom_field - for custom_field in CustomFieldInstance.objects.filter( - field=field, - document_id__in=target_doc_ids, - ) - } - custom_field_instances_to_create = [] - custom_field_instances_to_update = [] - for target_doc_id in target_doc_ids: - target_doc_field_instance = existing_custom_field_instances.get( - target_doc_id, - ) - if target_doc_field_instance is None: - custom_field_instances_to_create.append( - CustomFieldInstance( - document_id=target_doc_id, - field=field, - value_document_ids=[document.id], - ), - ) - elif target_doc_field_instance.value is None: - target_doc_field_instance.value_document_ids = [document.id] - custom_field_instances_to_update.append(target_doc_field_instance) - elif document.id not in target_doc_field_instance.value: - target_doc_field_instance.value_document_ids.append(document.id) - custom_field_instances_to_update.append(target_doc_field_instance) - - CustomFieldInstance.objects.bulk_create(custom_field_instances_to_create) - CustomFieldInstance.objects.bulk_update( - custom_field_instances_to_update, - ["value_document_ids"], - ) - Document.objects.filter(id__in=target_doc_ids).update(modified=timezone.now()) - - @staticmethod - def remove_doclink( - document: Document, - field: CustomField, - target_doc_id: int, - ): - """ - Removes a 'symmetrical' link to `document` from the target document's existing custom field instance - """ - target_doc_field_instance = CustomFieldInstance.objects.filter( - document_id=target_doc_id, - field=field, - ).first() - if ( - target_doc_field_instance is not None - and document.id in target_doc_field_instance.value - ): - target_doc_field_instance.value.remove(document.id) - target_doc_field_instance.save() - Document.objects.filter(id=target_doc_id).update(modified=timezone.now()) - class Meta: model = CustomFieldInstance fields = [ @@ -1031,7 +947,7 @@ class DocumentSerializer( ): # Doc link field is being removed entirely for doc_id in custom_field_instance.value: - CustomFieldInstanceSerializer.remove_doclink( + bulk_edit.remove_doclink( instance, custom_field_instance.field, doc_id, diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index e60585a37..1c4d36694 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -85,6 +85,7 @@ def _suggestion_printer( def set_correspondent( sender, document: Document, + *, logging_group=None, classifier: DocumentClassifier | None = None, replace=False, @@ -140,6 +141,7 @@ def set_correspondent( def set_document_type( sender, document: Document, + *, logging_group=None, classifier: DocumentClassifier | None = None, replace=False, @@ -196,6 +198,7 @@ def set_document_type( def set_tags( sender, document: Document, + *, logging_group=None, classifier: DocumentClassifier | None = None, replace=False, @@ -251,6 +254,7 @@ def set_tags( def set_storage_path( sender, document: Document, + *, logging_group=None, classifier: DocumentClassifier | None = None, replace=False, @@ -353,7 +357,7 @@ def cleanup_document_deletion(sender, instance, **kwargs): f"{filename} could not be deleted: {e}", ) elif filename and not os.path.isfile(filename): - logger.warn(f"Expected {filename} tp exist, but it did not") + logger.warning(f"Expected {filename} to exist, but it did not") delete_empty_directories( os.path.dirname(instance.source_path), diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 8b0cbf249..d8539d1ab 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -63,7 +63,7 @@ def index_optimize(): writer.commit(optimize=True) -def index_reindex(progress_bar_disable=False): +def index_reindex(*, progress_bar_disable=False): documents = Document.objects.all() ix = index.open_index(recreate=True) diff --git a/src/documents/tests/test_api_bulk_download.py b/src/documents/tests/test_api_bulk_download.py index 31bf182b5..a7e8f5df3 100644 --- a/src/documents/tests/test_api_bulk_download.py +++ b/src/documents/tests/test_api_bulk_download.py @@ -1,7 +1,6 @@ import datetime import io import json -import os import shutil import zipfile @@ -15,9 +14,10 @@ from documents.models import Correspondent from documents.models import Document from documents.models import DocumentType from documents.tests.utils import DirectoriesMixin +from documents.tests.utils import SampleDirMixin -class TestBulkDownload(DirectoriesMixin, APITestCase): +class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase): ENDPOINT = "/api/documents/bulk_download/" def setUp(self): @@ -51,22 +51,10 @@ class TestBulkDownload(DirectoriesMixin, APITestCase): archive_checksum="D", ) - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - self.doc2.source_path, - ) - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "simple.png"), - self.doc2b.source_path, - ) - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"), - self.doc3.source_path, - ) - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"), - self.doc3.archive_path, - ) + shutil.copy(self.SAMPLE_DIR / "simple.pdf", self.doc2.source_path) + shutil.copy(self.SAMPLE_DIR / "simple.png", self.doc2b.source_path) + shutil.copy(self.SAMPLE_DIR / "simple.jpg", self.doc3.source_path) + shutil.copy(self.SAMPLE_DIR / "test_with_bom.pdf", self.doc3.archive_path) def test_download_originals(self): response = self.client.post( diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index 70db15217..7010c5095 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -1,5 +1,4 @@ import datetime -import os import shutil import tempfile import uuid @@ -8,6 +7,7 @@ from binascii import hexlify from datetime import date from datetime import timedelta from pathlib import Path +from typing import TYPE_CHECKING from unittest import mock import celery @@ -171,19 +171,18 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): content = b"This is a test" content_thumbnail = b"thumbnail content" - with open(filename, "wb") as f: + with Path(filename).open("wb") as f: f.write(content) doc = Document.objects.create( title="none", - filename=os.path.basename(filename), + filename=Path(filename).name, mime_type="application/pdf", ) - with open( - os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"), - "wb", - ) as f: + if TYPE_CHECKING: + assert isinstance(self.dirs.thumbnail_dir, Path), self.dirs.thumbnail_dir + with (self.dirs.thumbnail_dir / f"{doc.pk:07d}.webp").open("wb") as f: f.write(content_thumbnail) response = self.client.get(f"/api/documents/{doc.pk}/download/") @@ -217,7 +216,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): content = b"This is a test" content_thumbnail = b"thumbnail content" - with open(filename, "wb") as f: + with Path(filename).open("wb") as f: f.write(content) user1 = User.objects.create_user(username="test1") @@ -229,15 +228,12 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): doc = Document.objects.create( title="none", - filename=os.path.basename(filename), + filename=Path(filename).name, mime_type="application/pdf", owner=user1, ) - with open( - os.path.join(self.dirs.thumbnail_dir, f"{doc.pk:07d}.webp"), - "wb", - ) as f: + with (Path(self.dirs.thumbnail_dir) / f"{doc.pk:07d}.webp").open("wb") as f: f.write(content_thumbnail) response = self.client.get(f"/api/documents/{doc.pk}/download/") @@ -272,10 +268,10 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): mime_type="application/pdf", ) - with open(doc.source_path, "wb") as f: + with Path(doc.source_path).open("wb") as f: f.write(content) - with open(doc.archive_path, "wb") as f: + with Path(doc.archive_path).open("wb") as f: f.write(content_archive) response = self.client.get(f"/api/documents/{doc.pk}/download/") @@ -305,7 +301,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): def test_document_actions_not_existing_file(self): doc = Document.objects.create( title="none", - filename=os.path.basename("asd"), + filename=Path("asd").name, mime_type="application/pdf", ) @@ -643,6 +639,13 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(len(results), 1) self.assertEqual(results[0]["id"], doc3.id) + response = self.client.get( + "/api/documents/?mime_type=pdf", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + def test_custom_field_select_filter(self): """ GIVEN: @@ -1026,10 +1029,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f}, @@ -1061,10 +1061,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", { @@ -1095,10 +1092,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"documenst": f}, @@ -1111,10 +1105,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.zip"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.zip").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f}, @@ -1127,10 +1118,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "title": "my custom title"}, @@ -1152,10 +1140,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): ) c = Correspondent.objects.create(name="test-corres") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "correspondent": c.id}, @@ -1176,10 +1161,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "correspondent": 3456}, @@ -1194,10 +1176,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): ) dt = DocumentType.objects.create(name="invoice") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "document_type": dt.id}, @@ -1218,10 +1197,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "document_type": 34578}, @@ -1236,10 +1212,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): ) sp = StoragePath.objects.create(name="invoices") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "storage_path": sp.id}, @@ -1260,10 +1233,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "storage_path": 34578}, @@ -1279,10 +1249,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): t1 = Tag.objects.create(name="tag1") t2 = Tag.objects.create(name="tag2") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "tags": [t2.id, t1.id]}, @@ -1305,10 +1272,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): t1 = Tag.objects.create(name="tag1") t2 = Tag.objects.create(name="tag2") - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "tags": [t2.id, t1.id, 734563]}, @@ -1332,10 +1296,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): 0, tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "created": created}, @@ -1353,10 +1314,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f, "archive_serial_number": 500}, @@ -1385,10 +1343,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): data_type=CustomField.FieldDataType.STRING, ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", { @@ -1417,10 +1372,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): id=str(uuid.uuid4()), ) - with open( - os.path.join(os.path.dirname(__file__), "samples", "invalid_pdf.pdf"), - "rb", - ) as f: + with (Path(__file__).parent / "samples" / "invalid_pdf.pdf").open("rb") as f: response = self.client.post( "/api/documents/post_document/", {"document": f}, @@ -1437,14 +1389,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): archive_filename="archive.pdf", ) - source_file = os.path.join( - os.path.dirname(__file__), - "samples", - "documents", - "thumbnails", - "0000001.webp", + source_file: Path = ( + Path(__file__).parent + / "samples" + / "documents" + / "thumbnails" + / "0000001.webp" ) - archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf") + archive_file: Path = Path(__file__).parent / "samples" / "simple.pdf" shutil.copy(source_file, doc.source_path) shutil.copy(archive_file, doc.archive_path) @@ -1460,8 +1412,8 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertGreater(len(meta["archive_metadata"]), 0) self.assertEqual(meta["media_filename"], "file.pdf") self.assertEqual(meta["archive_media_filename"], "archive.pdf") - self.assertEqual(meta["original_size"], os.stat(source_file).st_size) - self.assertEqual(meta["archive_size"], os.stat(archive_file).st_size) + self.assertEqual(meta["original_size"], Path(source_file).stat().st_size) + self.assertEqual(meta["archive_size"], Path(archive_file).stat().st_size) response = self.client.get(f"/api/documents/{doc.pk}/metadata/") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1477,10 +1429,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): mime_type="application/pdf", ) - shutil.copy( - os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), - doc.source_path, - ) + shutil.copy(Path(__file__).parent / "samples" / "simple.pdf", doc.source_path) response = self.client.get(f"/api/documents/{doc.pk}/metadata/") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1939,9 +1888,9 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): def test_get_logs(self): log_data = "test\ntest2\n" - with open(os.path.join(settings.LOGGING_DIR, "mail.log"), "w") as f: + with (Path(settings.LOGGING_DIR) / "mail.log").open("w") as f: f.write(log_data) - with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: + with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f: f.write(log_data) response = self.client.get("/api/logs/") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1949,7 +1898,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): def test_get_logs_only_when_exist(self): log_data = "test\ntest2\n" - with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: + with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f: f.write(log_data) response = self.client.get("/api/logs/") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1966,7 +1915,7 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): def test_get_log(self): log_data = "test\ntest2\n" - with open(os.path.join(settings.LOGGING_DIR, "paperless.log"), "w") as f: + with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f: f.write(log_data) response = self.client.get("/api/logs/paperless/") self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/documents/tests/test_api_filter_by_custom_fields.py b/src/documents/tests/test_api_filter_by_custom_fields.py index deb97bf29..70d43dfde 100644 --- a/src/documents/tests/test_api_filter_by_custom_fields.py +++ b/src/documents/tests/test_api_filter_by_custom_fields.py @@ -165,6 +165,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase): self, query: list, reference_predicate: Callable[[DocumentWrapper], bool], + *, match_nothing_ok=False, ): """ diff --git a/src/documents/tests/test_bulk_edit.py b/src/documents/tests/test_bulk_edit.py index 03c177343..4a7145d34 100644 --- a/src/documents/tests/test_bulk_edit.py +++ b/src/documents/tests/test_bulk_edit.py @@ -268,7 +268,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase): ) cf3 = CustomField.objects.create( name="cf3", - data_type=CustomField.FieldDataType.STRING, + data_type=CustomField.FieldDataType.DOCUMENTLINK, ) CustomFieldInstance.objects.create( document=self.doc2, @@ -284,7 +284,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase): ) bulk_edit.modify_custom_fields( [self.doc1.id, self.doc2.id], - add_custom_fields={cf2.id: None, cf3.id: "value"}, + add_custom_fields={cf2.id: None, cf3.id: [self.doc3.id]}, remove_custom_fields=[cf.id], ) @@ -301,7 +301,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase): ) self.assertEqual( self.doc1.custom_fields.get(field=cf3).value, - "value", + [self.doc3.id], ) self.assertEqual( self.doc2.custom_fields.count(), @@ -309,13 +309,33 @@ class TestBulkEdit(DirectoriesMixin, TestCase): ) self.assertEqual( self.doc2.custom_fields.get(field=cf3).value, - "value", + [self.doc3.id], + ) + # assert reflect document link + self.assertEqual( + self.doc3.custom_fields.first().value, + [self.doc2.id, self.doc1.id], ) self.async_task.assert_called_once() args, kwargs = self.async_task.call_args self.assertCountEqual(kwargs["document_ids"], [self.doc1.id, self.doc2.id]) + # removal of document link cf, should also remove symmetric link + bulk_edit.modify_custom_fields( + [self.doc3.id], + add_custom_fields={}, + remove_custom_fields=[cf3.id], + ) + self.assertNotIn( + self.doc3.id, + self.doc1.custom_fields.filter(field=cf3).first().value, + ) + self.assertNotIn( + self.doc3.id, + self.doc2.custom_fields.filter(field=cf3).first().value, + ) + def test_delete(self): self.assertEqual(Document.objects.count(), 5) bulk_edit.delete([self.doc1.id, self.doc2.id]) @@ -515,7 +535,12 @@ class TestPDFActions(DirectoriesMixin, TestCase): metadata_document_id = self.doc1.id user = User.objects.create(username="test_user") - result = bulk_edit.merge(doc_ids, None, False, user) + result = bulk_edit.merge( + doc_ids, + metadata_document_id=None, + delete_originals=False, + user=user, + ) expected_filename = ( f"{'_'.join([str(doc_id) for doc_id in doc_ids])[:100]}_merged.pdf" @@ -618,7 +643,7 @@ class TestPDFActions(DirectoriesMixin, TestCase): doc_ids = [self.doc2.id] pages = [[1, 2], [3]] user = User.objects.create(username="test_user") - result = bulk_edit.split(doc_ids, pages, False, user) + result = bulk_edit.split(doc_ids, pages, delete_originals=False, user=user) self.assertEqual(mock_consume_file.call_count, 2) consume_file_args, _ = mock_consume_file.call_args self.assertEqual(consume_file_args[1].title, "B (split 2)") diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py index aa452e15b..6f576ab24 100644 --- a/src/documents/tests/test_consumer.py +++ b/src/documents/tests/test_consumer.py @@ -21,6 +21,7 @@ from guardian.core import ObjectPermissionChecker from documents.consumer import ConsumerError from documents.data_models import DocumentMetadataOverrides +from documents.data_models import DocumentSource from documents.models import Correspondent from documents.models import CustomField from documents.models import Document @@ -35,6 +36,8 @@ from documents.tasks import sanity_check from documents.tests.utils import DirectoriesMixin from documents.tests.utils import FileSystemAssertsMixin from documents.tests.utils import GetConsumerMixin +from paperless_mail.models import MailRule +from paperless_mail.parsers import MailDocumentParser class TestAttributes(UnittestTestCase): @@ -233,7 +236,7 @@ class FaultyGenericExceptionParser(_BaseTestParser): raise Exception("Generic exception.") -def fake_magic_from_file(file, mime=False): +def fake_magic_from_file(file, *, mime=False): if mime: if file.name.startswith("invalid_pdf"): return "application/octet-stream" @@ -243,6 +246,8 @@ def fake_magic_from_file(file, mime=False): return "image/png" elif os.path.splitext(file)[1] == ".webp": return "image/webp" + elif os.path.splitext(file)[1] == ".eml": + return "message/rfc822" else: return "unknown" else: @@ -975,6 +980,59 @@ class TestConsumer( self.assertEqual(command[0], "qpdf") self.assertEqual(command[1], "--replace-input") + @mock.patch("paperless_mail.models.MailRule.objects.get") + @mock.patch("paperless_mail.parsers.MailDocumentParser.parse") + @mock.patch("documents.parsers.document_consumer_declaration.send") + def test_mail_parser_receives_mailrule( + self, + mock_consumer_declaration_send: mock.Mock, + mock_mail_parser_parse: mock.Mock, + mock_mailrule_get: mock.Mock, + ): + """ + GIVEN: + - A mail document from a mail rule + WHEN: + - The consumer is run + THEN: + - The mail parser should receive the mail rule + """ + mock_consumer_declaration_send.return_value = [ + ( + None, + { + "parser": MailDocumentParser, + "mime_types": {"message/rfc822": ".eml"}, + "weight": 0, + }, + ), + ] + mock_mailrule_get.return_value = mock.Mock( + pdf_layout=MailRule.PdfLayout.HTML_ONLY, + ) + with self.get_consumer( + filepath=( + Path(__file__).parent.parent.parent + / Path("paperless_mail") + / Path("tests") + / Path("samples") + ).resolve() + / "html.eml", + source=DocumentSource.MailFetch, + mailrule_id=1, + ) as consumer: + # fails because no gotenberg + with self.assertRaises( + ConsumerError, + ): + consumer.run() + mock_mail_parser_parse.assert_called_once_with( + consumer.working_copy, + "message/rfc822", + file_name="sample.pdf", + mailrule=mock_mailrule_get.return_value, + ) + @mock.patch("documents.consumer.magic.from_file", fake_magic_from_file) class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase): diff --git a/src/documents/tests/test_delayedquery.py b/src/documents/tests/test_delayedquery.py index 1895bd6c6..3ee4fb15d 100644 --- a/src/documents/tests/test_delayedquery.py +++ b/src/documents/tests/test_delayedquery.py @@ -10,7 +10,7 @@ class TestDelayedQuery(TestCase): super().setUp() # all tests run without permission criteria, so has_no_owner query will always # be appended. - self.has_no_owner = query.Or([query.Term("has_owner", False)]) + self.has_no_owner = query.Or([query.Term("has_owner", text=False)]) def _get_testset__id__in(self, param, field): return ( @@ -43,12 +43,12 @@ class TestDelayedQuery(TestCase): def test_get_permission_criteria(self): # tests contains tuples of user instances and the expected filter tests = ( - (None, [query.Term("has_owner", False)]), + (None, [query.Term("has_owner", text=False)]), (User(42, username="foo", is_superuser=True), []), ( User(42, username="foo", is_superuser=False), [ - query.Term("has_owner", False), + query.Term("has_owner", text=False), query.Term("owner_id", 42), query.Term("viewer_id", "42"), ], diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py index 7e2707403..808216d3d 100644 --- a/src/documents/tests/test_management_consumer.py +++ b/src/documents/tests/test_management_consumer.py @@ -93,7 +93,7 @@ class ConsumerThreadMixin(DocumentConsumeDelayMixin): else: print("Consumed a perfectly valid file.") # noqa: T201 - def slow_write_file(self, target, incomplete=False): + def slow_write_file(self, target, *, incomplete=False): with open(self.sample_file, "rb") as f: pdf_bytes = f.read() diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index 0a79b6cd7..eec2fcd4b 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -188,7 +188,7 @@ class TestExportImport( return manifest - def test_exporter(self, use_filename_format=False): + def test_exporter(self, *, use_filename_format=False): shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.copytree( os.path.join(os.path.dirname(__file__), "samples", "documents"), diff --git a/src/documents/tests/test_matchables.py b/src/documents/tests/test_matchables.py index 9ca23e53d..180cf77ed 100644 --- a/src/documents/tests/test_matchables.py +++ b/src/documents/tests/test_matchables.py @@ -23,6 +23,7 @@ class _TestMatchingBase(TestCase): match_algorithm: str, should_match: Iterable[str], no_match: Iterable[str], + *, case_sensitive: bool = False, ): for klass in (Tag, Correspondent, DocumentType): diff --git a/src/documents/tests/test_migration_workflows.py b/src/documents/tests/test_migration_workflows.py index 69f5ed5fb..989518818 100644 --- a/src/documents/tests/test_migration_workflows.py +++ b/src/documents/tests/test_migration_workflows.py @@ -8,7 +8,7 @@ class TestMigrateWorkflow(TestMigrations): dependencies = ( ( "paperless_mail", - "0028_alter_mailaccount_password_and_more", + "0029_mailrule_pdf_layout", ), ) diff --git a/src/documents/tests/utils.py b/src/documents/tests/utils.py index 739433bb6..fc50b3948 100644 --- a/src/documents/tests/utils.py +++ b/src/documents/tests/utils.py @@ -340,11 +340,16 @@ class GetConsumerMixin: filepath: Path, overrides: DocumentMetadataOverrides | None = None, source: DocumentSource = DocumentSource.ConsumeFolder, + mailrule_id: int | None = None, ) -> Generator[ConsumerPlugin, None, None]: # Store this for verification self.status = DummyProgressManager(filepath.name, None) reader = ConsumerPlugin( - ConsumableDocument(source, original_file=filepath), + ConsumableDocument( + source, + original_file=filepath, + mailrule_id=mailrule_id or None, + ), overrides or DocumentMetadataOverrides(), self.status, # type: ignore self.dirs.scratch_dir, diff --git a/src/documents/views.py b/src/documents/views.py index 109a71b72..a856883f3 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -15,7 +15,6 @@ from urllib.parse import quote from urllib.parse import urlparse import pathvalidate -from django.apps import apps from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import User @@ -1991,7 +1990,7 @@ class BulkDownloadView(GenericAPIView): strategy_class = ArchiveOnlyStrategy with zipfile.ZipFile(temp.name, "w", compression) as zipf: - strategy = strategy_class(zipf, follow_filename_format) + strategy = strategy_class(zipf, follow_formatting=follow_filename_format) for document in documents: strategy.add_document(document) @@ -2291,7 +2290,7 @@ class SharedLinkView(View): ) -def serve_file(doc: Document, use_archive: bool, disposition: str): +def serve_file(*, doc: Document, use_archive: bool, disposition: str): if use_archive: file_handle = doc.archive_file filename = doc.get_public_filename(archive=True) @@ -2675,18 +2674,14 @@ class SystemStatusView(PassUserMixin): classifier_status = "WARNING" raise FileNotFoundError(classifier_error) classifier_status = "OK" - task_result_model = apps.get_model("django_celery_results", "taskresult") - result = ( - task_result_model.objects.filter( - task_name="documents.tasks.train_classifier", - status="SUCCESS", + classifier_last_trained = ( + make_aware( + datetime.fromtimestamp(classifier.get_last_checked()), ) - .order_by( - "-date_done", - ) - .first() + if settings.MODEL_FILE.exists() + and classifier.get_last_checked() is not None + else None ) - classifier_last_trained = result.date_done if result else None except Exception as e: if classifier_status is None: classifier_status = "ERROR" diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 0ef4c1dc8..57494a5c2 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-27 08:19-0800\n" +"POT-Creation-Date: 2025-01-28 12:17-0800\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -90,7 +90,7 @@ msgid "Automatic" msgstr "" #: documents/models.py:67 documents/models.py:433 documents/models.py:1493 -#: paperless_mail/models.py:23 paperless_mail/models.py:136 +#: paperless_mail/models.py:23 paperless_mail/models.py:143 msgid "name" msgstr "" @@ -276,7 +276,7 @@ msgstr "" msgid "warning" msgstr "" -#: documents/models.py:387 paperless_mail/models.py:350 +#: documents/models.py:387 paperless_mail/models.py:363 msgid "error" msgstr "" @@ -818,7 +818,7 @@ msgstr "" msgid "filter filename" msgstr "" -#: documents/models.py:1066 paperless_mail/models.py:193 +#: documents/models.py:1066 paperless_mail/models.py:200 msgid "" "Only consume documents which entirely match this filename if specified. " "Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." @@ -988,15 +988,15 @@ msgid "" "Assign a document title, can include some placeholders, see documentation." msgstr "" -#: documents/models.py:1287 paperless_mail/models.py:261 +#: documents/models.py:1287 paperless_mail/models.py:274 msgid "assign this tag" msgstr "" -#: documents/models.py:1296 paperless_mail/models.py:269 +#: documents/models.py:1296 paperless_mail/models.py:282 msgid "assign this document type" msgstr "" -#: documents/models.py:1305 paperless_mail/models.py:283 +#: documents/models.py:1305 paperless_mail/models.py:296 msgid "assign this correspondent" msgstr "" @@ -1112,7 +1112,7 @@ msgstr "" msgid "workflow actions" msgstr "" -#: documents/models.py:1495 paperless_mail/models.py:138 +#: documents/models.py:1495 paperless_mail/models.py:145 msgid "order" msgstr "" @@ -1124,7 +1124,7 @@ msgstr "" msgid "actions" msgstr "" -#: documents/models.py:1511 paperless_mail/models.py:147 +#: documents/models.py:1511 paperless_mail/models.py:154 msgid "enabled" msgstr "" @@ -1838,161 +1838,185 @@ msgid "Process all files, including 'inline' attachments." msgstr "" #: paperless_mail/models.py:119 -msgid "Delete" +msgid "System default" msgstr "" #: paperless_mail/models.py:120 -msgid "Move to specified folder" +msgid "Text, then HTML" msgstr "" #: paperless_mail/models.py:121 -msgid "Mark as read, don't process read mails" +msgid "HTML, then text" msgstr "" #: paperless_mail/models.py:122 -msgid "Flag the mail, don't process flagged mails" +msgid "HTML only" msgstr "" #: paperless_mail/models.py:123 -msgid "Tag the mail with specified tag, don't process tagged mails" +msgid "Text only" msgstr "" #: paperless_mail/models.py:126 -msgid "Use subject as title" +msgid "Delete" msgstr "" #: paperless_mail/models.py:127 -msgid "Use attachment filename as title" +msgid "Move to specified folder" msgstr "" #: paperless_mail/models.py:128 -msgid "Do not assign title from rule" +msgid "Mark as read, don't process read mails" msgstr "" -#: paperless_mail/models.py:131 -msgid "Do not assign a correspondent" +#: paperless_mail/models.py:129 +msgid "Flag the mail, don't process flagged mails" msgstr "" -#: paperless_mail/models.py:132 -msgid "Use mail address" +#: paperless_mail/models.py:130 +msgid "Tag the mail with specified tag, don't process tagged mails" msgstr "" #: paperless_mail/models.py:133 -msgid "Use name (or mail address if not available)" +msgid "Use subject as title" msgstr "" #: paperless_mail/models.py:134 +msgid "Use attachment filename as title" +msgstr "" + +#: paperless_mail/models.py:135 +msgid "Do not assign title from rule" +msgstr "" + +#: paperless_mail/models.py:138 +msgid "Do not assign a correspondent" +msgstr "" + +#: paperless_mail/models.py:139 +msgid "Use mail address" +msgstr "" + +#: paperless_mail/models.py:140 +msgid "Use name (or mail address if not available)" +msgstr "" + +#: paperless_mail/models.py:141 msgid "Use correspondent selected below" msgstr "" -#: paperless_mail/models.py:144 +#: paperless_mail/models.py:151 msgid "account" msgstr "" -#: paperless_mail/models.py:150 paperless_mail/models.py:305 +#: paperless_mail/models.py:157 paperless_mail/models.py:318 msgid "folder" msgstr "" -#: paperless_mail/models.py:154 +#: paperless_mail/models.py:161 msgid "" "Subfolders must be separated by a delimiter, often a dot ('.') or slash " "('/'), but it varies by mail server." msgstr "" -#: paperless_mail/models.py:160 +#: paperless_mail/models.py:167 msgid "filter from" msgstr "" -#: paperless_mail/models.py:167 +#: paperless_mail/models.py:174 msgid "filter to" msgstr "" -#: paperless_mail/models.py:174 +#: paperless_mail/models.py:181 msgid "filter subject" msgstr "" -#: paperless_mail/models.py:181 +#: paperless_mail/models.py:188 msgid "filter body" msgstr "" -#: paperless_mail/models.py:188 +#: paperless_mail/models.py:195 msgid "filter attachment filename inclusive" msgstr "" -#: paperless_mail/models.py:200 +#: paperless_mail/models.py:207 msgid "filter attachment filename exclusive" msgstr "" -#: paperless_mail/models.py:205 +#: paperless_mail/models.py:212 msgid "" "Do not consume documents which entirely match this filename if specified. " "Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." msgstr "" -#: paperless_mail/models.py:212 +#: paperless_mail/models.py:219 msgid "maximum age" msgstr "" -#: paperless_mail/models.py:214 +#: paperless_mail/models.py:221 msgid "Specified in days." msgstr "" -#: paperless_mail/models.py:218 +#: paperless_mail/models.py:225 msgid "attachment type" msgstr "" -#: paperless_mail/models.py:222 +#: paperless_mail/models.py:229 msgid "" "Inline attachments include embedded images, so it's best to combine this " "option with a filename filter." msgstr "" -#: paperless_mail/models.py:228 +#: paperless_mail/models.py:235 msgid "consumption scope" msgstr "" -#: paperless_mail/models.py:234 +#: paperless_mail/models.py:241 +msgid "pdf layout" +msgstr "" + +#: paperless_mail/models.py:247 msgid "action" msgstr "" -#: paperless_mail/models.py:240 +#: paperless_mail/models.py:253 msgid "action parameter" msgstr "" -#: paperless_mail/models.py:245 +#: paperless_mail/models.py:258 msgid "" "Additional parameter for the action selected above, i.e., the target folder " "of the move to folder action. Subfolders must be separated by dots." msgstr "" -#: paperless_mail/models.py:253 +#: paperless_mail/models.py:266 msgid "assign title from" msgstr "" -#: paperless_mail/models.py:273 +#: paperless_mail/models.py:286 msgid "assign correspondent from" msgstr "" -#: paperless_mail/models.py:287 +#: paperless_mail/models.py:300 msgid "Assign the rule owner to documents" msgstr "" -#: paperless_mail/models.py:313 +#: paperless_mail/models.py:326 msgid "uid" msgstr "" -#: paperless_mail/models.py:321 +#: paperless_mail/models.py:334 msgid "subject" msgstr "" -#: paperless_mail/models.py:329 +#: paperless_mail/models.py:342 msgid "received" msgstr "" -#: paperless_mail/models.py:336 +#: paperless_mail/models.py:349 msgid "processed" msgstr "" -#: paperless_mail/models.py:342 +#: paperless_mail/models.py:355 msgid "status" msgstr "" diff --git a/src/paperless/consumers.py b/src/paperless/consumers.py index cf1a3b548..c72b58aa7 100644 --- a/src/paperless/consumers.py +++ b/src/paperless/consumers.py @@ -41,4 +41,10 @@ class StatusConsumer(WebsocketConsumer): self.close() else: if self._is_owner_or_unowned(event["data"]): - self.send(json.dumps(event["data"])) + self.send(json.dumps(event)) + + def documents_deleted(self, event): + if not self._authenticated(): + self.close() + else: + self.send(json.dumps(event)) diff --git a/src/paperless/serialisers.py b/src/paperless/serialisers.py index 2c186571f..461eef587 100644 --- a/src/paperless/serialisers.py +++ b/src/paperless/serialisers.py @@ -151,7 +151,7 @@ class SocialAccountSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer): - email = serializers.EmailField(allow_null=False) + email = serializers.EmailField(allow_blank=True, required=False) password = ObfuscatedPasswordField(required=False, allow_null=False) auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key") social_accounts = SocialAccountSerializer( diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 3e7e18e3e..6a5e98f46 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -1051,6 +1051,11 @@ CONVERT_MEMORY_LIMIT = os.getenv("PAPERLESS_CONVERT_MEMORY_LIMIT") GS_BINARY = os.getenv("PAPERLESS_GS_BINARY", "gs") +# Fallback layout for .eml consumption +EMAIL_PARSE_DEFAULT_LAYOUT = __get_int( + "PAPERLESS_EMAIL_PARSE_DEFAULT_LAYOUT", + 1, # MailRule.PdfLayout.TEXT_HTML but that can't be imported here +) # Pre-2.x versions of Paperless stored your documents locally with GPG # encryption, but that is no longer the default. This behaviour is still diff --git a/src/paperless/tests/test_websockets.py b/src/paperless/tests/test_websockets.py index bf838821a..5ba909d1c 100644 --- a/src/paperless/tests/test_websockets.py +++ b/src/paperless/tests/test_websockets.py @@ -5,6 +5,9 @@ from channels.testing import WebsocketCommunicator from django.test import TestCase from django.test import override_settings +from documents.plugins.helpers import DocumentsStatusManager +from documents.plugins.helpers import ProgressManager +from documents.plugins.helpers import ProgressStatusOptions from paperless.asgi import application TEST_CHANNEL_LAYERS = { @@ -22,6 +25,39 @@ class TestWebSockets(TestCase): self.assertFalse(connected) await communicator.disconnect() + @mock.patch("paperless.consumers.StatusConsumer.close") + @mock.patch("paperless.consumers.StatusConsumer._authenticated") + async def test_close_on_no_auth(self, _authenticated, mock_close): + _authenticated.return_value = True + + communicator = WebsocketCommunicator(application, "/ws/status/") + connected, subprotocol = await communicator.connect() + self.assertTrue(connected) + + message = {"type": "status_update", "data": {"task_id": "test"}} + + _authenticated.return_value = False + + channel_layer = get_channel_layer() + await channel_layer.group_send( + "status_updates", + message, + ) + await communicator.receive_nothing() + + mock_close.assert_called_once() + mock_close.reset_mock() + + message = {"type": "documents_deleted", "data": {"documents": [1, 2, 3]}} + + await channel_layer.group_send( + "status_updates", + message, + ) + await communicator.receive_nothing() + + mock_close.assert_called_once() + @mock.patch("paperless.consumers.StatusConsumer._authenticated") async def test_auth(self, _authenticated): _authenticated.return_value = True @@ -33,19 +69,19 @@ class TestWebSockets(TestCase): await communicator.disconnect() @mock.patch("paperless.consumers.StatusConsumer._authenticated") - async def test_receive(self, _authenticated): + async def test_receive_status_update(self, _authenticated): _authenticated.return_value = True communicator = WebsocketCommunicator(application, "/ws/status/") connected, subprotocol = await communicator.connect() self.assertTrue(connected) - message = {"task_id": "test"} + message = {"type": "status_update", "data": {"task_id": "test"}} channel_layer = get_channel_layer() await channel_layer.group_send( "status_updates", - {"type": "status_update", "data": message}, + message, ) response = await communicator.receive_json_from() @@ -53,3 +89,73 @@ class TestWebSockets(TestCase): self.assertEqual(response, message) await communicator.disconnect() + + @mock.patch("paperless.consumers.StatusConsumer._authenticated") + async def test_receive_documents_deleted(self, _authenticated): + _authenticated.return_value = True + + communicator = WebsocketCommunicator(application, "/ws/status/") + connected, subprotocol = await communicator.connect() + self.assertTrue(connected) + + message = {"type": "documents_deleted", "data": {"documents": [1, 2, 3]}} + + channel_layer = get_channel_layer() + await channel_layer.group_send( + "status_updates", + message, + ) + + response = await communicator.receive_json_from() + + self.assertEqual(response, message) + + await communicator.disconnect() + + @mock.patch("channels.layers.InMemoryChannelLayer.group_send") + def test_manager_send_progress(self, mock_group_send): + with ProgressManager(task_id="test") as manager: + manager.send_progress( + ProgressStatusOptions.STARTED, + "Test message", + 1, + 10, + extra_args={ + "foo": "bar", + }, + ) + + message = mock_group_send.call_args[0][1] + + self.assertEqual( + message, + { + "type": "status_update", + "data": { + "filename": None, + "task_id": "test", + "current_progress": 1, + "max_progress": 10, + "status": ProgressStatusOptions.STARTED, + "message": "Test message", + "foo": "bar", + }, + }, + ) + + @mock.patch("channels.layers.InMemoryChannelLayer.group_send") + def test_manager_send_documents_deleted(self, mock_group_send): + with DocumentsStatusManager() as manager: + manager.send_documents_deleted([1, 2, 3]) + + message = mock_group_send.call_args[0][1] + + self.assertEqual( + message, + { + "type": "documents_deleted", + "data": { + "documents": [1, 2, 3], + }, + }, + ) diff --git a/src/paperless/version.py b/src/paperless/version.py index 185208da1..b09a20ef4 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1,6 +1,6 @@ from typing import Final -__version__: Final[tuple[int, int, int]] = (2, 14, 6) +__version__: Final[tuple[int, int, int]] = (2, 14, 7) # Version string like X.Y.Z __full_version_str__: Final[str] = ".".join(map(str, __version__)) # Version string like X.Y diff --git a/src/paperless/views.py b/src/paperless/views.py index 1ef23a84a..fdd7c21a4 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -150,7 +150,7 @@ class UserViewSet(ModelViewSet): ).first() if authenticator is not None: delete_and_cleanup(request, authenticator) - return Response(True) + return Response(data=True) else: return HttpResponseNotFound("TOTP not found") @@ -292,7 +292,7 @@ class TOTPView(GenericAPIView): ).first() if authenticator is not None: delete_and_cleanup(request, authenticator) - return Response(True) + return Response(data=True) else: return HttpResponseNotFound("TOTP not found") diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index e25c4f227..cf35ea6cb 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -121,7 +121,7 @@ class MarkReadMailAction(BaseMailAction): return {"seen": False} def post_consume(self, M: MailBox, message_uid: str, parameter: str): - M.flag(message_uid, [MailMessageFlags.SEEN], True) + M.flag(message_uid, [MailMessageFlags.SEEN], value=True) class MoveMailAction(BaseMailAction): @@ -142,7 +142,7 @@ class FlagMailAction(BaseMailAction): return {"flagged": False} def post_consume(self, M: MailBox, message_uid: str, parameter: str): - M.flag(message_uid, [MailMessageFlags.FLAGGED], True) + M.flag(message_uid, [MailMessageFlags.FLAGGED], value=True) class TagMailAction(BaseMailAction): @@ -150,7 +150,7 @@ class TagMailAction(BaseMailAction): A mail action that tags mails after processing. """ - def __init__(self, parameter: str, supports_gmail_labels: bool): + def __init__(self, parameter: str, *, supports_gmail_labels: bool): # The custom tag should look like "apple:" if "apple:" in parameter.lower(): _, self.color = parameter.split(":") @@ -188,19 +188,19 @@ class TagMailAction(BaseMailAction): M.flag( message_uid, set(itertools.chain(*APPLE_MAIL_TAG_COLORS.values())), - False, + value=False, ) # Set new $MailFlagBits - M.flag(message_uid, APPLE_MAIL_TAG_COLORS.get(self.color), True) + M.flag(message_uid, APPLE_MAIL_TAG_COLORS.get(self.color), value=True) # Set the general \Flagged # This defaults to the "red" flag in AppleMail and # "stars" in Thunderbird or GMail - M.flag(message_uid, [MailMessageFlags.FLAGGED], True) + M.flag(message_uid, [MailMessageFlags.FLAGGED], value=True) elif self.keyword: - M.flag(message_uid, [self.keyword], True) + M.flag(message_uid, [self.keyword], value=True) else: raise MailError("No keyword specified.") @@ -268,7 +268,7 @@ def apply_mail_action( mailbox_login(M, account) M.folder.set(rule.folder) - action = get_rule_action(rule, supports_gmail_labels) + action = get_rule_action(rule, supports_gmail_labels=supports_gmail_labels) try: action.post_consume(M, message_uid, rule.action_parameter) except errors.ImapToolsError: @@ -356,7 +356,7 @@ def queue_consumption_tasks( ).delay() -def get_rule_action(rule: MailRule, supports_gmail_labels: bool) -> BaseMailAction: +def get_rule_action(rule: MailRule, *, supports_gmail_labels: bool) -> BaseMailAction: """ Returns a BaseMailAction instance for the given rule. """ @@ -370,12 +370,15 @@ def get_rule_action(rule: MailRule, supports_gmail_labels: bool) -> BaseMailActi elif rule.action == MailRule.MailAction.MARK_READ: return MarkReadMailAction() elif rule.action == MailRule.MailAction.TAG: - return TagMailAction(rule.action_parameter, supports_gmail_labels) + return TagMailAction( + rule.action_parameter, + supports_gmail_labels=supports_gmail_labels, + ) else: raise NotImplementedError("Unknown action.") # pragma: no cover -def make_criterias(rule: MailRule, supports_gmail_labels: bool): +def make_criterias(rule: MailRule, *, supports_gmail_labels: bool): """ Returns criteria to be applied to MailBox.fetch for the given rule. """ @@ -393,7 +396,10 @@ def make_criterias(rule: MailRule, supports_gmail_labels: bool): if rule.filter_body: criterias["body"] = rule.filter_body - rule_query = get_rule_action(rule, supports_gmail_labels).get_criteria() + rule_query = get_rule_action( + rule, + supports_gmail_labels=supports_gmail_labels, + ).get_criteria() if isinstance(rule_query, dict): if len(rule_query) or len(criterias): return AND(**rule_query, **criterias) @@ -563,7 +569,7 @@ class MailAccountHandler(LoggingMixin): total_processed_files += self._handle_mail_rule( M, rule, - supports_gmail_labels, + supports_gmail_labels=supports_gmail_labels, ) except Exception as e: self.log.exception( @@ -588,6 +594,7 @@ class MailAccountHandler(LoggingMixin): self, M: MailBox, rule: MailRule, + *, supports_gmail_labels: bool, ): folders = [rule.folder] @@ -616,7 +623,7 @@ class MailAccountHandler(LoggingMixin): f"does not exist in account {rule.account}", ) from err - criterias = make_criterias(rule, supports_gmail_labels) + criterias = make_criterias(rule, supports_gmail_labels=supports_gmail_labels) self.log.debug( f"Rule {rule}: Searching folder with criteria {criterias}", diff --git a/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py b/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py new file mode 100644 index 000000000..fe7a93b71 --- /dev/null +++ b/src/paperless_mail/migrations/0029_mailrule_pdf_layout.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.3 on 2024-11-24 12:39 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("paperless_mail", "0028_alter_mailaccount_password_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="mailrule", + name="pdf_layout", + field=models.PositiveIntegerField( + choices=[ + (0, "System default"), + (1, "Text, then HTML"), + (2, "HTML, then text"), + (3, "HTML only"), + (4, "Text only"), + ], + default=0, + verbose_name="pdf layout", + ), + ), + ] diff --git a/src/paperless_mail/models.py b/src/paperless_mail/models.py index 46b9db1ff..cf33a056b 100644 --- a/src/paperless_mail/models.py +++ b/src/paperless_mail/models.py @@ -115,6 +115,13 @@ class MailRule(document_models.ModelWithOwner): ATTACHMENTS_ONLY = 1, _("Only process attachments.") EVERYTHING = 2, _("Process all files, including 'inline' attachments.") + class PdfLayout(models.IntegerChoices): + DEFAULT = 0, _("System default") + TEXT_HTML = 1, _("Text, then HTML") + HTML_TEXT = 2, _("HTML, then text") + HTML_ONLY = 3, _("HTML only") + TEXT_ONLY = 4, _("Text only") + class MailAction(models.IntegerChoices): DELETE = 1, _("Delete") MOVE = 2, _("Move to specified folder") @@ -230,6 +237,12 @@ class MailRule(document_models.ModelWithOwner): default=ConsumptionScope.ATTACHMENTS_ONLY, ) + pdf_layout = models.PositiveIntegerField( + _("pdf layout"), + choices=PdfLayout.choices, + default=PdfLayout.DEFAULT, + ) + action = models.PositiveIntegerField( _("action"), choices=MailAction.choices, diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index d98fb7238..44032a2e9 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -22,6 +22,7 @@ from documents.parsers import DocumentParser from documents.parsers import ParseError from documents.parsers import make_thumbnail_from_pdf from paperless.models import OutputTypeChoices +from paperless_mail.models import MailRule class MailDocumentParser(DocumentParser): @@ -121,7 +122,13 @@ class MailDocumentParser(DocumentParser): result.sort(key=lambda item: (item["prefix"], item["key"])) return result - def parse(self, document_path: Path, mime_type: str, file_name=None): + def parse( + self, + document_path: Path, + mime_type: str, + file_name=None, + mailrule_id: int | None = None, + ): """ Parses the given .eml into formatted text, based on the decoded email. @@ -180,7 +187,11 @@ class MailDocumentParser(DocumentParser): self.date = mail.date self.log.debug("Creating a PDF from the email") - self.archive_path = self.generate_pdf(mail) + if mailrule_id: + rule = MailRule.objects.get(pk=mailrule_id) + self.archive_path = self.generate_pdf(mail, rule.pdf_layout) + else: + self.archive_path = self.generate_pdf(mail) @staticmethod def parse_file_to_message(filepath: Path) -> MailMessage: @@ -217,11 +228,19 @@ class MailDocumentParser(DocumentParser): f"{settings.TIKA_ENDPOINT}: {err}", ) from err - def generate_pdf(self, mail_message: MailMessage) -> Path: + def generate_pdf( + self, + mail_message: MailMessage, + pdf_layout: MailRule.PdfLayout | None = None, + ) -> Path: archive_path = Path(self.tempdir) / "merged.pdf" mail_pdf_file = self.generate_pdf_from_mail(mail_message) + pdf_layout = ( + pdf_layout or settings.EMAIL_PARSE_DEFAULT_LAYOUT + ) # EMAIL_PARSE_DEFAULT_LAYOUT is a MailRule.PdfLayout + # If no HTML content, create the PDF from the message # Otherwise, create 2 PDFs and merge them with Gotenberg if not mail_message.html: @@ -246,7 +265,15 @@ class MailDocumentParser(DocumentParser): if pdf_a_format is not None: route.pdf_format(pdf_a_format) - route.merge([mail_pdf_file, pdf_of_html_content]) + match pdf_layout: + case MailRule.PdfLayout.HTML_TEXT: + route.merge([pdf_of_html_content, mail_pdf_file]) + case MailRule.PdfLayout.HTML_ONLY: + route.merge([pdf_of_html_content]) + case MailRule.PdfLayout.TEXT_ONLY: + route.merge([mail_pdf_file]) + case MailRule.PdfLayout.TEXT_HTML | _: + route.merge([mail_pdf_file, pdf_of_html_content]) try: response = route.run() diff --git a/src/paperless_mail/serialisers.py b/src/paperless_mail/serialisers.py index 53a474275..c7a20acbf 100644 --- a/src/paperless_mail/serialisers.py +++ b/src/paperless_mail/serialisers.py @@ -96,6 +96,7 @@ class MailRuleSerializer(OwnedObjectSerializer): "order", "attachment_type", "consumption_scope", + "pdf_layout", "owner", "user_can_change", "permissions", diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 2311c3009..a73f9cf34 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -124,7 +124,7 @@ class BogusMailBox(AbstractContextManager): if username != self.USERNAME or access_token != self.ACCESS_TOKEN: raise MailboxLoginError("BAD", "OK") - def fetch(self, criteria, mark_seen, charset="", bulk=True): + def fetch(self, criteria, mark_seen, charset="", *, bulk=True): msg = self.messages criteria = str(criteria).strip("()").split(" ") @@ -190,7 +190,7 @@ class BogusMailBox(AbstractContextManager): raise Exception -def fake_magic_from_buffer(buffer, mime=False): +def fake_magic_from_buffer(buffer, *, mime=False): if mime: if "PDF" in str(buffer): return "application/pdf" @@ -206,6 +206,7 @@ class MessageBuilder: def create_message( self, + *, attachments: int | list[_AttachmentDef] = 1, body: str = "", subject: str = "the subject", @@ -783,12 +784,18 @@ class TestMail( ) self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 2) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", mark_seen=False)), + 2, + ) self.mail_account_handler.handle_mail_account(account) self.mailMocker.apply_mail_actions() - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 0) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", mark_seen=False)), + 0, + ) self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) def test_handle_mail_account_delete(self): @@ -853,7 +860,7 @@ class TestMail( self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) self.assertEqual( - len(self.mailMocker.bogus_mailbox.fetch("UNFLAGGED", False)), + len(self.mailMocker.bogus_mailbox.fetch("UNFLAGGED", mark_seen=False)), 2, ) @@ -861,7 +868,7 @@ class TestMail( self.mailMocker.apply_mail_actions() self.assertEqual( - len(self.mailMocker.bogus_mailbox.fetch("UNFLAGGED", False)), + len(self.mailMocker.bogus_mailbox.fetch("UNFLAGGED", mark_seen=False)), 1, ) self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) @@ -934,7 +941,12 @@ class TestMail( self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) self.assertEqual( - len(self.mailMocker.bogus_mailbox.fetch("UNKEYWORD processed", False)), + len( + self.mailMocker.bogus_mailbox.fetch( + "UNKEYWORD processed", + mark_seen=False, + ), + ), 2, ) @@ -943,7 +955,12 @@ class TestMail( self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) self.assertEqual( - len(self.mailMocker.bogus_mailbox.fetch("UNKEYWORD processed", False)), + len( + self.mailMocker.bogus_mailbox.fetch( + "UNKEYWORD processed", + mark_seen=False, + ), + ), 0, ) @@ -967,12 +984,18 @@ class TestMail( self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) criteria = NOT(gmail_label="processed") - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch(criteria, False)), 2) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch(criteria, mark_seen=False)), + 2, + ) self.mail_account_handler.handle_mail_account(account) self.mailMocker.apply_mail_actions() - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch(criteria, False)), 0) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch(criteria, mark_seen=False)), + 0, + ) self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) def test_tag_mail_action_applemail_wrong_input(self): @@ -980,7 +1003,7 @@ class TestMail( MailError, TagMailAction, "apple:black", - False, + supports_gmail_labels=False, ) def test_handle_mail_account_tag_applemail(self): @@ -1002,7 +1025,7 @@ class TestMail( self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) self.assertEqual( - len(self.mailMocker.bogus_mailbox.fetch("UNFLAGGED", False)), + len(self.mailMocker.bogus_mailbox.fetch("UNFLAGGED", mark_seen=False)), 2, ) @@ -1010,7 +1033,7 @@ class TestMail( self.mailMocker.apply_mail_actions() self.assertEqual( - len(self.mailMocker.bogus_mailbox.fetch("UNFLAGGED", False)), + len(self.mailMocker.bogus_mailbox.fetch("UNFLAGGED", mark_seen=False)), 0, ) self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) @@ -1324,13 +1347,19 @@ class TestMail( self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) self.mailMocker._queue_consumption_tasks_mock.assert_not_called() - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 2) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", mark_seen=False)), + 2, + ) self.mail_account_handler.handle_mail_account(account) self.mailMocker.apply_mail_actions() self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 2) - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 0) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", mark_seen=False)), + 0, + ) self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) def test_auth_plain_fallback_fails_still(self): @@ -1390,13 +1419,19 @@ class TestMail( self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 0) - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 2) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", mark_seen=False)), + 2, + ) self.mail_account_handler.handle_mail_account(account) self.mailMocker.apply_mail_actions() self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 2) - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 0) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", mark_seen=False)), + 0, + ) self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) def test_disabled_rule(self): @@ -1425,12 +1460,15 @@ class TestMail( self.mailMocker.apply_mail_actions() self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3) - self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 2) + self.assertEqual( + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", mark_seen=False)), + 2, + ) self.mail_account_handler.handle_mail_account(account) self.mailMocker.apply_mail_actions() self.assertEqual( - len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), + len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", mark_seen=False)), 2, ) # still 2 diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index e8186ea0f..dbd2c82cd 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -1,6 +1,7 @@ import datetime import logging from pathlib import Path +from unittest import mock import httpx import pytest @@ -662,3 +663,67 @@ class TestParser: request = httpx_mock.get_request() assert str(request.url) == "http://localhost:3000/forms/chromium/convert/html" + + @pytest.mark.httpx_mock(can_send_already_matched_responses=True) + @mock.patch("gotenberg_client._merge.MergeRoute.merge") + @mock.patch("paperless_mail.models.MailRule.objects.get") + def test_generate_pdf_layout_options( + self, + mock_mailrule_get: mock.Mock, + mock_merge_route: mock.Mock, + httpx_mock: HTTPXMock, + mail_parser: MailDocumentParser, + html_email_file: Path, + html_email_pdf_file: Path, + ): + """ + GIVEN: + - Email message + WHEN: + - Email is parsed with different layout options + THEN: + - Gotenberg is called with the correct layout option + """ + httpx_mock.add_response( + url="http://localhost:9998/tika/text", + method="PUT", + json={ + "Content-Type": "text/html", + "X-TIKA:Parsed-By": [], + "X-TIKA:content": "This is some Tika HTML text", + }, + ) + httpx_mock.add_response( + url="http://localhost:3000/forms/chromium/convert/html", + method="POST", + content=html_email_pdf_file.read_bytes(), + ) + httpx_mock.add_response( + url="http://localhost:3000/forms/pdfengines/merge", + method="POST", + content=b"Pretend merged PDF content", + ) + + def test_layout_option(layout_option, expected_calls, expected_pdf_names): + mock_mailrule_get.return_value = mock.Mock(pdf_layout=layout_option) + mail_parser.parse( + document_path=html_email_file, + mime_type="message/rfc822", + mailrule_id=1, + ) + args, _ = mock_merge_route.call_args + assert len(args[0]) == expected_calls + for i, pdf in enumerate(expected_pdf_names): + assert args[0][i].name == pdf + + # 1 = MailRule.PdfLayout.TEXT_HTML + test_layout_option(1, 2, ["email_as_pdf.pdf", "html.pdf"]) + + # 2 = MailRule.PdfLayout.HTML_TEXT + test_layout_option(2, 2, ["html.pdf", "email_as_pdf.pdf"]) + + # 3 = MailRule.PdfLayout.HTML_ONLY + test_layout_option(3, 1, ["html.pdf"]) + + # 4 = MailRule.PdfLayout.TEXT_ONLY + test_layout_option(4, 1, ["email_as_pdf.pdf"]) diff --git a/src/paperless_tesseract/parsers.py b/src/paperless_tesseract/parsers.py index e7968a61e..a8be899f5 100644 --- a/src/paperless_tesseract/parsers.py +++ b/src/paperless_tesseract/parsers.py @@ -214,6 +214,7 @@ class RasterisedDocumentParser(DocumentParser): mime_type, output_file, sidecar_file, + *, safe_fallback=False, ): if TYPE_CHECKING: