Merge branch 'dev'

This commit is contained in:
shamoon 2024-07-10 21:02:57 -07:00
commit 9a9ab85baf
254 changed files with 25607 additions and 19885 deletions

180
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,180 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim as main-app
ARG DEBIAN_FRONTEND=noninteractive
# Buildx provided, must be defined to use though
ARG TARGETARCH
# Can be workflow provided, defaults set for manual building
ARG JBIG2ENC_VERSION=0.29
ARG QPDF_VERSION=11.9.0
ARG GS_VERSION=10.03.1
# Set Python environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
# Ignore warning from Whitenoise
PYTHONWARNINGS="ignore:::django.http.response:517" \
PNGX_CONTAINERIZED=1
#
# Begin installation and configuration
# Order the steps below from least often changed to most
#
# Packages need for running
ARG RUNTIME_PACKAGES="\
# General utils
curl \
# Docker specific
gosu \
# Timezones support
tzdata \
# fonts for text file thumbnail generation
fonts-liberation \
gettext \
ghostscript \
gnupg \
icc-profiles-free \
imagemagick \
# PostgreSQL
postgresql-client \
# MySQL / MariaDB
mariadb-client \
# OCRmyPDF dependencies
tesseract-ocr \
tesseract-ocr-eng \
tesseract-ocr-deu \
tesseract-ocr-fra \
tesseract-ocr-ita \
tesseract-ocr-spa \
unpaper \
pngquant \
jbig2dec \
# lxml
libxml2 \
libxslt1.1 \
# itself
qpdf \
# Mime type detection
file \
libmagic1 \
media-types \
zlib1g \
# Barcode splitter
libzbar0 \
poppler-utils \
htop \
sudo"
# Install basic runtime packages.
# These change very infrequently
RUN set -eux \
echo "Installing system packages" \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES}
ARG PYTHON_PACKAGES="\
python3 \
python3-pip \
python3-wheel \
pipenv \
ca-certificates"
RUN set -eux \
echo "Installing python packages" \
&& apt-get update \
&& apt-get install --yes --quiet ${PYTHON_PACKAGES}
RUN set -eux \
&& echo "Installing pre-built updates" \
&& 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
# setup docker-specific things
# These change sometimes, but rarely
WORKDIR /usr/src/paperless/src/docker/
COPY [ \
"docker/imagemagick-policy.xml", \
"./" \
]
RUN set -eux \
&& echo "Configuring ImageMagick" \
&& mv imagemagick-policy.xml /etc/ImageMagick-6/policy.xml
# Packages needed only for building a few quick Python
# dependencies
ARG BUILD_PACKAGES="\
build-essential \
git \
# https://www.psycopg.org/docs/install.html#prerequisites
libpq-dev \
# https://github.com/PyMySQL/mysqlclient#linux
default-libmysqlclient-dev \
pkg-config \
pre-commit"
# 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 ${BUILD_PACKAGES}
RUN set -eux \
&& npm update npm -g
# add users, setup scripts
# Mount the compiled frontend to expected location
RUN set -eux \
&& echo "Setting up user/group" \
&& groupmod --new-name paperless node \
&& usermod --login paperless --home /usr/src/paperless node \
&& usermod -s /bin/bash paperless \
&& echo "paperless ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \
&& echo "Creating volume directories" \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/data \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/media \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/consume \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/export \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/.venv \
&& 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
VOLUME ["/usr/src/paperless/paperless-ngx/data", \
"/usr/src/paperless/paperless-ngx/media", \
"/usr/src/paperless/paperless-ngx/consume", \
"/usr/src/paperless/paperless-ngx/export", \
"/usr/src/paperless/paperless-ngx/.venv"]

117
.devcontainer/README.md Normal file
View File

@ -0,0 +1,117 @@
# Paperless NGX Development Environment
## Overview
Welcome to the Paperless NGX development environment! This setup uses VSCode DevContainers to provide a consistent and seamless development experience.
### What are DevContainers?
DevContainers are a feature in VSCode that allows you to develop within a Docker container. This ensures that your development environment is consistent across different machines and setups. By defining a containerized environment, you can eliminate the "works on my machine" problem.
### Advantages of DevContainers
- **Consistency**: Same environment for all developers.
- **Isolation**: Separate development environment from your local machine.
- **Reproducibility**: Easily recreate the environment on any machine.
- **Pre-configured Tools**: Include all necessary tools and dependencies in the container.
## DevContainer Setup
The DevContainer configuration provides up all the necessary services for Paperless NGX, including:
- Redis
- Gotenberg
- Tika
Data is stored using Docker volumes to ensure persistence across container restarts.
## Configuration Files
The setup includes debugging configurations (`launch.json`) and tasks (`tasks.json`) to help you manage and debug various parts of the project:
- **Backend Debugging:**
- `manage.py runserver`
- `manage.py document-consumer`
- `celery`
- **Maintenance Tasks:**
- Create superuser
- Run migrations
- Recreate virtual environment (`.venv` with pipenv)
- Compile frontend assets
## Getting Started
### Step 1: Running the DevContainer
To start the DevContainer:
1. Open VSCode.
2. Open the project folder.
3. Open the command palette:
- **Windows/Linux**: `Ctrl+Shift+P`
- **Mac**: `Cmd+Shift+P`
4. Type and select `Dev Containers: Rebuild and Reopen in Container`.
VSCode will build and start the DevContainer environment.
### Step 2: Initial Setup
Once the DevContainer is up and running, perform the following steps:
1. **Compile Frontend Assets**:
- Open the command palette:
- **Windows/Linux**: `Ctrl+Shift+P`
- **Mac**: `Cmd+Shift+P`
- Select `Tasks: Run Task`.
- Choose `Frontend Compile`.
2. **Run Database Migrations**:
- Open the command palette:
- **Windows/Linux**: `Ctrl+Shift+P`
- **Mac**: `Cmd+Shift+P`
- Select `Tasks: Run Task`.
- Choose `Migrate Database`.
3. **Create Superuser**:
- Open the command palette:
- **Windows/Linux**: `Ctrl+Shift+P`
- **Mac**: `Cmd+Shift+P`
- Select `Tasks: Run Task`.
- Choose `Create Superuser`.
### Debugging and Running Services
You can start and debug backend services either as debugging sessions via `launch.json` or as tasks.
#### Using `launch.json`:
1. Press `F5` or go to the **Run and Debug** view in VSCode.
2. Select the desired configuration:
- `Runserver`
- `Document Consumer`
- `Celery`
#### Using Tasks:
1. Open the command palette:
- **Windows/Linux**: `Ctrl+Shift+P`
- **Mac**: `Cmd+Shift+P`
2. Select `Tasks: Run Task`.
3. Choose the desired task:
- `Runserver`
- `Document Consumer`
- `Celery`
### Additional Maintenance Tasks
Additional tasks are available for common maintenance operations:
- **Recreate .venv**: For setting up the virtual environment using pipenv.
- **Migrate Database**: To apply database migrations.
- **Create Superuser**: To create an admin user for the application.
## Let's Get Started!
Follow the steps above to get your development environment up and running. Happy coding!

View File

@ -0,0 +1,16 @@
{
"name": "Paperless Development",
"dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml",
"service": "paperless-development",
"workspaceFolder": "/usr/src/paperless/paperless-ngx",
"postCreateCommand": "/bin/bash -c pre-commit install && pipenv install --dev",
"customizations": {
"vscode": {
"extensions": [
"mhutchie.git-graph",
"ms-python.python"
]
}
},
"remoteUser": "paperless"
}

View File

@ -0,0 +1,84 @@
# Docker Compose file for developing Paperless NGX in VSCode DevContainers.
# This file contains everything Paperless NGX needs to run.
# Paperless supports amd64, arm, and arm64 hardware.
# All compose files of Paperless configure it in the following way:
#
# - Paperless is (re)started on system boot if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# SQLite is used as the database. The SQLite file is stored in the data volume.
#
# In addition, this Docker Compose file adds the following optional
# configurations:
#
# - Apache Tika and Gotenberg servers are started with Paperless NGX and Paperless
# is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, PowerPoint, and their LibreOffice counterparts).
#
# This file is intended only to be used through VSCOde devcontainers. See README.md
# in the folder .devcontainer.
services:
broker:
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
# No ports need to be exposed; the VSCode DevContainer plugin manages them.
paperless-development:
image: paperless-ngx
build:
context: ../ # Dockerfile cannot access files from parent directories if context is not set.
dockerfile: ./.devcontainer/Dockerfile
restart: unless-stopped
depends_on:
- broker
- gotenberg
- tika
volumes:
- ..:/usr/src/paperless/paperless-ngx:delegated
- ../.devcontainer/vscode:/usr/src/paperless/paperless-ngx/.vscode:delegated # VSCode config files
- pipenv:/usr/src/paperless/paperless-ngx/.venv # Pipenv environment persisted in volume
- /usr/src/paperless/paperless-ngx/src/documents/static/frontend # Static frontend files exist only in container
- /usr/src/paperless/paperless-ngx/src/.pytest_cache
- /usr/src/paperless/paperless-ngx/.ruff_cache
- /usr/src/paperless/paperless-ngx/htmlcov
- /usr/src/paperless/paperless-ngx/.coverage
- data:/usr/src/paperless/paperless-ngx/data
- media:/usr/src/paperless/paperless-ngx/media
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
PAPERLESS_STATICDIR: ./src/documents/static
PAPERLESS_DEBUG: true
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "chown -R paperless:paperless /usr/src/paperless/paperless-ngx/src/documents/static/frontend && chown -R paperless:paperless /usr/src/paperless/paperless-ngx/.ruff_cache && while sleep 1000; do :; done"
gotenberg:
image: docker.io/gotenberg/gotenberg:7.10
restart: unless-stopped
# The Gotenberg Chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even JavaScript.
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
tika:
image: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:
redisdata:
pipenv:

View File

@ -0,0 +1,43 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "manage.py runserver",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/manage.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": ["runserver"],
"django": true
},
{
"name": "manage.py document_consumer",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/manage.py",
"console": "integratedTerminal",
"justMyCode": true,
"args": ["document_consumer"],
"django": true
},
{
"name": "celery",
"type": "python",
"cwd": "${workspaceFolder}/src",
"request": "launch",
"module": "celery",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${workspaceFolder}/src"
},
"args": [
"-A",
"paperless",
"worker",
"-l",
"DEBUG"
]
}
]
}

View File

@ -0,0 +1,11 @@
{
"python.testing.pytestArgs": [
"src"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"files.watcherExclude": {
"**/.venv/**": true,
"**/pytest_cache/**": true
}
}

View File

@ -0,0 +1,136 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "manage.py document_consumer",
"type": "shell",
"command": "pipenv run python manage.py document_consumer",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
},
{
"label": "manage.py runserver",
"type": "shell",
"command": "pipenv run python manage.py runserver",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
},
{
"label": "Maintenance: manage.py migrate",
"type": "shell",
"command": "pipenv run python manage.py migrate",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
},
{
"label": "Maintenance: manage.py createsuperuser",
"type": "shell",
"command": "pipenv run python manage.py createsuperuser",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
},
{
"label": "compile frontend",
"type": "shell",
"command": "npm ci && ./node_modules/.bin/ng build --configuration production",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src-ui"
}
},
{
"label": "Maintenance: recreate .venv",
"type": "shell",
"command": "rm -R -v .venv/* || pipenv install --dev",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Celery Worker",
"type": "shell",
"command": "pipenv run celery --app paperless worker -l DEBUG",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
}
]
}

View File

@ -49,10 +49,6 @@ updates:
- "paperless-ngx/backend" - "paperless-ngx/backend"
ignore: ignore:
- dependency-name: "uvicorn" - dependency-name: "uvicorn"
- dependency-name: "djangorestframework"
versions:
- "3.15.0"
- "3.15.1"
groups: groups:
development: development:
patterns: patterns:

View File

@ -398,7 +398,7 @@ jobs:
password: ${{ secrets.QUAY_ROBOT_TOKEN }} password: ${{ secrets.QUAY_ROBOT_TOKEN }}
- -
name: Build and push name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@ -406,6 +406,8 @@ jobs:
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker-meta.outputs.tags }} tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }} labels: ${{ steps.docker-meta.outputs.labels }}
build-args: |
PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }}
# Get cache layers from this branch, then dev # Get cache layers from this branch, then dev
# This allows new branches to get at least some cache benefits, generally from dev # This allows new branches to get at least some cache benefits, generally from dev
cache-from: | cache-from: |

2
.gitignore vendored
View File

@ -66,6 +66,8 @@ target/
.vscode .vscode
/src-ui/.vscode /src-ui/.vscode
/docs/.vscode /docs/.vscode
.vscode-server
*CommandMarker
# Other stuff that doesn't belong # Other stuff that doesn't belong
.virtualenv .virtualenv

View File

@ -47,7 +47,7 @@ repos:
exclude: "(^Pipfile\\.lock$)" exclude: "(^Pipfile\\.lock$)"
# Python hooks # Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.4.9' rev: 'v0.5.1'
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format - id: ruff-format

View File

@ -1 +1 @@
3.9.18 3.9.19

View File

@ -13,6 +13,16 @@ WORKDIR /src/src-ui
RUN set -eux \ RUN set -eux \
&& npm update npm -g \ && npm update npm -g \
&& npm ci && npm ci
ARG PNGX_TAG_VERSION=
# Add the tag to the environment file if its a tagged dev build
RUN set -eux && \
case "${PNGX_TAG_VERSION}" in \
dev|fix*|feature*) \
sed -i -E "s/version: '([0-9\.]+)'/version: '\1 #${PNGX_TAG_VERSION}'/g" /src/src-ui/src/environments/environment.prod.ts \
;; \
esac
RUN set -eux \ RUN set -eux \
&& ./node_modules/.bin/ng build --configuration production && ./node_modules/.bin/ng build --configuration production
@ -223,11 +233,11 @@ RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
&& python3 -m pip install --no-cache-dir --upgrade wheel \ && python3 -m pip install --no-cache-dir --upgrade wheel \
&& echo "Installing Python requirements" \ && echo "Installing Python requirements" \
&& curl --fail --silent --show-error --location \ && curl --fail --silent --show-error --location \
--output psycopg_c-3.1.19-cp311-cp311-linux_x86_64.whl \ --output psycopg_c-3.2.1-cp311-cp311-linux_x86_64.whl \
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.1.19/psycopg_c-3.1.19-cp311-cp311-linux_x86_64.whl \ https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.1/psycopg_c-3.2.1-cp311-cp311-linux_x86_64.whl \
&& curl --fail --silent --show-error --location \ && curl --fail --silent --show-error --location \
--output psycopg_c-3.1.19-cp311-cp311-linux_aarch64.whl \ --output psycopg_c-3.2.1-cp311-cp311-linux_aarch64.whl \
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.1.19/psycopg_c-3.1.19-cp311-cp311-linux_aarch64.whl \ https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.1/psycopg_c-3.2.1-cp311-cp311-linux_aarch64.whl \
&& python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \ && python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \
&& echo "Patching whitenoise for compression speedup" \ && echo "Patching whitenoise for compression speedup" \
&& curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch \ && curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch \

10
Pipfile
View File

@ -7,7 +7,7 @@ name = "pypi"
dateparser = "~=1.2" dateparser = "~=1.2"
# WARNING: django does not use semver. # WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes. # Only patch versions are guaranteed to not introduce breaking changes.
django = "~=4.2.13" django = "~=4.2.14"
django-allauth = {extras = ["socialaccount"], version = "*"} django-allauth = {extras = ["socialaccount"], version = "*"}
django-auditlog = "*" django-auditlog = "*"
django-celery-results = "*" django-celery-results = "*"
@ -18,7 +18,7 @@ django-filter = "~=24.2"
django-guardian = "*" django-guardian = "*"
django-multiselectfield = "*" django-multiselectfield = "*"
django-soft-delete = "*" django-soft-delete = "*"
djangorestframework = "==3.14.0" djangorestframework = "==3.15.2"
djangorestframework-guardian = "*" djangorestframework-guardian = "*"
drf-writable-nested = "*" drf-writable-nested = "*"
bleach = "*" bleach = "*"
@ -54,8 +54,8 @@ tqdm = "*"
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494 # See https://github.com/paperless-ngx/paperless-ngx/issues/5494
uvicorn = {extras = ["standard"], version = "==0.25.0"} uvicorn = {extras = ["standard"], version = "==0.25.0"}
watchdog = "~=4.0" watchdog = "~=4.0"
whitenoise = "~=6.6" whitenoise = "~=6.7"
whoosh="~=2.7" whoosh = "~=2.7"
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"} zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
[dev-packages] [dev-packages]
@ -71,6 +71,7 @@ pytest-httpx = "*"
pytest-env = "*" pytest-env = "*"
pytest-sugar = "*" pytest-sugar = "*"
pytest-xdist = "*" pytest-xdist = "*"
pytest-mock = "*"
pytest-rerunfailures = "*" pytest-rerunfailures = "*"
imagehash = "*" imagehash = "*"
daphne = "*" daphne = "*"
@ -93,5 +94,4 @@ types-tqdm = "*"
types-Markdown = "*" types-Markdown = "*"
types-Pygments = "*" types-Pygments = "*"
types-colorama = "*" types-colorama = "*"
types-psycopg2 = "*"
types-setuptools = "*" types-setuptools = "*"

1333
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
services: services:
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:7.10 image: docker.io/gotenberg/gotenberg:8.7
hostname: gotenberg hostname: gotenberg
container_name: gotenberg container_name: gotenberg
network_mode: host network_mode: host

View File

@ -77,7 +77,7 @@ services:
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:7.10 image: docker.io/gotenberg/gotenberg:8.7
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript. # want to allow external content like tracking pixels or even javascript.

View File

@ -71,7 +71,7 @@ services:
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:7.10 image: docker.io/gotenberg/gotenberg:8.7
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not

View File

@ -59,7 +59,7 @@ services:
PAPERLESS_TIKA_ENDPOINT: http://tika:9998 PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg: gotenberg:
image: docker.io/gotenberg/gotenberg:7.10 image: docker.io/gotenberg/gotenberg:8.7
restart: unless-stopped restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not # The gotenberg chromium route is used to convert .eml files. We do not

View File

@ -687,4 +687,5 @@ More details about configuration option for various providers can be found in th
### Disabling Regular Login ### Disabling Regular Login
Once external auth is set up, 'regular' login can be disabled with the [PAPERLESS_DISABLE_REGULAR_LOGIN](configuration.md#PAPERLESS_DISABLE_REGULAR_LOGIN) setting. Once external auth is set up, 'regular' login can be disabled with the [PAPERLESS_DISABLE_REGULAR_LOGIN](configuration.md#PAPERLESS_DISABLE_REGULAR_LOGIN) setting and / or users can be automatically
redirected with the [PAPERLESS_REDIRECT_LOGIN_TO_SSO](configuration.md#PAPERLESS_REDIRECT_LOGIN_TO_SSO) setting.

View File

@ -596,6 +596,14 @@ system. See the corresponding
: Disables the regular frontend username / password login, i.e. once you have setup SSO. Note that this setting does not disable the Django admin login nor logging in with local credentials via the API. To prevent access to the Django admin, consider blocking `/admin/` in your [web server or reverse proxy configuration](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx). : Disables the regular frontend username / password login, i.e. once you have setup SSO. Note that this setting does not disable the Django admin login nor logging in with local credentials via the API. To prevent access to the Django admin, consider blocking `/admin/` in your [web server or reverse proxy configuration](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx).
You can optionally also automatically redirect users to the SSO login with [PAPERLESS_REDIRECT_LOGIN_TO_SSO](#PAPERLESS_REDIRECT_LOGIN_TO_SSO)
Defaults to False
#### ['PAPERLESS_REDIRECT_LOGIN_TO_SSO=<bool>`](#PAPERLESS_REDIRECT_LOGIN_TO_SSO) {#PAPERLESS_REDIRECT_LOGIN_TO_SSO}
: When this setting is enabled users will automatically be redirected (using javascript) to the first SSO provider login. You may still want to disable the frontend login form for clarity.
Defaults to False Defaults to False
#### [`PAPERLESS_ACCOUNT_SESSION_REMEMBER=<bool>`](#PAPERLESS_ACCOUNT_SESSION_REMEMBER) {#PAPERLESS_ACCOUNT_SESSION_REMEMBER} #### [`PAPERLESS_ACCOUNT_SESSION_REMEMBER=<bool>`](#PAPERLESS_ACCOUNT_SESSION_REMEMBER) {#PAPERLESS_ACCOUNT_SESSION_REMEMBER}

View File

@ -445,6 +445,7 @@ The following custom field types are supported:
- `Number`: float number e.g. 12.3456 - `Number`: float number e.g. 12.3456
- `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30 - `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse - `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
- `Select`: a pre-defined list of strings from which the user can choose
## Share Links ## Share Links

View File

@ -77,7 +77,6 @@
"scripts": [], "scripts": [],
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"ng2-pdf-viewer", "ng2-pdf-viewer",
"filesize",
"file-saver" "file-saver"
], ],
"vendorChunk": true, "vendorChunk": true,

View File

@ -525,7 +525,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">337</context> <context context-type="linenumber">347</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3768927257183755959" datatype="html"> <trans-unit id="3768927257183755959" datatype="html">
@ -544,7 +544,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">19</context> <context context-type="linenumber">36</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
@ -584,7 +584,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">329</context> <context context-type="linenumber">339</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context> <context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
@ -718,7 +718,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">346</context> <context context-type="linenumber">356</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -726,7 +726,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">101</context> <context context-type="linenumber">105</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -1080,7 +1080,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">305</context> <context context-type="linenumber">315</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -1092,11 +1092,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">37</context> <context context-type="linenumber">39</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">81</context> <context context-type="linenumber">85</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -1390,11 +1390,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">22</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">66</context> <context context-type="linenumber">69</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -1437,7 +1437,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">80</context> <context context-type="linenumber">86</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context> <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context>
@ -1447,6 +1447,10 @@
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context> <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context>
<context context-type="linenumber">76</context> <context context-type="linenumber">76</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
<context context-type="linenumber">53</context> <context context-type="linenumber">53</context>
@ -1481,11 +1485,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">40</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">84</context> <context context-type="linenumber">88</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -1624,7 +1628,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">18</context> <context context-type="linenumber">35</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
@ -1871,7 +1875,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">64</context> <context context-type="linenumber">66</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -2153,7 +2157,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">74</context> <context context-type="linenumber">80</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
@ -2179,7 +2183,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">78</context> <context context-type="linenumber">84</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context> <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
@ -2214,42 +2218,74 @@
<source>Document deleted</source> <source>Document deleted</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">63</context> <context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="7295637485862454066" datatype="html">
<source>Error deleting document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">69</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">799</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7266264608936522311" datatype="html"> <trans-unit id="7266264608936522311" datatype="html">
<source>This operation will permanently delete the selected documents.</source> <source>This operation will permanently delete the selected documents.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">76</context> <context context-type="linenumber">82</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6804051092296228130" datatype="html"> <trans-unit id="6804051092296228130" datatype="html">
<source>This operation will permanently delete all documents in the trash.</source> <source>This operation will permanently delete all documents in the trash.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">77</context> <context context-type="linenumber">83</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6996183233986182894" datatype="html"> <trans-unit id="6996183233986182894" datatype="html">
<source>Document(s) deleted</source> <source>Document(s) deleted</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">87</context> <context context-type="linenumber">94</context>
</context-group>
</trans-unit>
<trans-unit id="6962724852893361467" datatype="html">
<source>Error deleting document(s)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">101</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7534569062269274401" datatype="html"> <trans-unit id="7534569062269274401" datatype="html">
<source>Document restored</source> <source>Document restored</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">97</context> <context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit id="9136016619414048201" datatype="html">
<source>Error restoring document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">117</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="960063472770266304" datatype="html"> <trans-unit id="960063472770266304" datatype="html">
<source>Document(s) restored</source> <source>Document(s) restored</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">106</context> <context context-type="linenumber">127</context>
</context-group>
</trans-unit>
<trans-unit id="8405416976953346141" datatype="html">
<source>Error restoring document(s)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">133</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8119815638230251386" datatype="html"> <trans-unit id="8119815638230251386" datatype="html">
@ -2306,6 +2342,10 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
<context context-type="linenumber">13</context> <context context-type="linenumber">13</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">22</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="5944812089887969249" datatype="html"> <trans-unit id="5944812089887969249" datatype="html">
<source>Groups</source> <source>Groups</source>
@ -2358,11 +2398,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">34</context> <context context-type="linenumber">36</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">78</context> <context context-type="linenumber">82</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context> <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
@ -3409,18 +3449,25 @@
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4910631867841099191" datatype="html">
<source>Add option</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="528950215505228201" datatype="html"> <trans-unit id="528950215505228201" datatype="html">
<source>Create new custom field</source> <source>Create new custom field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context>
<context context-type="linenumber">36</context> <context context-type="linenumber">80</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8751213029607178010" datatype="html"> <trans-unit id="8751213029607178010" datatype="html">
<source>Edit custom field</source> <source>Edit custom field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context>
<context context-type="linenumber">40</context> <context context-type="linenumber">84</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6672809941092516947" datatype="html"> <trans-unit id="6672809941092516947" datatype="html">
@ -3586,7 +3633,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">68</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7046259383943324039" datatype="html"> <trans-unit id="7046259383943324039" datatype="html">
@ -4640,7 +4687,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/select/select.component.ts</context> <context context-type="sourcefile">src/app/components/common/input/select/select.component.ts</context>
<context context-type="linenumber">158</context> <context context-type="linenumber">163</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1880237574877817137" datatype="html"> <trans-unit id="1880237574877817137" datatype="html">
@ -4722,7 +4769,7 @@
<source>Private</source> <source>Private</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/select/select.component.ts</context> <context context-type="sourcefile">src/app/components/common/input/select/select.component.ts</context>
<context context-type="linenumber">57</context> <context context-type="linenumber">62</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/tag/tag.component.html</context> <context context-type="sourcefile">src/app/components/common/tag/tag.component.html</context>
@ -4741,7 +4788,7 @@
<source>No items found</source> <source>No items found</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/select/select.component.ts</context> <context context-type="sourcefile">src/app/components/common/input/select/select.component.ts</context>
<context context-type="linenumber">92</context> <context context-type="linenumber">97</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6541407358060244620" datatype="html"> <trans-unit id="6541407358060244620" datatype="html">
@ -5065,6 +5112,10 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">6</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit> </trans-unit>
<trans-unit id="7103181924469214926" datatype="html"> <trans-unit id="7103181924469214926" datatype="html">
<source>Please select an object</source> <source>Please select an object</source>
@ -5808,14 +5859,14 @@
<source>Content</source> <source>Content</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">201</context> <context context-type="linenumber">211</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="218403386307979629" datatype="html"> <trans-unit id="218403386307979629" datatype="html">
<source>Metadata</source> <source>Metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">210</context> <context context-type="linenumber">220</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
@ -5826,119 +5877,119 @@
<source>Date modified</source> <source>Date modified</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">217</context> <context context-type="linenumber">227</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6392918669949841614" datatype="html"> <trans-unit id="6392918669949841614" datatype="html">
<source>Date added</source> <source>Date added</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">221</context> <context context-type="linenumber">231</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="146828917013192897" datatype="html"> <trans-unit id="146828917013192897" datatype="html">
<source>Media filename</source> <source>Media filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">225</context> <context context-type="linenumber">235</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4500855521601039868" datatype="html"> <trans-unit id="4500855521601039868" datatype="html">
<source>Original filename</source> <source>Original filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">229</context> <context context-type="linenumber">239</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7985558498848210210" datatype="html"> <trans-unit id="7985558498848210210" datatype="html">
<source>Original MD5 checksum</source> <source>Original MD5 checksum</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">233</context> <context context-type="linenumber">243</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5888243105821763422" datatype="html"> <trans-unit id="5888243105821763422" datatype="html">
<source>Original file size</source> <source>Original file size</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">237</context> <context context-type="linenumber">247</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2696647325713149563" datatype="html"> <trans-unit id="2696647325713149563" datatype="html">
<source>Original mime type</source> <source>Original mime type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">241</context> <context context-type="linenumber">251</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="342875990758166588" datatype="html"> <trans-unit id="342875990758166588" datatype="html">
<source>Archive MD5 checksum</source> <source>Archive MD5 checksum</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">246</context> <context context-type="linenumber">256</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6033581412811562084" datatype="html"> <trans-unit id="6033581412811562084" datatype="html">
<source>Archive file size</source> <source>Archive file size</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">252</context> <context context-type="linenumber">262</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6992781481378431874" datatype="html"> <trans-unit id="6992781481378431874" datatype="html">
<source>Original document metadata</source> <source>Original document metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">261</context> <context context-type="linenumber">271</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2846565152091361585" datatype="html"> <trans-unit id="2846565152091361585" datatype="html">
<source>Archived document metadata</source> <source>Archived document metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">264</context> <context context-type="linenumber">274</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1295614462098694869" datatype="html"> <trans-unit id="1295614462098694869" datatype="html">
<source>Preview</source> <source>Preview</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">271</context> <context context-type="linenumber">281</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7206723502037428235" datatype="html"> <trans-unit id="7206723502037428235" datatype="html">
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge text-bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source> <source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge text-bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">283,286</context> <context context-type="linenumber">293,296</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="186236568870281953" datatype="html"> <trans-unit id="186236568870281953" datatype="html">
<source>History</source> <source>History</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">294</context> <context context-type="linenumber">304</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5129524307369213584" datatype="html"> <trans-unit id="5129524307369213584" datatype="html">
<source>Save &amp; next</source> <source>Save &amp; next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">331</context> <context context-type="linenumber">341</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4910102545766233758" datatype="html"> <trans-unit id="4910102545766233758" datatype="html">
<source>Save &amp; close</source> <source>Save &amp; close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">334</context> <context context-type="linenumber">344</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8191371354890763172" datatype="html"> <trans-unit id="8191371354890763172" datatype="html">
<source>Enter Password</source> <source>Enter Password</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">385</context> <context context-type="linenumber">395</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2218903673684131427" datatype="html"> <trans-unit id="2218903673684131427" datatype="html">
@ -6073,13 +6124,6 @@
<context context-type="linenumber">716</context> <context context-type="linenumber">716</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7295637485862454066" datatype="html">
<source>Error deleting document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">799</context>
</context-group>
</trans-unit>
<trans-unit id="619486176823357521" datatype="html"> <trans-unit id="619486176823357521" datatype="html">
<source>Reprocess confirm</source> <source>Reprocess confirm</source>
<context-group purpose="location"> <context-group purpose="location">
@ -7348,28 +7392,35 @@
<source>No mail accounts defined.</source> <source>No mail accounts defined.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">50</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5364020217520256833" datatype="html"> <trans-unit id="5364020217520256833" datatype="html">
<source>Mail rules</source> <source>Mail rules</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">56</context> <context context-type="linenumber">58</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1372022816709469401" datatype="html"> <trans-unit id="1372022816709469401" datatype="html">
<source>Add Rule</source> <source>Add Rule</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">60</context>
</context-group>
</trans-unit>
<trans-unit id="2535466903620876415" datatype="html">
<source>Sort Order</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">67</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6751234988479444294" datatype="html"> <trans-unit id="6751234988479444294" datatype="html">
<source>No mail rules defined.</source> <source>No mail rules defined.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
<context context-type="linenumber">92</context> <context context-type="linenumber">96</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3178554336792037159" datatype="html"> <trans-unit id="3178554336792037159" datatype="html">
@ -7805,56 +7856,56 @@
<source>Boolean</source> <source>Boolean</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">18</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3973931101896534797" datatype="html"> <trans-unit id="3973931101896534797" datatype="html">
<source>Date</source> <source>Date</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">22</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="362956598863566327" datatype="html"> <trans-unit id="362956598863566327" datatype="html">
<source>Integer</source> <source>Integer</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">25</context> <context context-type="linenumber">26</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6370642728789544052" datatype="html"> <trans-unit id="6370642728789544052" datatype="html">
<source>Number</source> <source>Number</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">29</context> <context context-type="linenumber">30</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6430409302408843009" datatype="html"> <trans-unit id="6430409302408843009" datatype="html">
<source>Monetary</source> <source>Monetary</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">33</context> <context context-type="linenumber">34</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6162693758764653365" datatype="html"> <trans-unit id="6162693758764653365" datatype="html">
<source>Text</source> <source>Text</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">37</context> <context context-type="linenumber">38</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8308045076391224954" datatype="html"> <trans-unit id="8308045076391224954" datatype="html">
<source>Url</source> <source>Url</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">41</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3650316326183661476" datatype="html"> <trans-unit id="3650316326183661476" datatype="html">
<source>Document Link</source> <source>Document Link</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/custom-field.ts</context> <context context-type="sourcefile">src/app/data/custom-field.ts</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">46</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3553216189604488439" datatype="html"> <trans-unit id="3553216189604488439" datatype="html">

5457
src-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,17 +11,17 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^17.3.10", "@angular/cdk": "^18.0.6",
"@angular/common": "~17.3.9", "@angular/common": "~18.0.6",
"@angular/compiler": "~17.3.9", "@angular/compiler": "~18.0.6",
"@angular/core": "~17.3.9", "@angular/core": "~18.0.6",
"@angular/forms": "~17.3.9", "@angular/forms": "~18.0.6",
"@angular/localize": "~17.3.9", "@angular/localize": "~18.0.6",
"@angular/platform-browser": "~17.3.9", "@angular/platform-browser": "~18.0.6",
"@angular/platform-browser-dynamic": "~17.3.9", "@angular/platform-browser-dynamic": "~18.0.6",
"@angular/router": "~17.3.9", "@angular/router": "~18.0.6",
"@ng-bootstrap/ng-bootstrap": "^16.0.0", "@ng-bootstrap/ng-bootstrap": "^17.0.0",
"@ng-select/ng-select": "^12.0.7", "@ng-select/ng-select": "^13.4.1",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
@ -30,35 +30,37 @@
"ng2-pdf-viewer": "^10.2.2", "ng2-pdf-viewer": "^10.2.2",
"ngx-bootstrap-icons": "^1.9.3", "ngx-bootstrap-icons": "^1.9.3",
"ngx-color": "^9.0.0", "ngx-color": "^9.0.0",
"ngx-cookie-service": "^17.1.0", "ngx-cookie-service": "^18.0.0",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
"ngx-filesize": "^3.0.3", "ngx-ui-tour-ng-bootstrap": "^15.0.0",
"ngx-ui-tour-ng-bootstrap": "^14.0.3",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"zone.js": "^0.14.4" "zone.js": "^0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "17.0.3", "@angular-builders/jest": "^18.0.0",
"@angular-devkit/build-angular": "~17.3.7", "@angular-devkit/build-angular": "^18.0.7",
"@angular-eslint/builder": "17.4.1", "@angular-devkit/core": "^18.0.7",
"@angular-eslint/eslint-plugin": "17.4.1", "@angular-devkit/schematics": "^18.0.7",
"@angular-eslint/eslint-plugin-template": "17.4.1", "@angular-eslint/builder": "18.1.0",
"@angular-eslint/schematics": "17.4.1", "@angular-eslint/eslint-plugin": "18.1.0",
"@angular-eslint/template-parser": "17.4.1", "@angular-eslint/eslint-plugin-template": "18.1.0",
"@angular/cli": "~17.3.7", "@angular-eslint/schematics": "18.1.0",
"@angular/compiler-cli": "~17.3.2", "@angular-eslint/template-parser": "18.1.0",
"@angular/cli": "~18.0.7",
"@angular/compiler-cli": "~18.0.3",
"@playwright/test": "^1.42.1", "@playwright/test": "^1.42.1",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/node": "^20.12.2", "@types/node": "^20.12.2",
"@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0", "@typescript-eslint/parser": "^7.4.0",
"@typescript-eslint/utils": "^7.13.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jest-preset-angular": "^14.1.0", "jest-preset-angular": "^14.0.0",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"ts-node": "~10.9.1", "ts-node": "~10.9.1",

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { import {
ComponentFixture, ComponentFixture,
TestBed, TestBed,
@ -24,6 +24,7 @@ import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
import { HotKeyService } from './services/hot-key.service' import { HotKeyService } from './services/hot-key.service'
import { PermissionsGuard } from './guards/permissions.guard' import { PermissionsGuard } from './guards/permissions.guard'
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard' import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('AppComponent', () => { describe('AppComponent', () => {
let component: AppComponent let component: AppComponent
@ -39,14 +40,18 @@ describe('AppComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AppComponent, ToastsComponent, FileDropComponent], declarations: [AppComponent, ToastsComponent, FileDropComponent],
providers: [PermissionsGuard, DirtySavedViewGuard],
imports: [ imports: [
HttpClientTestingModule,
TourNgBootstrapModule, TourNgBootstrapModule,
RouterModule.forRoot(routes), RouterModule.forRoot(routes),
NgxFileDropModule, NgxFileDropModule,
NgbModalModule, NgbModalModule,
], ],
providers: [
PermissionsGuard,
DirtySavedViewGuard,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
tourService = TestBed.inject(TourService) tourService = TestBed.inject(TourService)

View File

@ -7,7 +7,11 @@ import {
NgbDateParserFormatter, NgbDateParserFormatter,
NgbModule, NgbModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http' import {
HTTP_INTERCEPTORS,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http'
import { DocumentListComponent } from './components/document-list/document-list.component' import { DocumentListComponent } from './components/document-list/document-list.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component' import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DashboardComponent } from './components/dashboard/dashboard.component' import { DashboardComponent } from './components/dashboard/dashboard.component'
@ -115,7 +119,6 @@ import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component' import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
import { MonetaryComponent } from './components/common/input/monetary/monetary.component' import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component' import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
import { NgxFilesizeModule } from 'ngx-filesize'
import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component' import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
@ -500,11 +503,11 @@ function initializeApp(settings: SettingsService) {
DeletePagesConfirmDialogComponent, DeletePagesConfirmDialogComponent,
TrashComponent, TrashComponent,
], ],
bootstrap: [AppComponent],
imports: [ imports: [
BrowserModule, BrowserModule,
AppRoutingModule, AppRoutingModule,
NgbModule, NgbModule,
HttpClientModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
PdfViewerModule, PdfViewerModule,
@ -514,7 +517,6 @@ function initializeApp(settings: SettingsService) {
TourNgBootstrapModule, TourNgBootstrapModule,
DragDropModule, DragDropModule,
NgxBootstrapIconsModule.pick(icons), NgxBootstrapIconsModule.pick(icons),
NgxFilesizeModule,
], ],
providers: [ providers: [
{ {
@ -543,7 +545,7 @@ function initializeApp(settings: SettingsService) {
DirtyDocGuard, DirtyDocGuard,
DirtySavedViewGuard, DirtySavedViewGuard,
UsernamePipe, UsernamePipe,
provideHttpClient(withInterceptorsFromDi()),
], ],
bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule {}

View File

@ -5,7 +5,7 @@ import { ConfigService } from 'src/app/services/config.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { OutputTypeConfig } from 'src/app/data/paperless-config' import { OutputTypeConfig } from 'src/app/data/paperless-config'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
@ -18,6 +18,7 @@ import { SelectComponent } from '../../common/input/select/select.component'
import { FileComponent } from '../../common/input/file/file.component' import { FileComponent } from '../../common/input/file/file.component'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('ConfigComponent', () => { describe('ConfigComponent', () => {
let component: ConfigComponent let component: ConfigComponent
@ -38,7 +39,6 @@ describe('ConfigComponent', () => {
PageHeaderComponent, PageHeaderComponent,
], ],
imports: [ imports: [
HttpClientTestingModule,
BrowserModule, BrowserModule,
NgbModule, NgbModule,
NgSelectModule, NgSelectModule,
@ -46,6 +46,10 @@ describe('ConfigComponent', () => {
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
configService = TestBed.inject(ConfigService) configService = TestBed.inject(ConfigService)

View File

@ -8,10 +8,11 @@ import { LogService } from 'src/app/services/rest/log.service'
import { PageHeaderComponent } from '../../common/page-header/page-header.component' import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LogsComponent } from './logs.component' import { LogsComponent } from './logs.component'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap' import { NgbModule, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'
import { BrowserModule, By } from '@angular/platform-browser' import { BrowserModule, By } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const paperless_logs = [ const paperless_logs = [
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.', '[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
@ -37,13 +38,15 @@ describe('LogsComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [LogsComponent, PageHeaderComponent], declarations: [LogsComponent, PageHeaderComponent],
providers: [],
imports: [ imports: [
HttpClientTestingModule,
BrowserModule, BrowserModule,
NgbModule, NgbModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
logService = TestBed.inject(LogService) logService = TestBed.inject(LogService)

View File

@ -348,7 +348,7 @@
@for (view of savedViews; track view) { @for (view of savedViews; track view) {
<li class="list-group-item py-3"> <li class="list-group-item py-3">
<div [formGroupName]="view.id" class="row"> <div [formGroupName]="view.id">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-text title="Name" formControlName="name"></pngx-input-text> <pngx-input-text title="Name" formControlName="name"></pngx-input-text>

View File

@ -1,5 +1,5 @@
import { ViewportScroller, DatePipe } from '@angular/common' import { ViewportScroller, DatePipe } from '@angular/common'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
@ -50,6 +50,7 @@ import {
} from 'src/app/data/system-status' } from 'src/app/data/system-status'
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component' import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
import { DragDropModule } from '@angular/cdk/drag-drop' import { DragDropModule } from '@angular/cdk/drag-drop'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const savedViews = [ const savedViews = [
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true }, { id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
@ -100,10 +101,8 @@ describe('SettingsComponent', () => {
ConfirmButtonComponent, ConfirmButtonComponent,
DragDropSelectComponent, DragDropSelectComponent,
], ],
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
imports: [ imports: [
NgbModule, NgbModule,
HttpClientTestingModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
@ -113,6 +112,13 @@ describe('SettingsComponent', () => {
NgbModalModule, NgbModalModule,
DragDropModule, DragDropModule,
], ],
providers: [
CustomDatePipe,
DatePipe,
PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
router = TestBed.inject(Router) router = TestBed.inject(Router)

View File

@ -1,7 +1,7 @@
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { import {
HttpTestingController, HttpTestingController,
HttpClientTestingModule, provideHttpClientTesting,
} from '@angular/common/http/testing' } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
@ -30,6 +30,7 @@ import { TasksComponent } from './tasks.component'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const tasks: PaperlessTask[] = [ const tasks: PaperlessTask[] = [
{ {
@ -125,6 +126,12 @@ describe('TasksComponent', () => {
CustomDatePipe, CustomDatePipe,
ConfirmDialogComponent, ConfirmDialogComponent,
], ],
imports: [
NgbModule,
RouterTestingModule.withRoutes(routes),
NgxBootstrapIconsModule.pick(allIcons),
FormsModule,
],
providers: [ providers: [
{ {
provide: PermissionsService, provide: PermissionsService,
@ -135,13 +142,8 @@ describe('TasksComponent', () => {
CustomDatePipe, CustomDatePipe,
DatePipe, DatePipe,
PermissionsGuard, PermissionsGuard,
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
NgbModule,
HttpClientTestingModule,
RouterTestingModule.withRoutes(routes),
NgxBootstrapIconsModule.pick(allIcons),
FormsModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -11,9 +11,11 @@ import {
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { TrashService } from 'src/app/services/trash.service' import { TrashService } from 'src/app/services/trash.service'
import { of } from 'rxjs' import { of, throwError } from 'rxjs'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ToastService } from 'src/app/services/toast.service'
const documentsInTrash = [ const documentsInTrash = [
{ {
@ -35,6 +37,7 @@ describe('TrashComponent', () => {
let fixture: ComponentFixture<TrashComponent> let fixture: ComponentFixture<TrashComponent>
let trashService: TrashService let trashService: TrashService
let modalService: NgbModal let modalService: NgbModal
let toastService: ToastService
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
@ -42,6 +45,7 @@ describe('TrashComponent', () => {
TrashComponent, TrashComponent,
PageHeaderComponent, PageHeaderComponent,
ConfirmDialogComponent, ConfirmDialogComponent,
SafeHtmlPipe,
], ],
imports: [ imports: [
HttpClientTestingModule, HttpClientTestingModule,
@ -56,6 +60,7 @@ describe('TrashComponent', () => {
fixture = TestBed.createComponent(TrashComponent) fixture = TestBed.createComponent(TrashComponent)
trashService = TestBed.inject(TrashService) trashService = TestBed.inject(TrashService)
modalService = TestBed.inject(NgbModal) modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
component = fixture.componentInstance component = fixture.componentInstance
fixture.detectChanges() fixture.detectChanges()
}) })
@ -74,12 +79,20 @@ describe('TrashComponent', () => {
expect(component.documentsInTrash).toEqual(documentsInTrash) expect(component.documentsInTrash).toEqual(documentsInTrash)
}) })
it('should support delete document', () => { it('should support delete document, show error if needed', () => {
const trashSpy = jest.spyOn(trashService, 'emptyTrash') const trashSpy = jest.spyOn(trashService, 'emptyTrash')
let modal let modal
modalService.activeInstances.subscribe((instances) => { modalService.activeInstances.subscribe((instances) => {
modal = instances[0] modal = instances[0]
}) })
const toastErrorSpy = jest.spyOn(toastService, 'showError')
// fail first
trashSpy.mockReturnValue(throwError(() => 'Error'))
component.delete(documentsInTrash[0])
modal.componentInstance.confirmClicked.next()
expect(toastErrorSpy).toHaveBeenCalled()
trashSpy.mockReturnValue(of('OK')) trashSpy.mockReturnValue(of('OK'))
component.delete(documentsInTrash[0]) component.delete(documentsInTrash[0])
expect(modal).toBeDefined() expect(modal).toBeDefined()
@ -87,12 +100,20 @@ describe('TrashComponent', () => {
expect(trashSpy).toHaveBeenCalled() expect(trashSpy).toHaveBeenCalled()
}) })
it('should support empty trash', () => { it('should support empty trash, show error if needed', () => {
const trashSpy = jest.spyOn(trashService, 'emptyTrash') const trashSpy = jest.spyOn(trashService, 'emptyTrash')
let modal let modal
modalService.activeInstances.subscribe((instances) => { modalService.activeInstances.subscribe((instances) => {
modal = instances[instances.length - 1] modal = instances[instances.length - 1]
}) })
const toastErrorSpy = jest.spyOn(toastService, 'showError')
// fail first
trashSpy.mockReturnValue(throwError(() => 'Error'))
component.emptyTrash()
modal.componentInstance.confirmClicked.next()
expect(toastErrorSpy).toHaveBeenCalled()
trashSpy.mockReturnValue(of('OK')) trashSpy.mockReturnValue(of('OK'))
component.emptyTrash() component.emptyTrash()
expect(modal).toBeDefined() expect(modal).toBeDefined()
@ -104,18 +125,34 @@ describe('TrashComponent', () => {
expect(trashSpy).toHaveBeenCalledWith([1, 2]) expect(trashSpy).toHaveBeenCalledWith([1, 2])
}) })
it('should support restore document', () => { it('should support restore document, show error if needed', () => {
const restoreSpy = jest.spyOn(trashService, 'restoreDocuments') const restoreSpy = jest.spyOn(trashService, 'restoreDocuments')
const reloadSpy = jest.spyOn(component, 'reload') const reloadSpy = jest.spyOn(component, 'reload')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
// fail first
restoreSpy.mockReturnValue(throwError(() => 'Error'))
component.restore(documentsInTrash[0])
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
restoreSpy.mockReturnValue(of('OK')) restoreSpy.mockReturnValue(of('OK'))
component.restore(documentsInTrash[0]) component.restore(documentsInTrash[0])
expect(restoreSpy).toHaveBeenCalledWith([documentsInTrash[0].id]) expect(restoreSpy).toHaveBeenCalledWith([documentsInTrash[0].id])
expect(reloadSpy).toHaveBeenCalled() expect(reloadSpy).toHaveBeenCalled()
}) })
it('should support restore all documents', () => { it('should support restore all documents, show error if needed', () => {
const restoreSpy = jest.spyOn(trashService, 'restoreDocuments') const restoreSpy = jest.spyOn(trashService, 'restoreDocuments')
const reloadSpy = jest.spyOn(component, 'reload') const reloadSpy = jest.spyOn(component, 'reload')
const toastErrorSpy = jest.spyOn(toastService, 'showError')
// fail first
restoreSpy.mockReturnValue(throwError(() => 'Error'))
component.restoreAll()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
restoreSpy.mockReturnValue(of('OK')) restoreSpy.mockReturnValue(of('OK'))
component.restoreAll() component.restoreAll()
expect(restoreSpy).toHaveBeenCalled() expect(restoreSpy).toHaveBeenCalled()

View File

@ -59,10 +59,16 @@ export class TrashComponent implements OnDestroy {
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => { .subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
this.trashService.emptyTrash([document.id]).subscribe(() => { this.trashService.emptyTrash([document.id]).subscribe({
this.toastService.showInfo($localize`Document deleted`) next: () => {
modal.close() this.toastService.showInfo($localize`Document deleted`)
this.reload() modal.close()
this.reload()
},
error: (err) => {
this.toastService.showError($localize`Error deleting document`, err)
modal.close()
},
}) })
}) })
} }
@ -83,29 +89,51 @@ export class TrashComponent implements OnDestroy {
.subscribe(() => { .subscribe(() => {
this.trashService this.trashService
.emptyTrash(documents ? Array.from(documents) : null) .emptyTrash(documents ? Array.from(documents) : null)
.subscribe(() => { .subscribe({
this.toastService.showInfo($localize`Document(s) deleted`) next: () => {
this.allToggled = false this.toastService.showInfo($localize`Document(s) deleted`)
modal.close() this.allToggled = false
this.reload() modal.close()
this.reload()
},
error: (err) => {
this.toastService.showError(
$localize`Error deleting document(s)`,
err
)
modal.close()
},
}) })
}) })
} }
restore(document: Document) { restore(document: Document) {
this.trashService.restoreDocuments([document.id]).subscribe(() => { this.trashService.restoreDocuments([document.id]).subscribe({
this.toastService.showInfo($localize`Document restored`) next: () => {
this.reload() this.toastService.showInfo($localize`Document restored`)
this.reload()
},
error: (err) => {
this.toastService.showError($localize`Error restoring document`, err)
},
}) })
} }
restoreAll(documents: Set<number> = null) { restoreAll(documents: Set<number> = null) {
this.trashService this.trashService
.restoreDocuments(documents ? Array.from(documents) : null) .restoreDocuments(documents ? Array.from(documents) : null)
.subscribe(() => { .subscribe({
this.toastService.showInfo($localize`Document(s) restored`) next: () => {
this.allToggled = false this.toastService.showInfo($localize`Document(s) restored`)
this.reload() this.allToggled = false
this.reload()
},
error: (err) => {
this.toastService.showError(
$localize`Error restoring document(s)`,
err
)
},
}) })
} }

View File

@ -1,5 +1,5 @@
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { import {
ComponentFixture, ComponentFixture,
TestBed, TestBed,
@ -44,6 +44,7 @@ import { UsersAndGroupsComponent } from './users-groups.component'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { Group } from 'src/app/data/group' import { Group } from 'src/app/data/group'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const users = [ const users = [
{ id: 1, username: 'user1', is_superuser: false }, { id: 1, username: 'user1', is_superuser: false },
@ -84,10 +85,8 @@ describe('UsersAndGroupsComponent', () => {
PermissionsGroupComponent, PermissionsGroupComponent,
IfOwnerDirective, IfOwnerDirective,
], ],
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
imports: [ imports: [
NgbModule, NgbModule,
HttpClientTestingModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
@ -95,6 +94,13 @@ describe('UsersAndGroupsComponent', () => {
NgSelectModule, NgSelectModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [
CustomDatePipe,
DatePipe,
PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(UsersAndGroupsComponent) fixture = TestBed.createComponent(UsersAndGroupsComponent)
settingsService = TestBed.inject(SettingsService) settingsService = TestBed.inject(SettingsService)

View File

@ -1,6 +1,6 @@
import { import {
HttpClientTestingModule,
HttpTestingController, HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing' } from '@angular/common/http/testing'
import { AppFrameComponent } from './app-frame.component' import { AppFrameComponent } from './app-frame.component'
import { import {
@ -37,6 +37,7 @@ import { SavedView } from 'src/app/data/saved-view'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component' import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { GlobalSearchComponent } from './global-search/global-search.component' import { GlobalSearchComponent } from './global-search/global-search.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const saved_views = [ const saved_views = [
{ {
@ -100,7 +101,6 @@ describe('AppFrameComponent', () => {
GlobalSearchComponent, GlobalSearchComponent,
], ],
imports: [ imports: [
HttpClientTestingModule,
BrowserModule, BrowserModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
NgbModule, NgbModule,
@ -150,6 +150,8 @@ describe('AppFrameComponent', () => {
}, },
}, },
PermissionsGuard, PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
], ],
}).compileComponents() }).compileComponents()

View File

@ -17,7 +17,7 @@ import {
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component' import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { import {
FILTER_FULLTEXT_QUERY, FILTER_FULLTEXT_QUERY,
@ -40,6 +40,7 @@ import { DataType } from 'src/app/data/datatype'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params' import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings' import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const searchResults = { const searchResults = {
total: 11, total: 11,
@ -139,13 +140,16 @@ describe('GlobalSearchComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [GlobalSearchComponent], declarations: [GlobalSearchComponent],
imports: [ imports: [
HttpClientTestingModule,
NgbModalModule, NgbModalModule,
NgbDropdownModule, NgbDropdownModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
searchService = TestBed.inject(SearchService) searchService = TestBed.inject(SearchService)

View File

@ -1,11 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component' import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { PdfViewerComponent } from 'ng2-pdf-viewer' import { PdfViewerComponent } from 'ng2-pdf-viewer'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('DeletePagesConfirmDialogComponent', () => { describe('DeletePagesConfirmDialogComponent', () => {
let component: DeletePagesConfirmDialogComponent let component: DeletePagesConfirmDialogComponent
@ -14,13 +15,17 @@ describe('DeletePagesConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [DeletePagesConfirmDialogComponent, PdfViewerComponent], declarations: [DeletePagesConfirmDialogComponent, PdfViewerComponent],
providers: [NgbActiveModal, SafeHtmlPipe],
imports: [ imports: [
HttpClientTestingModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
], ],
providers: [
NgbActiveModal,
SafeHtmlPipe,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(DeletePagesConfirmDialogComponent) fixture = TestBed.createComponent(DeletePagesConfirmDialogComponent)
component = fixture.componentInstance component = fixture.componentInstance

View File

@ -1,11 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component' import { MergeConfirmDialogComponent } from './merge-confirm-dialog.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs' import { of } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('MergeConfirmDialogComponent', () => { describe('MergeConfirmDialogComponent', () => {
let component: MergeConfirmDialogComponent let component: MergeConfirmDialogComponent
@ -15,13 +16,16 @@ describe('MergeConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [MergeConfirmDialogComponent], declarations: [MergeConfirmDialogComponent],
providers: [NgbActiveModal],
imports: [ imports: [
HttpClientTestingModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
], ],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(MergeConfirmDialogComponent) fixture = TestBed.createComponent(MergeConfirmDialogComponent)

View File

@ -2,8 +2,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component' import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('RotateConfirmDialogComponent', () => { describe('RotateConfirmDialogComponent', () => {
let component: RotateConfirmDialogComponent let component: RotateConfirmDialogComponent
@ -12,10 +13,12 @@ describe('RotateConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [RotateConfirmDialogComponent, SafeHtmlPipe], declarations: [RotateConfirmDialogComponent, SafeHtmlPipe],
providers: [NgbActiveModal, SafeHtmlPipe], imports: [NgxBootstrapIconsModule.pick(allIcons)],
imports: [ providers: [
HttpClientTestingModule, NgbActiveModal,
NgxBootstrapIconsModule.pick(allIcons), SafeHtmlPipe,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
], ],
}).compileComponents() }).compileComponents()

View File

@ -23,7 +23,7 @@
</div> </div>
<div class="col-4"> <div class="col-4">
<div class="d-grid"> <div class="d-grid">
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="page === totalPages"> <button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit">
<i-bs name="plus-circle"></i-bs>&nbsp; <i-bs name="plus-circle"></i-bs>&nbsp;
<span i18n>Add Split</span> <span i18n>Add Split</span>
</button> </button>

View File

@ -1,13 +1,14 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { SplitConfirmDialogComponent } from './split-confirm-dialog.component' import { SplitConfirmDialogComponent } from './split-confirm-dialog.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ReactiveFormsModule, FormsModule } from '@angular/forms' import { ReactiveFormsModule, FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { PdfViewerModule } from 'ng2-pdf-viewer' import { PdfViewerModule } from 'ng2-pdf-viewer'
import { of } from 'rxjs' import { of } from 'rxjs'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('SplitConfirmDialogComponent', () => { describe('SplitConfirmDialogComponent', () => {
let component: SplitConfirmDialogComponent let component: SplitConfirmDialogComponent
@ -17,14 +18,17 @@ describe('SplitConfirmDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [SplitConfirmDialogComponent], declarations: [SplitConfirmDialogComponent],
providers: [NgbActiveModal],
imports: [ imports: [
HttpClientTestingModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
PdfViewerModule, PdfViewerModule,
], ],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(SplitConfirmDialogComponent) fixture = TestBed.createComponent(SplitConfirmDialogComponent)
@ -88,4 +92,16 @@ describe('SplitConfirmDialogComponent', () => {
component.pdfPreviewLoaded({ numPages: 5 } as any) component.pdfPreviewLoaded({ numPages: 5 } as any)
expect(component.totalPages).toEqual(5) expect(component.totalPages).toEqual(5)
}) })
it('should correctly disable split button', () => {
component.totalPages = 5
component.page = 1
expect(component.canSplit).toBeTruthy()
component.page = 5
expect(component.canSplit).toBeFalsy()
component.page = 4
expect(component.canSplit).toBeTruthy()
component['pages'] = new Set([1, 2, 3, 4])
expect(component.canSplit).toBeFalsy()
})
}) })

View File

@ -42,6 +42,14 @@ export class SplitConfirmDialogComponent
public totalPages: number public totalPages: number
public deleteOriginal: boolean = false public deleteOriginal: boolean = false
public get canSplit(): boolean {
return (
this.page < this.totalPages &&
this.pages.size < this.totalPages - 1 &&
!this.pages.has(this.page)
)
}
public get pdfSrc(): string { public get pdfSrc(): string {
return this.documentService.getPreviewUrl(this.documentID) return this.documentService.getPreviewUrl(this.documentID)
} }

View File

@ -30,6 +30,9 @@
<input type="checkbox" id="{{field.name}}" name="{{field.name}}" [checked]="value" value="" class="form-check-input ms-2 mt-0 pe-none"> <input type="checkbox" id="{{field.name}}" name="{{field.name}}" [checked]="value" value="" class="form-check-input ms-2 mt-0 pe-none">
</div> </div>
} }
@case (CustomFieldDataType.Select) {
<span [ngbTooltip]="nameTooltip">{{getSelectValue(field, value)}}</span>
}
@default { @default {
<span [ngbTooltip]="nameTooltip">{{value}}</span> <span [ngbTooltip]="nameTooltip">{{value}}</span>
} }

View File

@ -4,13 +4,22 @@ import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { CustomFieldDisplayComponent } from './custom-field-display.component' import { CustomFieldDisplayComponent } from './custom-field-display.component'
import { DisplayField, Document } from 'src/app/data/document' import { DisplayField, Document } from 'src/app/data/document'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const customFields: CustomField[] = [ const customFields: CustomField[] = [
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String }, { id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
{ id: 2, name: 'Field 2', data_type: CustomFieldDataType.Monetary }, { id: 2, name: 'Field 2', data_type: CustomFieldDataType.Monetary },
{ id: 3, name: 'Field 3', data_type: CustomFieldDataType.DocumentLink }, { id: 3, name: 'Field 3', data_type: CustomFieldDataType.DocumentLink },
{
id: 4,
name: 'Field 4',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: ['Option 1', 'Option 2', 'Option 3'],
},
},
] ]
const document: Document = { const document: Document = {
id: 1, id: 1,
@ -31,8 +40,12 @@ describe('CustomFieldDisplayComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [CustomFieldDisplayComponent], declarations: [CustomFieldDisplayComponent],
providers: [DocumentService], imports: [],
imports: [HttpClientTestingModule], providers: [
DocumentService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
}) })
@ -98,4 +111,8 @@ describe('CustomFieldDisplayComponent', () => {
expect(component.currency).toEqual('EUR') expect(component.currency).toEqual('EUR')
expect(component.value).toEqual(100) expect(component.value).toEqual(100)
}) })
it('should show select value', () => {
expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
})
}) })

View File

@ -115,6 +115,10 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
return this.docLinkDocuments?.find((d) => d.id === docId)?.title return this.docLinkDocuments?.find((d) => d.id === docId)?.title
} }
public getSelectValue(field: CustomField, index: number): string {
return field.extra_data.select_options[index]
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.unsubscribeNotifier.next(true) this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete() this.unsubscribeNotifier.complete()

View File

@ -5,7 +5,7 @@ import {
tick, tick,
} from '@angular/core/testing' } from '@angular/core/testing'
import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component' import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { of } from 'rxjs' import { of } from 'rxjs'
@ -22,6 +22,7 @@ import {
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const fields: CustomField[] = [ const fields: CustomField[] = [
{ {
@ -47,7 +48,6 @@ describe('CustomFieldsDropdownComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [CustomFieldsDropdownComponent, SelectComponent], declarations: [CustomFieldsDropdownComponent, SelectComponent],
imports: [ imports: [
HttpClientTestingModule,
NgSelectModule, NgSelectModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
@ -55,6 +55,10 @@ describe('CustomFieldsDropdownComponent', () => {
NgbDropdownModule, NgbDropdownModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}) })
customFieldService = TestBed.inject(CustomFieldsService) customFieldService = TestBed.inject(CustomFieldsService)
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)

View File

@ -10,7 +10,7 @@ import {
DateSelection, DateSelection,
RelativeDate, RelativeDate,
} from './dates-dropdown.component' } from './dates-dropdown.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component' import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
@ -18,6 +18,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('DatesDropdownComponent', () => { describe('DatesDropdownComponent', () => {
let component: DatesDropdownComponent let component: DatesDropdownComponent
@ -31,14 +32,19 @@ describe('DatesDropdownComponent', () => {
ClearableBadgeComponent, ClearableBadgeComponent,
CustomDatePipe, CustomDatePipe,
], ],
providers: [SettingsService, CustomDatePipe, DatePipe],
imports: [ imports: [
HttpClientTestingModule,
NgbModule, NgbModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [
SettingsService,
CustomDatePipe,
DatePipe,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
settingsService = TestBed.inject(SettingsService) settingsService = TestBed.inject(SettingsService)

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
@ -11,6 +11,7 @@ import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component' import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('CorrespondentEditDialogComponent', () => { describe('CorrespondentEditDialogComponent', () => {
let component: CorrespondentEditDialogComponent let component: CorrespondentEditDialogComponent
@ -27,13 +28,11 @@ describe('CorrespondentEditDialogComponent', () => {
TextComponent, TextComponent,
PermissionsFormComponent, PermissionsFormComponent,
], ],
providers: [NgbActiveModal], imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
imports: [ providers: [
HttpClientTestingModule, NgbActiveModal,
FormsModule, provideHttpClient(withInterceptorsFromDi()),
ReactiveFormsModule, provideHttpClientTesting(),
NgSelectModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -13,6 +13,23 @@
@if (typeFieldDisabled) { @if (typeFieldDisabled) {
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small> <small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
} }
<div [formGroup]="objectForm.controls.extra_data">
@switch (objectForm.get('data_type').value) {
@case (CustomFieldDataType.Select) {
<button type="button" class="btn btn-sm btn-primary my-2" (click)="addSelectOption()">
<span i18n>Add option</span>&nbsp;<i-bs name="plus-circle"></i-bs>
</button>
<div formArrayName="select_options">
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
<div class="input-group input-group-sm my-2">
<input #selectOption type="text" class="form-control" [formControl]="option" autocomplete="off">
<button type="button" class="btn btn-outline-danger" (click)="removeSelectOption(i)" i18n>Delete</button>
</div>
}
</div>
}
}
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { CustomFieldEditDialogComponent } from './custom-field-edit-dialog.component' import { CustomFieldEditDialogComponent } from './custom-field-edit-dialog.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
@ -12,6 +12,10 @@ import { SettingsService } from 'src/app/services/settings.service'
import { SelectComponent } from '../../input/select/select.component' import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { CustomFieldDataType } from 'src/app/data/custom-field'
import { ElementRef, QueryList } from '@angular/core'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('CustomFieldEditDialogComponent', () => { describe('CustomFieldEditDialogComponent', () => {
let component: CustomFieldEditDialogComponent let component: CustomFieldEditDialogComponent
@ -28,13 +32,17 @@ describe('CustomFieldEditDialogComponent', () => {
TextComponent, TextComponent,
SafeHtmlPipe, SafeHtmlPipe,
], ],
providers: [NgbActiveModal],
imports: [ imports: [
HttpClientTestingModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgSelectModule, NgSelectModule,
NgbModule, NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
], ],
}).compileComponents() }).compileComponents()
@ -64,4 +72,55 @@ describe('CustomFieldEditDialogComponent', () => {
component.ngOnInit() component.ngOnInit()
expect(component.objectForm.get('data_type').disabled).toBeTruthy() expect(component.objectForm.get('data_type').disabled).toBeTruthy()
}) })
it('should initialize select options on edit', () => {
component.dialogMode = EditDialogMode.EDIT
component.object = {
id: 1,
name: 'Field 1',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: ['Option 1', 'Option 2', 'Option 3'],
},
}
fixture.detectChanges()
component.ngOnInit()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(3)
})
it('should support add / remove select options', () => {
component.dialogMode = EditDialogMode.CREATE
fixture.detectChanges()
component.ngOnInit()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(1)
component.addSelectOption()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(2)
component.addSelectOption()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(3)
component.removeSelectOption(0)
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(2)
})
it('should focus on last select option input', () => {
const selectOptionInputs = component[
'selectOptionInputs'
] as QueryList<ElementRef>
component.dialogMode = EditDialogMode.CREATE
component.objectForm.get('data_type').setValue(CustomFieldDataType.Select)
component.ngOnInit()
component.ngAfterViewInit()
component.addSelectOption()
fixture.detectChanges()
expect(document.activeElement).toBe(selectOptionInputs.last.nativeElement)
})
}) })

View File

@ -1,11 +1,24 @@
import { Component, OnInit } from '@angular/core' import {
import { FormGroup, FormControl } from '@angular/forms' AfterViewInit,
Component,
ElementRef,
OnDestroy,
OnInit,
QueryList,
ViewChildren,
} from '@angular/core'
import { FormGroup, FormControl, FormArray } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { DATA_TYPE_LABELS, CustomField } from 'src/app/data/custom-field' import {
DATA_TYPE_LABELS,
CustomField,
CustomFieldDataType,
} from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component' import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
import { Subject, takeUntil } from 'rxjs'
@Component({ @Component({
selector: 'pngx-custom-field-edit-dialog', selector: 'pngx-custom-field-edit-dialog',
@ -14,8 +27,20 @@ import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
}) })
export class CustomFieldEditDialogComponent export class CustomFieldEditDialogComponent
extends EditDialogComponent<CustomField> extends EditDialogComponent<CustomField>
implements OnInit implements OnInit, AfterViewInit, OnDestroy
{ {
CustomFieldDataType = CustomFieldDataType
@ViewChildren('selectOption')
private selectOptionInputs: QueryList<ElementRef>
private unsubscribeNotifier: Subject<any> = new Subject()
private get selectOptions(): FormArray {
return (this.objectForm.controls.extra_data as FormGroup).controls
.select_options as FormArray
}
constructor( constructor(
service: CustomFieldsService, service: CustomFieldsService,
activeModal: NgbActiveModal, activeModal: NgbActiveModal,
@ -30,6 +55,25 @@ export class CustomFieldEditDialogComponent
if (this.typeFieldDisabled) { if (this.typeFieldDisabled) {
this.objectForm.get('data_type').disable() this.objectForm.get('data_type').disable()
} }
if (this.object?.data_type === CustomFieldDataType.Select) {
this.selectOptions.clear()
this.object.extra_data.select_options.forEach((option) =>
this.selectOptions.push(new FormControl(option))
)
}
}
ngAfterViewInit(): void {
this.selectOptionInputs.changes
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.selectOptionInputs.last.nativeElement.focus()
})
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
} }
getCreateTitle() { getCreateTitle() {
@ -44,6 +88,9 @@ export class CustomFieldEditDialogComponent
return new FormGroup({ return new FormGroup({
name: new FormControl(null), name: new FormControl(null),
data_type: new FormControl(null), data_type: new FormControl(null),
extra_data: new FormGroup({
select_options: new FormArray([new FormControl(null)]),
}),
}) })
} }
@ -54,4 +101,12 @@ export class CustomFieldEditDialogComponent
get typeFieldDisabled(): boolean { get typeFieldDisabled(): boolean {
return this.dialogMode === EditDialogMode.EDIT return this.dialogMode === EditDialogMode.EDIT
} }
public addSelectOption() {
this.selectOptions.push(new FormControl(''))
}
public removeSelectOption(index: number) {
this.selectOptions.removeAt(index)
}
} }

View File

@ -10,7 +10,7 @@
<div class="modal-body"> <div class="modal-body">
<div class="col"> <div class="col">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select> <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
@if (patternRequired) { @if (patternRequired) {
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text> <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
@ -11,6 +11,7 @@ import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component' import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('DocumentTypeEditDialogComponent', () => { describe('DocumentTypeEditDialogComponent', () => {
let component: DocumentTypeEditDialogComponent let component: DocumentTypeEditDialogComponent
@ -27,13 +28,11 @@ describe('DocumentTypeEditDialogComponent', () => {
TextComponent, TextComponent,
PermissionsFormComponent, PermissionsFormComponent,
], ],
providers: [NgbActiveModal], imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
imports: [ providers: [
HttpClientTestingModule, NgbActiveModal,
FormsModule, provideHttpClient(withInterceptorsFromDi()),
ReactiveFormsModule, provideHttpClientTesting(),
NgSelectModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,6 +1,6 @@
import { import {
HttpTestingController, HttpTestingController,
HttpClientTestingModule, provideHttpClientTesting,
} from '@angular/common/http/testing' } from '@angular/common/http/testing'
import { Component } from '@angular/core' import { Component } from '@angular/core'
import { import {
@ -30,6 +30,7 @@ import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { EditDialogComponent, EditDialogMode } from './edit-dialog.component' import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
@Component({ @Component({
template: ` template: `
@ -96,6 +97,7 @@ describe('EditDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [TestComponent], declarations: [TestComponent],
imports: [FormsModule, ReactiveFormsModule],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
{ {
@ -114,8 +116,9 @@ describe('EditDialogComponent', () => {
}, },
SettingsService, SettingsService,
TagService, TagService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
], ],
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
}).compileComponents() }).compileComponents()
tagService = TestBed.inject(TagService) tagService = TestBed.inject(TagService)

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
@ -12,6 +12,7 @@ import { TextComponent } from '../../input/text/text.component'
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component' import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { GroupEditDialogComponent } from './group-edit-dialog.component' import { GroupEditDialogComponent } from './group-edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('GroupEditDialogComponent', () => { describe('GroupEditDialogComponent', () => {
let component: GroupEditDialogComponent let component: GroupEditDialogComponent
@ -29,13 +30,11 @@ describe('GroupEditDialogComponent', () => {
PermissionsFormComponent, PermissionsFormComponent,
PermissionsSelectComponent, PermissionsSelectComponent,
], ],
providers: [NgbActiveModal], imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
imports: [ providers: [
HttpClientTestingModule, NgbActiveModal,
FormsModule, provideHttpClient(withInterceptorsFromDi()),
ReactiveFormsModule, provideHttpClientTesting(),
NgSelectModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,6 +1,6 @@
import { import {
HttpTestingController, HttpTestingController,
HttpClientTestingModule, provideHttpClientTesting,
} from '@angular/common/http/testing' } from '@angular/common/http/testing'
import { import {
ComponentFixture, ComponentFixture,
@ -23,6 +23,7 @@ import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { MailAccountEditDialogComponent } from './mail-account-edit-dialog.component' import { MailAccountEditDialogComponent } from './mail-account-edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('MailAccountEditDialogComponent', () => { describe('MailAccountEditDialogComponent', () => {
let component: MailAccountEditDialogComponent let component: MailAccountEditDialogComponent
@ -42,13 +43,11 @@ describe('MailAccountEditDialogComponent', () => {
PermissionsFormComponent, PermissionsFormComponent,
PasswordComponent, PasswordComponent,
], ],
providers: [NgbActiveModal], imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
imports: [ providers: [
HttpClientTestingModule, NgbActiveModal,
FormsModule, provideHttpClient(withInterceptorsFromDi()),
ReactiveFormsModule, provideHttpClientTesting(),
NgSelectModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
@ -23,6 +23,7 @@ import { TagsComponent } from '../../input/tags/tags.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { MailRuleEditDialogComponent } from './mail-rule-edit-dialog.component' import { MailRuleEditDialogComponent } from './mail-rule-edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('MailRuleEditDialogComponent', () => { describe('MailRuleEditDialogComponent', () => {
let component: MailRuleEditDialogComponent let component: MailRuleEditDialogComponent
@ -43,6 +44,7 @@ describe('MailRuleEditDialogComponent', () => {
SafeHtmlPipe, SafeHtmlPipe,
CheckComponent, CheckComponent,
], ],
imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
{ {
@ -63,13 +65,8 @@ describe('MailRuleEditDialogComponent', () => {
listAll: () => of([]), listAll: () => of([]),
}, },
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
@ -12,6 +12,7 @@ import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component' import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component' import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('StoragePathEditDialogComponent', () => { describe('StoragePathEditDialogComponent', () => {
let component: StoragePathEditDialogComponent let component: StoragePathEditDialogComponent
@ -29,13 +30,11 @@ describe('StoragePathEditDialogComponent', () => {
PermissionsFormComponent, PermissionsFormComponent,
SafeHtmlPipe, SafeHtmlPipe,
], ],
providers: [NgbActiveModal], imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
imports: [ providers: [
HttpClientTestingModule, NgbActiveModal,
FormsModule, provideHttpClient(withInterceptorsFromDi()),
ReactiveFormsModule, provideHttpClientTesting(),
NgSelectModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
@ -14,6 +14,7 @@ import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { TagEditDialogComponent } from './tag-edit-dialog.component' import { TagEditDialogComponent } from './tag-edit-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('TagEditDialogComponent', () => { describe('TagEditDialogComponent', () => {
let component: TagEditDialogComponent let component: TagEditDialogComponent
@ -32,15 +33,19 @@ describe('TagEditDialogComponent', () => {
ColorComponent, ColorComponent,
CheckComponent, CheckComponent,
], ],
providers: [NgbActiveModal, SettingsService],
imports: [ imports: [
HttpClientTestingModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgSelectModule, NgSelectModule,
NgbModule, NgbModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [
NgbActiveModal,
SettingsService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(TagEditDialogComponent) fixture = TestBed.createComponent(TagEditDialogComponent)

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { import {
FormsModule, FormsModule,
@ -19,6 +19,7 @@ import { TextComponent } from '../../input/text/text.component'
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component' import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
import { EditDialogMode } from '../edit-dialog.component' import { EditDialogMode } from '../edit-dialog.component'
import { UserEditDialogComponent } from './user-edit-dialog.component' import { UserEditDialogComponent } from './user-edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('UserEditDialogComponent', () => { describe('UserEditDialogComponent', () => {
let component: UserEditDialogComponent let component: UserEditDialogComponent
@ -37,6 +38,7 @@ describe('UserEditDialogComponent', () => {
PermissionsFormComponent, PermissionsFormComponent,
PermissionsSelectComponent, PermissionsSelectComponent,
], ],
imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
{ {
@ -54,13 +56,8 @@ describe('UserEditDialogComponent', () => {
}, },
}, },
SettingsService, SettingsService,
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
@ -39,6 +39,7 @@ import {
} from 'src/app/data/workflow-action' } from 'src/app/data/workflow-action'
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model' import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component' import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const workflow: Workflow = { const workflow: Workflow = {
name: 'Workflow 1', name: 'Workflow 1',
@ -88,6 +89,7 @@ describe('WorkflowEditDialogComponent', () => {
SafeHtmlPipe, SafeHtmlPipe,
ConfirmButtonComponent, ConfirmButtonComponent,
], ],
imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
{ {
@ -150,13 +152,8 @@ describe('WorkflowEditDialogComponent', () => {
}), }),
}, },
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -5,7 +5,7 @@ import {
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { DateComponent } from './date.component' import { DateComponent } from './date.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { import {
NgbDateParserFormatter, NgbDateParserFormatter,
NgbDatepickerModule, NgbDatepickerModule,
@ -13,6 +13,7 @@ import {
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter' import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('DateComponent', () => { describe('DateComponent', () => {
let component: DateComponent let component: DateComponent
@ -22,19 +23,20 @@ describe('DateComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DateComponent], declarations: [DateComponent],
imports: [
FormsModule,
ReactiveFormsModule,
NgbDatepickerModule,
RouterTestingModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [ providers: [
{ {
provide: NgbDateParserFormatter, provide: NgbDateParserFormatter,
useClass: LocalizedDateParserFormatter, useClass: LocalizedDateParserFormatter,
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule,
NgbDatepickerModule,
RouterTestingModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { import {
FormsModule, FormsModule,
@ -10,6 +10,7 @@ import { of, throwError } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { DocumentLinkComponent } from './document-link.component' import { DocumentLinkComponent } from './document-link.component'
import { FILTER_TITLE } from 'src/app/data/filter-rule-type' import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const documents = [ const documents = [
{ {
@ -38,11 +39,10 @@ describe('DocumentLinkComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DocumentLinkComponent], declarations: [DocumentLinkComponent],
imports: [ imports: [NgSelectModule, FormsModule, ReactiveFormsModule],
HttpClientTestingModule, providers: [
NgSelectModule, provideHttpClient(withInterceptorsFromDi()),
FormsModule, provideHttpClientTesting(),
ReactiveFormsModule,
], ],
}) })
documentService = TestBed.inject(DocumentService) documentService = TestBed.inject(DocumentService)

View File

@ -13,10 +13,10 @@
} }
</div> </div>
</div> </div>
<div class="d-flex flex-row mt-2 align-items-center bg-light p-2"> <div class="mt-2 align-items-center bg-light p-2">
<div class="d-flex flex-row gap-2 w-100 mh-1" style="min-height: 1em;" <div class="d-flex flex-wrap flex-row gap-2 w-100"
cdkDropList #unselectedList="cdkDropList" cdkDropList #unselectedList="cdkDropList"
cdkDropListOrientation="horizontal" cdkDropListOrientation="mixed"
(cdkDropListDropped)="drop($event)" (cdkDropListDropped)="drop($event)"
[cdkDropListConnectedTo]="[selectedList]"> [cdkDropListConnectedTo]="[selectedList]">
@for (item of unselectedItems; track item.id) { @for (item of unselectedItems; track item.id) {

View File

@ -1,7 +1,3 @@
.badge { .badge {
cursor: move; cursor: move;
} }
.d-flex {
overflow-x: scroll;
}

View File

@ -1,8 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FileComponent } from './file.component' import { FileComponent } from './file.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('FileComponent', () => { describe('FileComponent', () => {
let component: FileComponent let component: FileComponent
@ -11,7 +12,11 @@ describe('FileComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FileComponent], declarations: [FileComponent],
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule], imports: [FormsModule, ReactiveFormsModule],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(FileComponent) fixture = TestBed.createComponent(FileComponent)

View File

@ -4,9 +4,10 @@ import {
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { CurrencyPipe } from '@angular/common' import { CurrencyPipe } from '@angular/common'
import { MonetaryComponent } from './monetary.component' import { MonetaryComponent } from './monetary.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('MonetaryComponent', () => { describe('MonetaryComponent', () => {
let component: MonetaryComponent let component: MonetaryComponent
@ -15,8 +16,12 @@ describe('MonetaryComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [MonetaryComponent], declarations: [MonetaryComponent],
providers: [CurrencyPipe], imports: [FormsModule, ReactiveFormsModule],
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule], providers: [
CurrencyPipe,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(MonetaryComponent) fixture = TestBed.createComponent(MonetaryComponent)

View File

@ -6,8 +6,9 @@ import {
} from '@angular/forms' } from '@angular/forms'
import { NumberComponent } from './number.component' import { NumberComponent } from './number.component'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { of } from 'rxjs' import { of } from 'rxjs'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('NumberComponent', () => { describe('NumberComponent', () => {
let component: NumberComponent let component: NumberComponent
@ -18,8 +19,12 @@ describe('NumberComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [NumberComponent], declarations: [NumberComponent],
providers: [DocumentService], imports: [FormsModule, ReactiveFormsModule],
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule], providers: [
DocumentService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(NumberComponent) fixture = TestBed.createComponent(NumberComponent)

View File

@ -9,8 +9,9 @@ import { SelectComponent } from '../../select/select.component'
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap' import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
import { PermissionsGroupComponent } from '../permissions-group/permissions-group.component' import { PermissionsGroupComponent } from '../permissions-group/permissions-group.component'
import { PermissionsUserComponent } from '../permissions-user/permissions-user.component' import { PermissionsUserComponent } from '../permissions-user/permissions-user.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('PermissionsFormComponent', () => { describe('PermissionsFormComponent', () => {
let component: PermissionsFormComponent let component: PermissionsFormComponent
@ -24,14 +25,16 @@ describe('PermissionsFormComponent', () => {
PermissionsGroupComponent, PermissionsGroupComponent,
PermissionsUserComponent, PermissionsUserComponent,
], ],
providers: [],
imports: [ imports: [
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgbAccordionModule, NgbAccordionModule,
HttpClientTestingModule,
NgSelectModule, NgSelectModule,
], ],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(PermissionsFormComponent) fixture = TestBed.createComponent(PermissionsFormComponent)

View File

@ -5,10 +5,11 @@ import {
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { PermissionsGroupComponent } from './permissions-group.component' import { PermissionsGroupComponent } from './permissions-group.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { GroupService } from 'src/app/services/rest/group.service' import { GroupService } from 'src/app/services/rest/group.service'
import { of } from 'rxjs' import { of } from 'rxjs'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('PermissionsGroupComponent', () => { describe('PermissionsGroupComponent', () => {
let component: PermissionsGroupComponent let component: PermissionsGroupComponent
@ -19,12 +20,11 @@ describe('PermissionsGroupComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PermissionsGroupComponent], declarations: [PermissionsGroupComponent],
providers: [GroupService], imports: [FormsModule, ReactiveFormsModule, NgSelectModule],
imports: [ providers: [
FormsModule, GroupService,
ReactiveFormsModule, provideHttpClient(withInterceptorsFromDi()),
HttpClientTestingModule, provideHttpClientTesting(),
NgSelectModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -5,11 +5,12 @@ import {
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { PermissionsUserComponent } from './permissions-user.component' import { PermissionsUserComponent } from './permissions-user.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { GroupService } from 'src/app/services/rest/group.service' import { GroupService } from 'src/app/services/rest/group.service'
import { of } from 'rxjs' import { of } from 'rxjs'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('PermissionsUserComponent', () => { describe('PermissionsUserComponent', () => {
let component: PermissionsUserComponent let component: PermissionsUserComponent
@ -20,12 +21,11 @@ describe('PermissionsUserComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PermissionsUserComponent], declarations: [PermissionsUserComponent],
providers: [UserService], imports: [FormsModule, ReactiveFormsModule, NgSelectModule],
imports: [ providers: [
FormsModule, UserService,
ReactiveFormsModule, provideHttpClient(withInterceptorsFromDi()),
HttpClientTestingModule, provideHttpClientTesting(),
NgSelectModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -132,4 +132,12 @@ describe('SelectComponent', () => {
const expectedTitle = `Filter documents with this ${component.title}` const expectedTitle = `Filter documents with this ${component.title}`
expect(component.filterButtonTitle).toEqual(expectedTitle) expect(component.filterButtonTitle).toEqual(expectedTitle)
}) })
it('should support setting items as a plain array', () => {
component.itemsArray = ['foo', 'bar']
expect(component.items).toEqual([
{ id: 0, name: 'foo' },
{ id: 1, name: 'bar' },
])
})
}) })

View File

@ -34,6 +34,11 @@ export class SelectComponent extends AbstractInputComponent<number> {
if (items && this.value) this.checkForPrivateItems(this.value) if (items && this.value) this.checkForPrivateItems(this.value)
} }
@Input()
set itemsArray(items: any[]) {
this._items = items.map((item, index) => ({ id: index, name: item }))
}
writeValue(newValue: any): void { writeValue(newValue: any): void {
if (newValue && this._items) { if (newValue && this._items) {
this.checkForPrivateItems(newValue) this.checkForPrivateItems(newValue)

View File

@ -49,7 +49,7 @@
@if (getSuggestions().length > 0) { @if (getSuggestions().length > 0) {
<small class="position-absolute top-100"> <small class="position-absolute top-100">
<span i18n>Suggestions:</span>&nbsp; <span i18n>Suggestions:</span>&nbsp;
@for (tag of getSuggestions(); track tag) { @for (tag of getSuggestions(); track tag.id) {
<a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>&nbsp; <a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a>&nbsp;
} }
</small> </small>

View File

@ -12,7 +12,7 @@ import {
} from 'src/app/data/matching-model' } from 'src/app/data/matching-model'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { of } from 'rxjs' import { of } from 'rxjs'
import { TagService } from 'src/app/services/rest/tag.service' import { TagService } from 'src/app/services/rest/tag.service'
import { import {
@ -31,6 +31,7 @@ import { PermissionsFormComponent } from '../permissions/permissions-form/permis
import { SelectComponent } from '../select/select.component' import { SelectComponent } from '../select/select.component'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const tags: Tag[] = [ const tags: Tag[] = [
{ {
@ -74,6 +75,16 @@ describe('TagsComponent', () => {
ColorComponent, ColorComponent,
CheckComponent, CheckComponent,
], ],
imports: [
FormsModule,
ReactiveFormsModule,
NgSelectModule,
RouterTestingModule,
NgbModalModule,
NgbAccordionModule,
NgbPopoverModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [ providers: [
{ {
provide: TagService, provide: TagService,
@ -90,17 +101,8 @@ describe('TagsComponent', () => {
}), }),
}, },
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
FormsModule,
ReactiveFormsModule,
NgSelectModule,
RouterTestingModule,
HttpClientTestingModule,
NgbModalModule,
NgbAccordionModule,
NgbPopoverModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@ -2,9 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
import { LogoComponent } from './logo.component' import { LogoComponent } from './logo.component'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('LogoComponent', () => { describe('LogoComponent', () => {
let component: LogoComponent let component: LogoComponent
@ -14,7 +15,11 @@ describe('LogoComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [LogoComponent], declarations: [LogoComponent],
imports: [HttpClientTestingModule], imports: [],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}) })
settingsService = TestBed.inject(SettingsService) settingsService = TestBed.inject(SettingsService)
fixture = TestBed.createComponent(LogoComponent) fixture = TestBed.createComponent(LogoComponent)

View File

@ -1,6 +1,6 @@
<div class="row pt-3 pb-3 pb-md-2 align-items-center"> <div class="row pt-3 pb-3 pb-md-2 align-items-center">
<div class="col-md text-truncate"> <div class="col-md text-truncate">
<h3 class="text-truncate" style="line-height: 1.4"> <h3 class="text-truncate" style="line-height: 1.4" tourAnchor="tour.dashboard">
{{title}} {{title}}
@if (subTitle) { @if (subTitle) {
<span class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span> <span class="h6 mb-0 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span>

View File

@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { PermissionsDialogComponent } from './permissions-dialog.component' import { PermissionsDialogComponent } from './permissions-dialog.component'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { of } from 'rxjs' import { of } from 'rxjs'
@ -12,6 +12,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component' import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component' import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
import { SwitchComponent } from '../input/switch/switch.component' import { SwitchComponent } from '../input/switch/switch.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const set_permissions = { const set_permissions = {
owner: 10, owner: 10,
@ -43,6 +44,7 @@ describe('PermissionsDialogComponent', () => {
PermissionsUserComponent, PermissionsUserComponent,
PermissionsGroupComponent, PermissionsGroupComponent,
], ],
imports: [NgSelectModule, FormsModule, ReactiveFormsModule, NgbModule],
providers: [ providers: [
NgbActiveModal, NgbActiveModal,
{ {
@ -63,13 +65,8 @@ describe('PermissionsDialogComponent', () => {
}), }),
}, },
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
HttpClientTestingModule,
NgSelectModule,
FormsModule,
ReactiveFormsModule,
NgbModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
@ -15,6 +15,7 @@ import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.comp
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const currentUserID = 13 const currentUserID = 13
@ -30,6 +31,13 @@ describe('PermissionsFilterDropdownComponent', () => {
ClearableBadgeComponent, ClearableBadgeComponent,
IfPermissionsDirective, IfPermissionsDirective,
], ],
imports: [
NgSelectModule,
FormsModule,
ReactiveFormsModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [ providers: [
{ {
provide: UserService, provide: UserService,
@ -63,14 +71,8 @@ describe('PermissionsFilterDropdownComponent', () => {
}, },
}, },
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
HttpClientTestingModule,
NgSelectModule,
FormsModule,
ReactiveFormsModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@ -14,7 +14,8 @@ import { By } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const permissions = [ const permissions = [
'add_document', 'add_document',
@ -36,13 +37,15 @@ describe('PermissionsSelectComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PermissionsSelectComponent], declarations: [PermissionsSelectComponent],
providers: [],
imports: [ imports: [
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgbModule, NgbModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
HttpClientTestingModule, ],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
], ],
}).compileComponents() }).compileComponents()

View File

@ -5,10 +5,11 @@ import { By } from '@angular/platform-browser'
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { PdfViewerModule } from 'ng2-pdf-viewer' import { PdfViewerModule } from 'ng2-pdf-viewer'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const doc = { const doc = {
id: 10, id: 10,
@ -26,10 +27,10 @@ describe('PreviewPopupComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PreviewPopupComponent, SafeUrlPipe], declarations: [PreviewPopupComponent, SafeUrlPipe],
imports: [ imports: [NgxBootstrapIconsModule.pick(allIcons), PdfViewerModule],
HttpClientTestingModule, providers: [
NgxBootstrapIconsModule.pick(allIcons), provideHttpClient(withInterceptorsFromDi()),
PdfViewerModule, provideHttpClientTesting(),
], ],
}) })
settingsService = TestBed.inject(SettingsService) settingsService = TestBed.inject(SettingsService)

View File

@ -14,7 +14,7 @@ import {
NgbModalModule, NgbModalModule,
NgbPopoverModule, NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { HttpClientModule } from '@angular/common/http' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { TextComponent } from '../input/text/text.component' import { TextComponent } from '../input/text/text.component'
import { PasswordComponent } from '../input/password/password.component' import { PasswordComponent } from '../input/password/password.component'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
@ -55,9 +55,7 @@ describe('ProfileEditDialogComponent', () => {
PasswordComponent, PasswordComponent,
ConfirmButtonComponent, ConfirmButtonComponent,
], ],
providers: [NgbActiveModal],
imports: [ imports: [
HttpClientModule,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
NgbModalModule, NgbModalModule,
@ -65,6 +63,7 @@ describe('ProfileEditDialogComponent', () => {
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
NgbPopoverModule, NgbPopoverModule,
], ],
providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())],
}) })
profileService = TestBed.inject(ProfileService) profileService = TestBed.inject(ProfileService)
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)

View File

@ -1,6 +1,6 @@
import { import {
HttpTestingController, HttpTestingController,
HttpClientTestingModule, provideHttpClientTesting,
} from '@angular/common/http/testing' } from '@angular/common/http/testing'
import { import {
ComponentFixture, ComponentFixture,
@ -18,6 +18,7 @@ import { ShareLinksDropdownComponent } from './share-links-dropdown.component'
import { Clipboard } from '@angular/cdk/clipboard' import { Clipboard } from '@angular/cdk/clipboard'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('ShareLinksDropdownComponent', () => { describe('ShareLinksDropdownComponent', () => {
let component: ShareLinksDropdownComponent let component: ShareLinksDropdownComponent
@ -31,11 +32,14 @@ describe('ShareLinksDropdownComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ShareLinksDropdownComponent], declarations: [ShareLinksDropdownComponent],
imports: [ imports: [
HttpClientTestingModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
], ],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}) })
fixture = TestBed.createComponent(ShareLinksDropdownComponent) fixture = TestBed.createComponent(ShareLinksDropdownComponent)

View File

@ -28,7 +28,7 @@
<dt i18n>Media Storage</dt> <dt i18n>Media Storage</dt>
<dd> <dd>
<ngb-progressbar style="height: 4px;" class="mt-2 mb-1" type="primary" [max]="status.storage.total" [value]="status.storage.total - status.storage.available"></ngb-progressbar> <ngb-progressbar style="height: 4px;" class="mt-2 mb-1" type="primary" [max]="status.storage.total" [value]="status.storage.total - status.storage.available"></ngb-progressbar>
<span class="small">{{status.storage.available | filesize}} <ng-container i18n>available</ng-container> ({{status.storage.total | filesize}} <ng-container i18n>total</ng-container>)</span> <span class="small">{{status.storage.available | fileSize}} <ng-container i18n>available</ng-container> ({{status.storage.total | fileSize}} <ng-container i18n>total</ng-container>)</span>
</dd> </dd>
</dl> </dl>
</div> </div>

View File

@ -17,9 +17,10 @@ import {
InstallType, InstallType,
SystemStatus, SystemStatus,
} from 'src/app/data/system-status' } from 'src/app/data/system-status'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { NgxFilesizeModule } from 'ngx-filesize' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
const status: SystemStatus = { const status: SystemStatus = {
pngx_version: '2.4.3', pngx_version: '2.4.3',
@ -57,17 +58,19 @@ describe('SystemStatusDialogComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [SystemStatusDialogComponent], declarations: [SystemStatusDialogComponent, FileSizePipe],
providers: [NgbActiveModal],
imports: [ imports: [
NgbModalModule, NgbModalModule,
ClipboardModule, ClipboardModule,
HttpClientTestingModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
NgxFilesizeModule,
NgbPopoverModule, NgbPopoverModule,
NgbProgressbarModule, NgbProgressbarModule,
], ],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(SystemStatusDialogComponent) fixture = TestBed.createComponent(SystemStatusDialogComponent)

View File

@ -8,11 +8,12 @@ import {
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ToastsComponent } from './toasts.component' import { ToastsComponent } from './toasts.component'
import { ComponentFixture } from '@angular/core/testing' import { ComponentFixture } from '@angular/core/testing'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { of } from 'rxjs' import { of } from 'rxjs'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { Clipboard } from '@angular/cdk/clipboard' import { Clipboard } from '@angular/cdk/clipboard'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const toasts = [ const toasts = [
{ {
@ -46,11 +47,7 @@ describe('ToastsComponent', () => {
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ToastsComponent], declarations: [ToastsComponent],
imports: [ imports: [NgbModule, NgxBootstrapIconsModule.pick(allIcons)],
HttpClientTestingModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [ providers: [
{ {
provide: ToastService, provide: ToastService,
@ -58,6 +55,8 @@ describe('ToastsComponent', () => {
getToasts: () => of(toasts), getToasts: () => of(toasts),
}, },
}, },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
], ],
}).compileComponents() }).compileComponents()

View File

@ -4,7 +4,7 @@
<div class="row"> <div class="row">
<div class="col-12 col-lg-8 col-xl-9 mb-4"> <div class="col-12 col-lg-8 col-xl-9 mb-4">
<div class="row row-cols-1 g-4" tourAnchor="tour.dashboard" <div class="row row-cols-1 g-4"
cdkDropList cdkDropList
[cdkDropListDisabled]="settingsService.globalDropzoneActive" [cdkDropListDisabled]="settingsService.globalDropzoneActive"
(cdkDropListDropped)="onDrop($event)" (cdkDropListDropped)="onDrop($event)"

View File

@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap' import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { DashboardComponent } from './dashboard.component' import { DashboardComponent } from './dashboard.component'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { StatisticsWidgetComponent } from './widgets/statistics-widget/statistics-widget.component' import { StatisticsWidgetComponent } from './widgets/statistics-widget/statistics-widget.component'
import { PageHeaderComponent } from '../common/page-header/page-header.component' import { PageHeaderComponent } from '../common/page-header/page-header.component'
@ -22,6 +22,7 @@ import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop' import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
import { SavedView } from 'src/app/data/saved-view' import { SavedView } from 'src/app/data/saved-view'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const saved_views = [ const saved_views = [
{ {
@ -81,6 +82,13 @@ describe('DashboardComponent', () => {
SavedViewWidgetComponent, SavedViewWidgetComponent,
LogoComponent, LogoComponent,
], ],
imports: [
NgbAlertModule,
RouterTestingModule,
TourNgBootstrapModule,
DragDropModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [ providers: [
PermissionsGuard, PermissionsGuard,
{ {
@ -101,14 +109,8 @@ describe('DashboardComponent', () => {
dashboardViews: saved_views.filter((v) => v.show_on_dashboard), dashboardViews: saved_views.filter((v) => v.show_on_dashboard),
}, },
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
NgbAlertModule,
HttpClientTestingModule,
RouterTestingModule,
TourNgBootstrapModule,
DragDropModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@ -88,7 +88,7 @@
</tbody> </tbody>
</table> </table>
} @else if (documents.length && displayMode === DisplayMode.SMALL_CARDS) { } @else if (documents.length && displayMode === DisplayMode.SMALL_CARDS) {
<div class="row row-cols-paperless-cards my-n2"> <div content class="row row-cols-paperless-cards my-n2">
@for (d of documents; track d.id) { @for (d of documents; track d.id) {
<pngx-document-card-small <pngx-document-card-small
class="p-0" class="p-0"
@ -103,7 +103,7 @@
} }
</div> </div>
} @else if (documents.length && displayMode === DisplayMode.LARGE_CARDS) { } @else if (documents.length && displayMode === DisplayMode.LARGE_CARDS) {
<div class="row my-n2"> <div content class="row my-n2">
@for (d of documents; track d.id) { @for (d of documents; track d.id) {
<pngx-document-card-large <pngx-document-card-large
(dblClickDocument)="openDocumentDetail(d)" (dblClickDocument)="openDocumentDetail(d)"
@ -118,7 +118,7 @@
} }
</div> </div>
} @else { } @else {
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p> <p content i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
} }

View File

@ -1,5 +1,5 @@
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { import {
ComponentFixture, ComponentFixture,
TestBed, TestBed,
@ -41,6 +41,7 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service
import { CustomFieldDataType } from 'src/app/data/custom-field' import { CustomFieldDataType } from 'src/app/data/custom-field'
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component' import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
import { DisplayMode, DisplayField } from 'src/app/data/document' import { DisplayMode, DisplayField } from 'src/app/data/document'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const savedView: SavedView = { const savedView: SavedView = {
id: 1, id: 1,
@ -125,6 +126,12 @@ describe('SavedViewWidgetComponent', () => {
PreviewPopupComponent, PreviewPopupComponent,
CustomFieldDisplayComponent, CustomFieldDisplayComponent,
], ],
imports: [
NgbModule,
RouterTestingModule.withRoutes(routes),
DragDropModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [ providers: [
PermissionsGuard, PermissionsGuard,
DocumentService, DocumentService,
@ -163,13 +170,8 @@ describe('SavedViewWidgetComponent', () => {
}), }),
}, },
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
HttpClientTestingModule,
NgbModule,
RouterTestingModule.withRoutes(routes),
DragDropModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@ -2,8 +2,8 @@ import { TestBed } from '@angular/core/testing'
import { StatisticsWidgetComponent } from './statistics-widget.component' import { StatisticsWidgetComponent } from './statistics-widget.component'
import { ComponentFixture } from '@angular/core/testing' import { ComponentFixture } from '@angular/core/testing'
import { import {
HttpClientTestingModule,
HttpTestingController, HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing' } from '@angular/common/http/testing'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
@ -18,6 +18,7 @@ import {
} from 'src/app/services/consumer-status.service' } from 'src/app/services/consumer-status.service'
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { DragDropModule } from '@angular/cdk/drag-drop' import { DragDropModule } from '@angular/cdk/drag-drop'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
describe('StatisticsWidgetComponent', () => { describe('StatisticsWidgetComponent', () => {
let component: StatisticsWidgetComponent let component: StatisticsWidgetComponent
@ -33,13 +34,16 @@ describe('StatisticsWidgetComponent', () => {
WidgetFrameComponent, WidgetFrameComponent,
IfPermissionsDirective, IfPermissionsDirective,
], ],
providers: [PermissionsGuard],
imports: [ imports: [
HttpClientTestingModule,
NgbModule, NgbModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
DragDropModule, DragDropModule,
], ],
providers: [
PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(StatisticsWidgetComponent) fixture = TestBed.createComponent(StatisticsWidgetComponent)

View File

@ -39,7 +39,7 @@
<a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a> <a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
</p> </p>
} }
<div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded"> <div #hiddenAlerts="ngbCollapse" [ngbCollapse]="!alertsExpanded" (ngbCollapseChange)="alertsExpanded = $event">
@for (status of getStatusHidden(); track status) { @for (status of getStatusHidden(); track status) {
<div> <div>
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container> <ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>

View File

@ -1,4 +1,4 @@
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { import {
ComponentFixture, ComponentFixture,
TestBed, TestBed,
@ -27,6 +27,7 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
import { UploadFileWidgetComponent } from './upload-file-widget.component' import { UploadFileWidgetComponent } from './upload-file-widget.component'
import { DragDropModule } from '@angular/cdk/drag-drop' import { DragDropModule } from '@angular/cdk/drag-drop'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
const FAILED_STATUSES = [new FileStatus()] const FAILED_STATUSES = [new FileStatus()]
const WORKING_STATUSES = [new FileStatus(), new FileStatus()] const WORKING_STATUSES = [new FileStatus(), new FileStatus()]
@ -59,6 +60,13 @@ describe('UploadFileWidgetComponent', () => {
WidgetFrameComponent, WidgetFrameComponent,
IfPermissionsDirective, IfPermissionsDirective,
], ],
imports: [
NgbModule,
RouterTestingModule.withRoutes(routes),
NgbAlertModule,
DragDropModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [ providers: [
PermissionsGuard, PermissionsGuard,
{ {
@ -67,14 +75,8 @@ describe('UploadFileWidgetComponent', () => {
currentUserCan: () => true, currentUserCan: () => true,
}, },
}, },
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
HttpClientTestingModule,
NgbModule,
RouterTestingModule.withRoutes(routes),
NgbAlertModule,
DragDropModule,
NgxBootstrapIconsModule.pick(allIcons),
], ],
}).compileComponents() }).compileComponents()

View File

@ -186,6 +186,16 @@
[horizontal]="true" [horizontal]="true"
[error]="getCustomFieldError(i)"></pngx-input-document-link> [error]="getCustomFieldError(i)"></pngx-input-document-link>
} }
@case (CustomFieldDataType.Select) {
<pngx-input-select formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
[itemsArray]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
[allowNull]="true"
[horizontal]="true"
[removable]="userIsOwner"
(removed)="removeField(fieldInstance)"
[error]="getCustomFieldError(i)"></pngx-input-select>
}
} }
</div> </div>
} }

View File

@ -1,7 +1,7 @@
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { import {
HttpClientTestingModule,
HttpTestingController, HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing' } from '@angular/common/http/testing'
import { import {
ComponentFixture, ComponentFixture,
@ -83,6 +83,8 @@ import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-conf
import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component' import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
import { PdfViewerModule } from 'ng2-pdf-viewer' import { PdfViewerModule } from 'ng2-pdf-viewer'
import { DataType } from 'src/app/data/datatype' import { DataType } from 'src/app/data/datatype'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { TagService } from 'src/app/services/rest/tag.service'
const doc: Document = { const doc: Document = {
id: 3, id: 3,
@ -182,8 +184,51 @@ describe('DocumentDetailComponent', () => {
RotateConfirmDialogComponent, RotateConfirmDialogComponent,
DeletePagesConfirmDialogComponent, DeletePagesConfirmDialogComponent,
], ],
imports: [
RouterModule.forRoot(routes),
NgbModule,
NgSelectModule,
FormsModule,
ReactiveFormsModule,
NgbModalModule,
NgxBootstrapIconsModule.pick(allIcons),
PdfViewerModule,
],
providers: [ providers: [
DocumentTitlePipe, DocumentTitlePipe,
{
provide: TagService,
useValue: {
listAll: () =>
of({
count: 3,
all: [41, 42, 43],
results: [
{
id: 41,
name: 'Tag41',
is_inbox_tag: true,
color: '#ff0000',
text_color: '#000000',
},
{
id: 42,
name: 'Tag42',
is_inbox_tag: true,
color: '#ff0000',
text_color: '#000000',
},
{
id: 43,
name: 'Tag43',
is_inbox_tag: true,
color: '#ff0000',
text_color: '#000000',
},
],
}),
},
},
{ {
provide: CorrespondentService, provide: CorrespondentService,
useValue: { useValue: {
@ -257,17 +302,8 @@ describe('DocumentDetailComponent', () => {
PermissionsGuard, PermissionsGuard,
CustomDatePipe, CustomDatePipe,
DatePipe, DatePipe,
], provideHttpClient(withInterceptorsFromDi()),
imports: [ provideHttpClientTesting(),
RouterModule.forRoot(routes),
HttpClientTestingModule,
NgbModule,
NgSelectModule,
FormsModule,
ReactiveFormsModule,
NgbModalModule,
NgxBootstrapIconsModule.pick(allIcons),
PdfViewerModule,
], ],
}).compileComponents() }).compileComponents()
@ -989,10 +1025,10 @@ describe('DocumentDetailComponent', () => {
it('should get suggestions', () => { it('should get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions') const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
suggestionsSpy.mockReturnValue(of({ tags: [1, 2] })) suggestionsSpy.mockReturnValue(of({ tags: [42, 43] }))
initNormally() initNormally()
expect(suggestionsSpy).toHaveBeenCalled() expect(suggestionsSpy).toHaveBeenCalled()
expect(component.suggestions).toEqual({ tags: [1, 2] }) expect(component.suggestions).toEqual({ tags: [42, 43] })
}) })
it('should show error if needed for get suggestions', () => { it('should show error if needed for get suggestions', () => {

View File

@ -11,7 +11,7 @@
{{title}} {{title}}
</h6> </h6>
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expand"> <div #collapse="ngbCollapse" [ngbCollapse]="!expand" (ngbCollapseChange)="expand = $event">
<table class="table table-borderless"> <table class="table table-borderless">
<tbody> <tbody>
@for (m of metadata; track m) { @for (m of metadata; track m) {

View File

@ -48,7 +48,7 @@
@if (change.key === 'content') { @if (change.key === 'content') {
<code class="text-primary">{{ change.value[1]?.substring(0,100) }}...</code> <code class="text-primary">{{ change.value[1]?.substring(0,100) }}...</code>
} @else { } @else {
<code class="text-primary">{{ change.value[1] }}</code> <code class="text-primary">{{ getPrettyName(change.key, change.value[1]) | async }}</code>
} }
</li> </li>
} }

View File

@ -4,31 +4,48 @@ import { DocumentHistoryComponent } from './document-history.component'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { of } from 'rxjs' import { of } from 'rxjs'
import { AuditLogAction } from 'src/app/data/auditlog-entry' import { AuditLogAction } from 'src/app/data/auditlog-entry'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { DataType } from 'src/app/data/datatype'
describe('DocumentHistoryComponent', () => { describe('DocumentHistoryComponent', () => {
let component: DocumentHistoryComponent let component: DocumentHistoryComponent
let fixture: ComponentFixture<DocumentHistoryComponent> let fixture: ComponentFixture<DocumentHistoryComponent>
let documentService: DocumentService let documentService: DocumentService
let correspondentService: CorrespondentService
let documentTypeService: DocumentTypeService
let storagePathService: StoragePathService
let userService: UserService
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [DocumentHistoryComponent, CustomDatePipe], declarations: [DocumentHistoryComponent, CustomDatePipe],
providers: [DatePipe],
imports: [ imports: [
HttpClientTestingModule,
NgbCollapseModule, NgbCollapseModule,
NgxBootstrapIconsModule.pick(allIcons), NgxBootstrapIconsModule.pick(allIcons),
NgbTooltipModule, NgbTooltipModule,
], ],
providers: [
DatePipe,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(DocumentHistoryComponent) fixture = TestBed.createComponent(DocumentHistoryComponent)
documentService = TestBed.inject(DocumentService) documentService = TestBed.inject(DocumentService)
correspondentService = TestBed.inject(CorrespondentService)
documentTypeService = TestBed.inject(DocumentTypeService)
storagePathService = TestBed.inject(StoragePathService)
userService = TestBed.inject(UserService)
component = fixture.componentInstance component = fixture.componentInstance
}) })
@ -55,4 +72,91 @@ describe('DocumentHistoryComponent', () => {
fixture.detectChanges() fixture.detectChanges()
expect(getHistorySpy).toHaveBeenCalledWith(1) expect(getHistorySpy).toHaveBeenCalledWith(1)
}) })
it('getPrettyName should return the correspondent name', () => {
const correspondentId = '1'
const correspondentName = 'John Doe'
const getCachedSpy = jest
.spyOn(correspondentService, 'getCached')
.mockReturnValue(of({ name: correspondentName }))
component
.getPrettyName(DataType.Correspondent, correspondentId)
.subscribe((result) => {
expect(result).toBe(correspondentName)
})
expect(getCachedSpy).toHaveBeenCalledWith(parseInt(correspondentId))
// no correspondent found
getCachedSpy.mockReturnValue(of(null))
component
.getPrettyName(DataType.Correspondent, correspondentId)
.subscribe((result) => {
expect(result).toBe(correspondentId)
})
})
it('getPrettyName should return the document type name', () => {
const documentTypeId = '1'
const documentTypeName = 'Invoice'
const getCachedSpy = jest
.spyOn(documentTypeService, 'getCached')
.mockReturnValue(of({ name: documentTypeName }))
component
.getPrettyName(DataType.DocumentType, documentTypeId)
.subscribe((result) => {
expect(result).toBe(documentTypeName)
})
expect(getCachedSpy).toHaveBeenCalledWith(parseInt(documentTypeId))
// no document type found
getCachedSpy.mockReturnValue(of(null))
component
.getPrettyName(DataType.DocumentType, documentTypeId)
.subscribe((result) => {
expect(result).toBe(documentTypeId)
})
})
it('getPrettyName should return the storage path path', () => {
const storagePathId = '1'
const storagePath = '/path/to/storage'
const getCachedSpy = jest
.spyOn(storagePathService, 'getCached')
.mockReturnValue(of({ path: storagePath }))
component
.getPrettyName(DataType.StoragePath, storagePathId)
.subscribe((result) => {
expect(result).toBe(storagePath)
})
expect(getCachedSpy).toHaveBeenCalledWith(parseInt(storagePathId))
// no storage path found
getCachedSpy.mockReturnValue(of(null))
component
.getPrettyName(DataType.StoragePath, storagePathId)
.subscribe((result) => {
expect(result).toBe(storagePathId)
})
})
it('getPrettyName should return the owner username', () => {
const ownerId = '1'
const ownerUsername = 'user1'
const getCachedSpy = jest
.spyOn(userService, 'getCached')
.mockReturnValue(of({ username: ownerUsername }))
component.getPrettyName('owner', ownerId).subscribe((result) => {
expect(result).toBe(ownerUsername)
})
expect(getCachedSpy).toHaveBeenCalledWith(parseInt(ownerId))
// no user found
getCachedSpy.mockReturnValue(of(null))
component.getPrettyName('owner', ownerId).subscribe((result) => {
expect(result).toBe(ownerId)
})
})
it('getPrettyName should return the value as is for other types', () => {
const id = '123'
component.getPrettyName('other', id).subscribe((result) => {
expect(result).toBe(id)
})
})
}) })

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