mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Merge branch 'dev'
This commit is contained in:
		
							
								
								
									
										180
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										117
									
								
								.devcontainer/README.md
									
									
									
									
									
										Normal 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!
 | 
			
		||||
							
								
								
									
										16
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal 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"
 | 
			
		||||
  }
 | 
			
		||||
							
								
								
									
										84
									
								
								.devcontainer/docker-compose.devcontainer.sqlite-tika.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								.devcontainer/docker-compose.devcontainer.sqlite-tika.yml
									
									
									
									
									
										Normal 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:
 | 
			
		||||
							
								
								
									
										43
									
								
								.devcontainer/vscode/launch.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								.devcontainer/vscode/launch.json
									
									
									
									
									
										Normal 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"
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								.devcontainer/vscode/settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.devcontainer/vscode/settings.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
    "python.testing.pytestArgs": [
 | 
			
		||||
        "src"
 | 
			
		||||
    ],
 | 
			
		||||
    "python.testing.unittestEnabled": false,
 | 
			
		||||
    "python.testing.pytestEnabled": true,
 | 
			
		||||
    "files.watcherExclude": {
 | 
			
		||||
        "**/.venv/**": true,
 | 
			
		||||
        "**/pytest_cache/**": true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										136
									
								
								.devcontainer/vscode/tasks.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								.devcontainer/vscode/tasks.json
									
									
									
									
									
										Normal 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"
 | 
			
		||||
		}
 | 
			
		||||
	  }
 | 
			
		||||
	]
 | 
			
		||||
  }
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							@@ -49,10 +49,6 @@ updates:
 | 
			
		||||
      - "paperless-ngx/backend"
 | 
			
		||||
    ignore:
 | 
			
		||||
      - dependency-name: "uvicorn"
 | 
			
		||||
      - dependency-name: "djangorestframework"
 | 
			
		||||
        versions:
 | 
			
		||||
          - "3.15.0"
 | 
			
		||||
          - "3.15.1"
 | 
			
		||||
    groups:
 | 
			
		||||
      development:
 | 
			
		||||
        patterns:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -398,7 +398,7 @@ jobs:
 | 
			
		||||
          password: ${{ secrets.QUAY_ROBOT_TOKEN }}
 | 
			
		||||
      -
 | 
			
		||||
        name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v5
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          file: ./Dockerfile
 | 
			
		||||
@@ -406,6 +406,8 @@ jobs:
 | 
			
		||||
          push: ${{ github.event_name != 'pull_request' }}
 | 
			
		||||
          tags: ${{ steps.docker-meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.docker-meta.outputs.labels }}
 | 
			
		||||
          build-args: |
 | 
			
		||||
            PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }}
 | 
			
		||||
          # Get cache layers from this branch, then dev
 | 
			
		||||
          # This allows new branches to get at least some cache benefits, generally from dev
 | 
			
		||||
          cache-from: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -66,6 +66,8 @@ target/
 | 
			
		||||
.vscode
 | 
			
		||||
/src-ui/.vscode
 | 
			
		||||
/docs/.vscode
 | 
			
		||||
.vscode-server
 | 
			
		||||
*CommandMarker
 | 
			
		||||
 | 
			
		||||
# Other stuff that doesn't belong
 | 
			
		||||
.virtualenv
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ repos:
 | 
			
		||||
        exclude: "(^Pipfile\\.lock$)"
 | 
			
		||||
  # Python hooks
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    rev: 'v0.4.9'
 | 
			
		||||
    rev: 'v0.5.1'
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: ruff
 | 
			
		||||
      - id: ruff-format
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
3.9.18
 | 
			
		||||
3.9.19
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -13,6 +13,16 @@ WORKDIR /src/src-ui
 | 
			
		||||
RUN set -eux \
 | 
			
		||||
  && npm update npm -g \
 | 
			
		||||
  && 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 \
 | 
			
		||||
  && ./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 \
 | 
			
		||||
  && echo "Installing Python requirements" \
 | 
			
		||||
    && curl --fail --silent --show-error --location \
 | 
			
		||||
    --output psycopg_c-3.1.19-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 \
 | 
			
		||||
    --output psycopg_c-3.2.1-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 \
 | 
			
		||||
    --output psycopg_c-3.1.19-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 \
 | 
			
		||||
    --output psycopg_c-3.2.1-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 \
 | 
			
		||||
  && echo "Patching whitenoise for compression speedup" \
 | 
			
		||||
    && curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch \
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Pipfile
									
									
									
									
									
								
							@@ -7,7 +7,7 @@ name = "pypi"
 | 
			
		||||
dateparser = "~=1.2"
 | 
			
		||||
# WARNING: django does not use semver.
 | 
			
		||||
#          Only patch versions are guaranteed to not introduce breaking changes.
 | 
			
		||||
django = "~=4.2.13"
 | 
			
		||||
django = "~=4.2.14"
 | 
			
		||||
django-allauth = {extras = ["socialaccount"], version = "*"}
 | 
			
		||||
django-auditlog = "*"
 | 
			
		||||
django-celery-results = "*"
 | 
			
		||||
@@ -18,7 +18,7 @@ django-filter = "~=24.2"
 | 
			
		||||
django-guardian = "*"
 | 
			
		||||
django-multiselectfield = "*"
 | 
			
		||||
django-soft-delete = "*"
 | 
			
		||||
djangorestframework = "==3.14.0"
 | 
			
		||||
djangorestframework = "==3.15.2"
 | 
			
		||||
djangorestframework-guardian = "*"
 | 
			
		||||
drf-writable-nested = "*"
 | 
			
		||||
bleach = "*"
 | 
			
		||||
@@ -54,8 +54,8 @@ tqdm = "*"
 | 
			
		||||
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494
 | 
			
		||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
 | 
			
		||||
watchdog = "~=4.0"
 | 
			
		||||
whitenoise = "~=6.6"
 | 
			
		||||
whoosh="~=2.7"
 | 
			
		||||
whitenoise = "~=6.7"
 | 
			
		||||
whoosh = "~=2.7"
 | 
			
		||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
 | 
			
		||||
 | 
			
		||||
[dev-packages]
 | 
			
		||||
@@ -71,6 +71,7 @@ pytest-httpx = "*"
 | 
			
		||||
pytest-env = "*"
 | 
			
		||||
pytest-sugar = "*"
 | 
			
		||||
pytest-xdist = "*"
 | 
			
		||||
pytest-mock = "*"
 | 
			
		||||
pytest-rerunfailures = "*"
 | 
			
		||||
imagehash = "*"
 | 
			
		||||
daphne = "*"
 | 
			
		||||
@@ -93,5 +94,4 @@ types-tqdm = "*"
 | 
			
		||||
types-Markdown = "*"
 | 
			
		||||
types-Pygments = "*"
 | 
			
		||||
types-colorama = "*"
 | 
			
		||||
types-psycopg2 = "*"
 | 
			
		||||
types-setuptools = "*"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1333
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1333
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  gotenberg:
 | 
			
		||||
    image: docker.io/gotenberg/gotenberg:7.10
 | 
			
		||||
    image: docker.io/gotenberg/gotenberg:8.7
 | 
			
		||||
    hostname: gotenberg
 | 
			
		||||
    container_name: gotenberg
 | 
			
		||||
    network_mode: host
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@ services:
 | 
			
		||||
      PAPERLESS_TIKA_ENDPOINT: http://tika:9998
 | 
			
		||||
 | 
			
		||||
  gotenberg:
 | 
			
		||||
    image: docker.io/gotenberg/gotenberg:7.10
 | 
			
		||||
    image: docker.io/gotenberg/gotenberg:8.7
 | 
			
		||||
    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.
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ services:
 | 
			
		||||
      PAPERLESS_TIKA_ENDPOINT: http://tika:9998
 | 
			
		||||
 | 
			
		||||
  gotenberg:
 | 
			
		||||
    image: docker.io/gotenberg/gotenberg:7.10
 | 
			
		||||
    image: docker.io/gotenberg/gotenberg:8.7
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
 | 
			
		||||
    # The gotenberg chromium route is used to convert .eml files. We do not
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ services:
 | 
			
		||||
      PAPERLESS_TIKA_ENDPOINT: http://tika:9998
 | 
			
		||||
 | 
			
		||||
  gotenberg:
 | 
			
		||||
    image: docker.io/gotenberg/gotenberg:7.10
 | 
			
		||||
    image: docker.io/gotenberg/gotenberg:8.7
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
 | 
			
		||||
    # The gotenberg chromium route is used to convert .eml files. We do not
 | 
			
		||||
 
 | 
			
		||||
@@ -687,4 +687,5 @@ More details about configuration option for various providers can be found in th
 | 
			
		||||
 | 
			
		||||
### 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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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).
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
#### [`PAPERLESS_ACCOUNT_SESSION_REMEMBER=<bool>`](#PAPERLESS_ACCOUNT_SESSION_REMEMBER) {#PAPERLESS_ACCOUNT_SESSION_REMEMBER}
 | 
			
		||||
 
 | 
			
		||||
@@ -445,6 +445,7 @@ The following custom field types are supported:
 | 
			
		||||
- `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
 | 
			
		||||
- `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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,6 @@
 | 
			
		||||
            "scripts": [],
 | 
			
		||||
            "allowedCommonJsDependencies": [
 | 
			
		||||
              "ng2-pdf-viewer",
 | 
			
		||||
              "filesize",
 | 
			
		||||
              "file-saver"
 | 
			
		||||
            ],
 | 
			
		||||
            "vendorChunk": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -525,7 +525,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3768927257183755959" datatype="html">
 | 
			
		||||
@@ -544,7 +544,7 @@
 | 
			
		||||
        </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">19</context>
 | 
			
		||||
          <context context-type="linenumber">36</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -584,7 +584,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
 | 
			
		||||
@@ -726,7 +726,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
 | 
			
		||||
@@ -1080,7 +1080,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
 | 
			
		||||
@@ -1092,11 +1092,11 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
 | 
			
		||||
@@ -1390,11 +1390,11 @@
 | 
			
		||||
        </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 context-type="linenumber">23</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
 | 
			
		||||
@@ -1437,7 +1437,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <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="linenumber">76</context>
 | 
			
		||||
        </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 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>
 | 
			
		||||
@@ -1481,11 +1485,11 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
 | 
			
		||||
@@ -1624,7 +1628,7 @@
 | 
			
		||||
        </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">18</context>
 | 
			
		||||
          <context context-type="linenumber">35</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -1871,7 +1875,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
 | 
			
		||||
@@ -2153,7 +2157,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
 | 
			
		||||
@@ -2179,7 +2183,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
 | 
			
		||||
@@ -2214,42 +2218,74 @@
 | 
			
		||||
        <source>Document deleted</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7266264608936522311" datatype="html">
 | 
			
		||||
        <source>This operation will permanently delete the selected documents.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6804051092296228130" datatype="html">
 | 
			
		||||
        <source>This operation will permanently delete all documents in the trash.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6996183233986182894" datatype="html">
 | 
			
		||||
        <source>Document(s) deleted</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7534569062269274401" datatype="html">
 | 
			
		||||
        <source>Document restored</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="960063472770266304" datatype="html">
 | 
			
		||||
        <source>Document(s) restored</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <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="linenumber">13</context>
 | 
			
		||||
        </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 id="5944812089887969249" datatype="html">
 | 
			
		||||
        <source>Groups</source>
 | 
			
		||||
@@ -2358,11 +2398,11 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <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-group>
 | 
			
		||||
      </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">
 | 
			
		||||
        <source>Create new custom field</source>
 | 
			
		||||
        <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="linenumber">36</context>
 | 
			
		||||
          <context context-type="linenumber">80</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8751213029607178010" datatype="html">
 | 
			
		||||
        <source>Edit custom field</source>
 | 
			
		||||
        <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="linenumber">40</context>
 | 
			
		||||
          <context context-type="linenumber">84</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6672809941092516947" datatype="html">
 | 
			
		||||
@@ -3586,7 +3633,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7046259383943324039" datatype="html">
 | 
			
		||||
@@ -4640,7 +4687,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1880237574877817137" datatype="html">
 | 
			
		||||
@@ -4722,7 +4769,7 @@
 | 
			
		||||
        <source>Private</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/common/tag/tag.component.html</context>
 | 
			
		||||
@@ -4741,7 +4788,7 @@
 | 
			
		||||
        <source>No items found</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <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="linenumber">6</context>
 | 
			
		||||
        </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 id="7103181924469214926" datatype="html">
 | 
			
		||||
        <source>Please select an object</source>
 | 
			
		||||
@@ -5808,14 +5859,14 @@
 | 
			
		||||
        <source>Content</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="218403386307979629" datatype="html">
 | 
			
		||||
        <source>Metadata</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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 purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
 | 
			
		||||
@@ -5826,119 +5877,119 @@
 | 
			
		||||
        <source>Date modified</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6392918669949841614" datatype="html">
 | 
			
		||||
        <source>Date added</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="146828917013192897" datatype="html">
 | 
			
		||||
        <source>Media filename</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="4500855521601039868" datatype="html">
 | 
			
		||||
        <source>Original filename</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7985558498848210210" datatype="html">
 | 
			
		||||
        <source>Original MD5 checksum</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5888243105821763422" datatype="html">
 | 
			
		||||
        <source>Original file size</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2696647325713149563" datatype="html">
 | 
			
		||||
        <source>Original mime type</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="342875990758166588" datatype="html">
 | 
			
		||||
        <source>Archive MD5 checksum</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6033581412811562084" datatype="html">
 | 
			
		||||
        <source>Archive file size</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6992781481378431874" datatype="html">
 | 
			
		||||
        <source>Original document metadata</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2846565152091361585" datatype="html">
 | 
			
		||||
        <source>Archived document metadata</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1295614462098694869" datatype="html">
 | 
			
		||||
        <source>Preview</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <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="<span class="badge text-bg-secondary ms-1">"/><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 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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="186236568870281953" datatype="html">
 | 
			
		||||
        <source>History</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5129524307369213584" datatype="html">
 | 
			
		||||
        <source>Save & next</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="4910102545766233758" datatype="html">
 | 
			
		||||
        <source>Save & close</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8191371354890763172" datatype="html">
 | 
			
		||||
        <source>Enter Password</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="2218903673684131427" datatype="html">
 | 
			
		||||
@@ -6073,13 +6124,6 @@
 | 
			
		||||
          <context context-type="linenumber">716</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/document-detail/document-detail.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">799</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="619486176823357521" datatype="html">
 | 
			
		||||
        <source>Reprocess confirm</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
@@ -7348,28 +7392,35 @@
 | 
			
		||||
        <source>No mail accounts defined.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5364020217520256833" datatype="html">
 | 
			
		||||
        <source>Mail rules</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="1372022816709469401" datatype="html">
 | 
			
		||||
        <source>Add Rule</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6751234988479444294" datatype="html">
 | 
			
		||||
        <source>No mail rules defined.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3178554336792037159" datatype="html">
 | 
			
		||||
@@ -7805,56 +7856,56 @@
 | 
			
		||||
        <source>Boolean</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3973931101896534797" datatype="html">
 | 
			
		||||
        <source>Date</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="362956598863566327" datatype="html">
 | 
			
		||||
        <source>Integer</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6370642728789544052" datatype="html">
 | 
			
		||||
        <source>Number</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6430409302408843009" datatype="html">
 | 
			
		||||
        <source>Monetary</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6162693758764653365" datatype="html">
 | 
			
		||||
        <source>Text</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8308045076391224954" datatype="html">
 | 
			
		||||
        <source>Url</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3650316326183661476" datatype="html">
 | 
			
		||||
        <source>Document Link</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <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>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="3553216189604488439" datatype="html">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5457
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5457
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -11,17 +11,17 @@
 | 
			
		||||
  },
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@angular/cdk": "^17.3.10",
 | 
			
		||||
    "@angular/common": "~17.3.9",
 | 
			
		||||
    "@angular/compiler": "~17.3.9",
 | 
			
		||||
    "@angular/core": "~17.3.9",
 | 
			
		||||
    "@angular/forms": "~17.3.9",
 | 
			
		||||
    "@angular/localize": "~17.3.9",
 | 
			
		||||
    "@angular/platform-browser": "~17.3.9",
 | 
			
		||||
    "@angular/platform-browser-dynamic": "~17.3.9",
 | 
			
		||||
    "@angular/router": "~17.3.9",
 | 
			
		||||
    "@ng-bootstrap/ng-bootstrap": "^16.0.0",
 | 
			
		||||
    "@ng-select/ng-select": "^12.0.7",
 | 
			
		||||
    "@angular/cdk": "^18.0.6",
 | 
			
		||||
    "@angular/common": "~18.0.6",
 | 
			
		||||
    "@angular/compiler": "~18.0.6",
 | 
			
		||||
    "@angular/core": "~18.0.6",
 | 
			
		||||
    "@angular/forms": "~18.0.6",
 | 
			
		||||
    "@angular/localize": "~18.0.6",
 | 
			
		||||
    "@angular/platform-browser": "~18.0.6",
 | 
			
		||||
    "@angular/platform-browser-dynamic": "~18.0.6",
 | 
			
		||||
    "@angular/router": "~18.0.6",
 | 
			
		||||
    "@ng-bootstrap/ng-bootstrap": "^17.0.0",
 | 
			
		||||
    "@ng-select/ng-select": "^13.4.1",
 | 
			
		||||
    "@ngneat/dirty-check-forms": "^3.0.3",
 | 
			
		||||
    "@popperjs/core": "^2.11.8",
 | 
			
		||||
    "bootstrap": "^5.3.3",
 | 
			
		||||
@@ -30,35 +30,37 @@
 | 
			
		||||
    "ng2-pdf-viewer": "^10.2.2",
 | 
			
		||||
    "ngx-bootstrap-icons": "^1.9.3",
 | 
			
		||||
    "ngx-color": "^9.0.0",
 | 
			
		||||
    "ngx-cookie-service": "^17.1.0",
 | 
			
		||||
    "ngx-cookie-service": "^18.0.0",
 | 
			
		||||
    "ngx-file-drop": "^16.0.0",
 | 
			
		||||
    "ngx-filesize": "^3.0.3",
 | 
			
		||||
    "ngx-ui-tour-ng-bootstrap": "^14.0.3",
 | 
			
		||||
    "ngx-ui-tour-ng-bootstrap": "^15.0.0",
 | 
			
		||||
    "rxjs": "^7.8.1",
 | 
			
		||||
    "tslib": "^2.6.2",
 | 
			
		||||
    "uuid": "^9.0.1",
 | 
			
		||||
    "zone.js": "^0.14.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@angular-builders/jest": "17.0.3",
 | 
			
		||||
    "@angular-devkit/build-angular": "~17.3.7",
 | 
			
		||||
    "@angular-eslint/builder": "17.4.1",
 | 
			
		||||
    "@angular-eslint/eslint-plugin": "17.4.1",
 | 
			
		||||
    "@angular-eslint/eslint-plugin-template": "17.4.1",
 | 
			
		||||
    "@angular-eslint/schematics": "17.4.1",
 | 
			
		||||
    "@angular-eslint/template-parser": "17.4.1",
 | 
			
		||||
    "@angular/cli": "~17.3.7",
 | 
			
		||||
    "@angular/compiler-cli": "~17.3.2",
 | 
			
		||||
    "@angular-builders/jest": "^18.0.0",
 | 
			
		||||
    "@angular-devkit/build-angular": "^18.0.7",
 | 
			
		||||
    "@angular-devkit/core": "^18.0.7",
 | 
			
		||||
    "@angular-devkit/schematics": "^18.0.7",
 | 
			
		||||
    "@angular-eslint/builder": "18.1.0",
 | 
			
		||||
    "@angular-eslint/eslint-plugin": "18.1.0",
 | 
			
		||||
    "@angular-eslint/eslint-plugin-template": "18.1.0",
 | 
			
		||||
    "@angular-eslint/schematics": "18.1.0",
 | 
			
		||||
    "@angular-eslint/template-parser": "18.1.0",
 | 
			
		||||
    "@angular/cli": "~18.0.7",
 | 
			
		||||
    "@angular/compiler-cli": "~18.0.3",
 | 
			
		||||
    "@playwright/test": "^1.42.1",
 | 
			
		||||
    "@types/jest": "^29.5.12",
 | 
			
		||||
    "@types/node": "^20.12.2",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^7.4.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^7.4.0",
 | 
			
		||||
    "@typescript-eslint/utils": "^7.13.0",
 | 
			
		||||
    "concurrently": "^8.2.2",
 | 
			
		||||
    "eslint": "^8.57.0",
 | 
			
		||||
    "jest": "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",
 | 
			
		||||
    "patch-package": "^8.0.0",
 | 
			
		||||
    "ts-node": "~10.9.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
 | 
			
		||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
 | 
			
		||||
import {
 | 
			
		||||
  ComponentFixture,
 | 
			
		||||
  TestBed,
 | 
			
		||||
@@ -24,6 +24,7 @@ import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { HotKeyService } from './services/hot-key.service'
 | 
			
		||||
import { PermissionsGuard } from './guards/permissions.guard'
 | 
			
		||||
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('AppComponent', () => {
 | 
			
		||||
  let component: AppComponent
 | 
			
		||||
@@ -39,14 +40,18 @@ describe('AppComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [AppComponent, ToastsComponent, FileDropComponent],
 | 
			
		||||
      providers: [PermissionsGuard, DirtySavedViewGuard],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        TourNgBootstrapModule,
 | 
			
		||||
        RouterModule.forRoot(routes),
 | 
			
		||||
        NgxFileDropModule,
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        DirtySavedViewGuard,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    tourService = TestBed.inject(TourService)
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,11 @@ import {
 | 
			
		||||
  NgbDateParserFormatter,
 | 
			
		||||
  NgbModule,
 | 
			
		||||
} 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 { DocumentDetailComponent } from './components/document-detail/document-detail.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 { MonetaryComponent } from './components/common/input/monetary/monetary.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 { 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'
 | 
			
		||||
@@ -500,11 +503,11 @@ function initializeApp(settings: SettingsService) {
 | 
			
		||||
    DeletePagesConfirmDialogComponent,
 | 
			
		||||
    TrashComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  bootstrap: [AppComponent],
 | 
			
		||||
  imports: [
 | 
			
		||||
    BrowserModule,
 | 
			
		||||
    AppRoutingModule,
 | 
			
		||||
    NgbModule,
 | 
			
		||||
    HttpClientModule,
 | 
			
		||||
    FormsModule,
 | 
			
		||||
    ReactiveFormsModule,
 | 
			
		||||
    PdfViewerModule,
 | 
			
		||||
@@ -514,7 +517,6 @@ function initializeApp(settings: SettingsService) {
 | 
			
		||||
    TourNgBootstrapModule,
 | 
			
		||||
    DragDropModule,
 | 
			
		||||
    NgxBootstrapIconsModule.pick(icons),
 | 
			
		||||
    NgxFilesizeModule,
 | 
			
		||||
  ],
 | 
			
		||||
  providers: [
 | 
			
		||||
    {
 | 
			
		||||
@@ -543,7 +545,7 @@ function initializeApp(settings: SettingsService) {
 | 
			
		||||
    DirtyDocGuard,
 | 
			
		||||
    DirtySavedViewGuard,
 | 
			
		||||
    UsernamePipe,
 | 
			
		||||
    provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
  ],
 | 
			
		||||
  bootstrap: [AppComponent],
 | 
			
		||||
})
 | 
			
		||||
export class AppModule {}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import { ConfigService } from 'src/app/services/config.service'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
import { of, throwError } from 'rxjs'
 | 
			
		||||
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 { NgbModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
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 { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('ConfigComponent', () => {
 | 
			
		||||
  let component: ConfigComponent
 | 
			
		||||
@@ -38,7 +39,6 @@ describe('ConfigComponent', () => {
 | 
			
		||||
        PageHeaderComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        BrowserModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
@@ -46,6 +46,10 @@ describe('ConfigComponent', () => {
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    configService = TestBed.inject(ConfigService)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,11 @@ import { LogService } from 'src/app/services/rest/log.service'
 | 
			
		||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
 | 
			
		||||
import { LogsComponent } from './logs.component'
 | 
			
		||||
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 { BrowserModule, By } from '@angular/platform-browser'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const paperless_logs = [
 | 
			
		||||
  '[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
 | 
			
		||||
@@ -37,13 +38,15 @@ describe('LogsComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [LogsComponent, PageHeaderComponent],
 | 
			
		||||
      providers: [],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        BrowserModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    logService = TestBed.inject(LogService)
 | 
			
		||||
 
 | 
			
		||||
@@ -348,7 +348,7 @@
 | 
			
		||||
 | 
			
		||||
          @for (view of savedViews; track view) {
 | 
			
		||||
            <li class="list-group-item py-3">
 | 
			
		||||
            <div [formGroupName]="view.id" class="row">
 | 
			
		||||
            <div [formGroupName]="view.id">
 | 
			
		||||
              <div class="row">
 | 
			
		||||
                <div class="col">
 | 
			
		||||
                  <pngx-input-text title="Name" formControlName="name"></pngx-input-text>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
import { By } from '@angular/platform-browser'
 | 
			
		||||
@@ -50,6 +50,7 @@ import {
 | 
			
		||||
} from 'src/app/data/system-status'
 | 
			
		||||
import { DragDropSelectComponent } from '../../common/input/drag-drop-select/drag-drop-select.component'
 | 
			
		||||
import { DragDropModule } from '@angular/cdk/drag-drop'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const savedViews = [
 | 
			
		||||
  { id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
 | 
			
		||||
@@ -100,10 +101,8 @@ describe('SettingsComponent', () => {
 | 
			
		||||
        ConfirmButtonComponent,
 | 
			
		||||
        DragDropSelectComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [CustomDatePipe, DatePipe, PermissionsGuard],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
@@ -113,6 +112,13 @@ describe('SettingsComponent', () => {
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
        DragDropModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        CustomDatePipe,
 | 
			
		||||
        DatePipe,
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    router = TestBed.inject(Router)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { DatePipe } from '@angular/common'
 | 
			
		||||
import {
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  HttpClientTestingModule,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
import { By } from '@angular/platform-browser'
 | 
			
		||||
@@ -30,6 +30,7 @@ import { TasksComponent } from './tasks.component'
 | 
			
		||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { FormsModule } from '@angular/forms'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const tasks: PaperlessTask[] = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -125,6 +126,12 @@ describe('TasksComponent', () => {
 | 
			
		||||
        CustomDatePipe,
 | 
			
		||||
        ConfirmDialogComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        FormsModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: PermissionsService,
 | 
			
		||||
@@ -135,13 +142,8 @@ describe('TasksComponent', () => {
 | 
			
		||||
        CustomDatePipe,
 | 
			
		||||
        DatePipe,
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,11 @@ import {
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
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 { By } from '@angular/platform-browser'
 | 
			
		||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
 | 
			
		||||
const documentsInTrash = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -35,6 +37,7 @@ describe('TrashComponent', () => {
 | 
			
		||||
  let fixture: ComponentFixture<TrashComponent>
 | 
			
		||||
  let trashService: TrashService
 | 
			
		||||
  let modalService: NgbModal
 | 
			
		||||
  let toastService: ToastService
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
@@ -42,6 +45,7 @@ describe('TrashComponent', () => {
 | 
			
		||||
        TrashComponent,
 | 
			
		||||
        PageHeaderComponent,
 | 
			
		||||
        ConfirmDialogComponent,
 | 
			
		||||
        SafeHtmlPipe,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
@@ -56,6 +60,7 @@ describe('TrashComponent', () => {
 | 
			
		||||
    fixture = TestBed.createComponent(TrashComponent)
 | 
			
		||||
    trashService = TestBed.inject(TrashService)
 | 
			
		||||
    modalService = TestBed.inject(NgbModal)
 | 
			
		||||
    toastService = TestBed.inject(ToastService)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
  })
 | 
			
		||||
@@ -74,12 +79,20 @@ describe('TrashComponent', () => {
 | 
			
		||||
    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')
 | 
			
		||||
    let modal
 | 
			
		||||
    modalService.activeInstances.subscribe((instances) => {
 | 
			
		||||
      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'))
 | 
			
		||||
    component.delete(documentsInTrash[0])
 | 
			
		||||
    expect(modal).toBeDefined()
 | 
			
		||||
@@ -87,12 +100,20 @@ describe('TrashComponent', () => {
 | 
			
		||||
    expect(trashSpy).toHaveBeenCalled()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support empty trash', () => {
 | 
			
		||||
  it('should support empty trash, show error if needed', () => {
 | 
			
		||||
    const trashSpy = jest.spyOn(trashService, 'emptyTrash')
 | 
			
		||||
    let modal
 | 
			
		||||
    modalService.activeInstances.subscribe((instances) => {
 | 
			
		||||
      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'))
 | 
			
		||||
    component.emptyTrash()
 | 
			
		||||
    expect(modal).toBeDefined()
 | 
			
		||||
@@ -104,18 +125,34 @@ describe('TrashComponent', () => {
 | 
			
		||||
    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 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'))
 | 
			
		||||
    component.restore(documentsInTrash[0])
 | 
			
		||||
    expect(restoreSpy).toHaveBeenCalledWith([documentsInTrash[0].id])
 | 
			
		||||
    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 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'))
 | 
			
		||||
    component.restoreAll()
 | 
			
		||||
    expect(restoreSpy).toHaveBeenCalled()
 | 
			
		||||
 
 | 
			
		||||
@@ -59,10 +59,16 @@ export class TrashComponent implements OnDestroy {
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
        modal.componentInstance.buttonsEnabled = false
 | 
			
		||||
        this.trashService.emptyTrash([document.id]).subscribe(() => {
 | 
			
		||||
          this.toastService.showInfo($localize`Document deleted`)
 | 
			
		||||
          modal.close()
 | 
			
		||||
          this.reload()
 | 
			
		||||
        this.trashService.emptyTrash([document.id]).subscribe({
 | 
			
		||||
          next: () => {
 | 
			
		||||
            this.toastService.showInfo($localize`Document deleted`)
 | 
			
		||||
            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(() => {
 | 
			
		||||
        this.trashService
 | 
			
		||||
          .emptyTrash(documents ? Array.from(documents) : null)
 | 
			
		||||
          .subscribe(() => {
 | 
			
		||||
            this.toastService.showInfo($localize`Document(s) deleted`)
 | 
			
		||||
            this.allToggled = false
 | 
			
		||||
            modal.close()
 | 
			
		||||
            this.reload()
 | 
			
		||||
          .subscribe({
 | 
			
		||||
            next: () => {
 | 
			
		||||
              this.toastService.showInfo($localize`Document(s) deleted`)
 | 
			
		||||
              this.allToggled = false
 | 
			
		||||
              modal.close()
 | 
			
		||||
              this.reload()
 | 
			
		||||
            },
 | 
			
		||||
            error: (err) => {
 | 
			
		||||
              this.toastService.showError(
 | 
			
		||||
                $localize`Error deleting document(s)`,
 | 
			
		||||
                err
 | 
			
		||||
              )
 | 
			
		||||
              modal.close()
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  restore(document: Document) {
 | 
			
		||||
    this.trashService.restoreDocuments([document.id]).subscribe(() => {
 | 
			
		||||
      this.toastService.showInfo($localize`Document restored`)
 | 
			
		||||
      this.reload()
 | 
			
		||||
    this.trashService.restoreDocuments([document.id]).subscribe({
 | 
			
		||||
      next: () => {
 | 
			
		||||
        this.toastService.showInfo($localize`Document restored`)
 | 
			
		||||
        this.reload()
 | 
			
		||||
      },
 | 
			
		||||
      error: (err) => {
 | 
			
		||||
        this.toastService.showError($localize`Error restoring document`, err)
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  restoreAll(documents: Set<number> = null) {
 | 
			
		||||
    this.trashService
 | 
			
		||||
      .restoreDocuments(documents ? Array.from(documents) : null)
 | 
			
		||||
      .subscribe(() => {
 | 
			
		||||
        this.toastService.showInfo($localize`Document(s) restored`)
 | 
			
		||||
        this.allToggled = false
 | 
			
		||||
        this.reload()
 | 
			
		||||
      .subscribe({
 | 
			
		||||
        next: () => {
 | 
			
		||||
          this.toastService.showInfo($localize`Document(s) restored`)
 | 
			
		||||
          this.allToggled = false
 | 
			
		||||
          this.reload()
 | 
			
		||||
        },
 | 
			
		||||
        error: (err) => {
 | 
			
		||||
          this.toastService.showError(
 | 
			
		||||
            $localize`Error restoring document(s)`,
 | 
			
		||||
            err
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { DatePipe } from '@angular/common'
 | 
			
		||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
 | 
			
		||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
 | 
			
		||||
import {
 | 
			
		||||
  ComponentFixture,
 | 
			
		||||
  TestBed,
 | 
			
		||||
@@ -44,6 +44,7 @@ import { UsersAndGroupsComponent } from './users-groups.component'
 | 
			
		||||
import { User } from 'src/app/data/user'
 | 
			
		||||
import { Group } from 'src/app/data/group'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const users = [
 | 
			
		||||
  { id: 1, username: 'user1', is_superuser: false },
 | 
			
		||||
@@ -84,10 +85,8 @@ describe('UsersAndGroupsComponent', () => {
 | 
			
		||||
        PermissionsGroupComponent,
 | 
			
		||||
        IfOwnerDirective,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [CustomDatePipe, DatePipe, PermissionsGuard],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
@@ -95,6 +94,13 @@ describe('UsersAndGroupsComponent', () => {
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        CustomDatePipe,
 | 
			
		||||
        DatePipe,
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
    fixture = TestBed.createComponent(UsersAndGroupsComponent)
 | 
			
		||||
    settingsService = TestBed.inject(SettingsService)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
  HttpClientTestingModule,
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import { AppFrameComponent } from './app-frame.component'
 | 
			
		||||
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 { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { GlobalSearchComponent } from './global-search/global-search.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const saved_views = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -100,7 +101,6 @@ describe('AppFrameComponent', () => {
 | 
			
		||||
        GlobalSearchComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        BrowserModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        NgbModule,
 | 
			
		||||
@@ -150,6 +150,8 @@ describe('AppFrameComponent', () => {
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import {
 | 
			
		||||
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 { 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 {
 | 
			
		||||
  FILTER_FULLTEXT_QUERY,
 | 
			
		||||
@@ -40,6 +40,7 @@ import { DataType } from 'src/app/data/datatype'
 | 
			
		||||
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const searchResults = {
 | 
			
		||||
  total: 11,
 | 
			
		||||
@@ -139,13 +140,16 @@ describe('GlobalSearchComponent', () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [GlobalSearchComponent],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
        NgbDropdownModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    searchService = TestBed.inject(SearchService)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
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 { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
 | 
			
		||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
import { PdfViewerComponent } from 'ng2-pdf-viewer'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('DeletePagesConfirmDialogComponent', () => {
 | 
			
		||||
  let component: DeletePagesConfirmDialogComponent
 | 
			
		||||
@@ -14,13 +15,17 @@ describe('DeletePagesConfirmDialogComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [DeletePagesConfirmDialogComponent, PdfViewerComponent],
 | 
			
		||||
      providers: [NgbActiveModal, SafeHtmlPipe],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        SafeHtmlPipe,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
    fixture = TestBed.createComponent(DeletePagesConfirmDialogComponent)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
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 { of } from 'rxjs'
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service'
 | 
			
		||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('MergeConfirmDialogComponent', () => {
 | 
			
		||||
  let component: MergeConfirmDialogComponent
 | 
			
		||||
@@ -15,13 +16,16 @@ describe('MergeConfirmDialogComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [MergeConfirmDialogComponent],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(MergeConfirmDialogComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
 | 
			
		||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('RotateConfirmDialogComponent', () => {
 | 
			
		||||
  let component: RotateConfirmDialogComponent
 | 
			
		||||
@@ -12,10 +13,12 @@ describe('RotateConfirmDialogComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [RotateConfirmDialogComponent, SafeHtmlPipe],
 | 
			
		||||
      providers: [NgbActiveModal, SafeHtmlPipe],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      imports: [NgxBootstrapIconsModule.pick(allIcons)],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        SafeHtmlPipe,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-4">
 | 
			
		||||
            <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> 
 | 
			
		||||
                    <span i18n>Add Split</span>
 | 
			
		||||
                </button>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
 | 
			
		||||
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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service'
 | 
			
		||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
 | 
			
		||||
import { of } from 'rxjs'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('SplitConfirmDialogComponent', () => {
 | 
			
		||||
  let component: SplitConfirmDialogComponent
 | 
			
		||||
@@ -17,14 +18,17 @@ describe('SplitConfirmDialogComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [SplitConfirmDialogComponent],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        PdfViewerModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(SplitConfirmDialogComponent)
 | 
			
		||||
@@ -88,4 +92,16 @@ describe('SplitConfirmDialogComponent', () => {
 | 
			
		||||
    component.pdfPreviewLoaded({ numPages: 5 } as any)
 | 
			
		||||
    expect(component.totalPages).toEqual(5)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should correctly disable split button', () => {
 | 
			
		||||
    component.totalPages = 5
 | 
			
		||||
    component.page = 1
 | 
			
		||||
    expect(component.canSplit).toBeTruthy()
 | 
			
		||||
    component.page = 5
 | 
			
		||||
    expect(component.canSplit).toBeFalsy()
 | 
			
		||||
    component.page = 4
 | 
			
		||||
    expect(component.canSplit).toBeTruthy()
 | 
			
		||||
    component['pages'] = new Set([1, 2, 3, 4])
 | 
			
		||||
    expect(component.canSplit).toBeFalsy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,14 @@ export class SplitConfirmDialogComponent
 | 
			
		||||
  public totalPages: number
 | 
			
		||||
  public deleteOriginal: boolean = false
 | 
			
		||||
 | 
			
		||||
  public get canSplit(): boolean {
 | 
			
		||||
    return (
 | 
			
		||||
      this.page < this.totalPages &&
 | 
			
		||||
      this.pages.size < this.totalPages - 1 &&
 | 
			
		||||
      !this.pages.has(this.page)
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public get pdfSrc(): string {
 | 
			
		||||
    return this.documentService.getPreviewUrl(this.documentID)
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
                </div>
 | 
			
		||||
            }
 | 
			
		||||
            @case (CustomFieldDataType.Select) {
 | 
			
		||||
                <span [ngbTooltip]="nameTooltip">{{getSelectValue(field, value)}}</span>
 | 
			
		||||
            }
 | 
			
		||||
            @default {
 | 
			
		||||
              <span [ngbTooltip]="nameTooltip">{{value}}</span>
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,22 @@ import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service'
 | 
			
		||||
import { CustomFieldDisplayComponent } from './custom-field-display.component'
 | 
			
		||||
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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const customFields: CustomField[] = [
 | 
			
		||||
  { id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
 | 
			
		||||
  { id: 2, name: 'Field 2', data_type: CustomFieldDataType.Monetary },
 | 
			
		||||
  { 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 = {
 | 
			
		||||
  id: 1,
 | 
			
		||||
@@ -31,8 +40,12 @@ describe('CustomFieldDisplayComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [CustomFieldDisplayComponent],
 | 
			
		||||
      providers: [DocumentService],
 | 
			
		||||
      imports: [HttpClientTestingModule],
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        DocumentService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -98,4 +111,8 @@ describe('CustomFieldDisplayComponent', () => {
 | 
			
		||||
    expect(component.currency).toEqual('EUR')
 | 
			
		||||
    expect(component.value).toEqual(100)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should show select value', () => {
 | 
			
		||||
    expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -115,6 +115,10 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
 | 
			
		||||
    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 {
 | 
			
		||||
    this.unsubscribeNotifier.next(true)
 | 
			
		||||
    this.unsubscribeNotifier.complete()
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import {
 | 
			
		||||
  tick,
 | 
			
		||||
} from '@angular/core/testing'
 | 
			
		||||
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 { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
 | 
			
		||||
import { of } from 'rxjs'
 | 
			
		||||
@@ -22,6 +22,7 @@ import {
 | 
			
		||||
import { CustomFieldEditDialogComponent } from '../edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
 | 
			
		||||
import { By } from '@angular/platform-browser'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const fields: CustomField[] = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -47,7 +48,6 @@ describe('CustomFieldsDropdownComponent', () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [CustomFieldsDropdownComponent, SelectComponent],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
@@ -55,6 +55,10 @@ describe('CustomFieldsDropdownComponent', () => {
 | 
			
		||||
        NgbDropdownModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
    customFieldService = TestBed.inject(CustomFieldsService)
 | 
			
		||||
    toastService = TestBed.inject(ToastService)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import {
 | 
			
		||||
  DateSelection,
 | 
			
		||||
  RelativeDate,
 | 
			
		||||
} 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 { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
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 { DatePipe } from '@angular/common'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('DatesDropdownComponent', () => {
 | 
			
		||||
  let component: DatesDropdownComponent
 | 
			
		||||
@@ -31,14 +32,19 @@ describe('DatesDropdownComponent', () => {
 | 
			
		||||
        ClearableBadgeComponent,
 | 
			
		||||
        CustomDatePipe,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [SettingsService, CustomDatePipe, DatePipe],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        SettingsService,
 | 
			
		||||
        CustomDatePipe,
 | 
			
		||||
        DatePipe,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    settingsService = TestBed.inject(SettingsService)
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
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 { EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('CorrespondentEditDialogComponent', () => {
 | 
			
		||||
  let component: CorrespondentEditDialogComponent
 | 
			
		||||
@@ -27,13 +28,11 @@ describe('CorrespondentEditDialogComponent', () => {
 | 
			
		||||
        TextComponent,
 | 
			
		||||
        PermissionsFormComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,23 @@
 | 
			
		||||
    @if (typeFieldDisabled) {
 | 
			
		||||
      <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> <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 class="modal-footer">
 | 
			
		||||
    <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
 | 
			
		||||
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 { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
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 { TextComponent } from '../../input/text/text.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', () => {
 | 
			
		||||
  let component: CustomFieldEditDialogComponent
 | 
			
		||||
@@ -28,13 +32,17 @@ describe('CustomFieldEditDialogComponent', () => {
 | 
			
		||||
        TextComponent,
 | 
			
		||||
        SafeHtmlPipe,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
@@ -64,4 +72,55 @@ describe('CustomFieldEditDialogComponent', () => {
 | 
			
		||||
    component.ngOnInit()
 | 
			
		||||
    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)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,24 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core'
 | 
			
		||||
import { FormGroup, FormControl } from '@angular/forms'
 | 
			
		||||
import {
 | 
			
		||||
  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 { 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 { UserService } from 'src/app/services/rest/user.service'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
import { Subject, takeUntil } from 'rxjs'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'pngx-custom-field-edit-dialog',
 | 
			
		||||
@@ -14,8 +27,20 @@ import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
})
 | 
			
		||||
export class CustomFieldEditDialogComponent
 | 
			
		||||
  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(
 | 
			
		||||
    service: CustomFieldsService,
 | 
			
		||||
    activeModal: NgbActiveModal,
 | 
			
		||||
@@ -30,6 +55,25 @@ export class CustomFieldEditDialogComponent
 | 
			
		||||
    if (this.typeFieldDisabled) {
 | 
			
		||||
      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() {
 | 
			
		||||
@@ -44,6 +88,9 @@ export class CustomFieldEditDialogComponent
 | 
			
		||||
    return new FormGroup({
 | 
			
		||||
      name: 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 {
 | 
			
		||||
    return this.dialogMode === EditDialogMode.EDIT
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public addSelectOption() {
 | 
			
		||||
    this.selectOptions.push(new FormControl(''))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public removeSelectOption(index: number) {
 | 
			
		||||
    this.selectOptions.removeAt(index)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
  <div class="modal-body">
 | 
			
		||||
 | 
			
		||||
    <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>
 | 
			
		||||
      @if (patternRequired) {
 | 
			
		||||
        <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
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 { EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('DocumentTypeEditDialogComponent', () => {
 | 
			
		||||
  let component: DocumentTypeEditDialogComponent
 | 
			
		||||
@@ -27,13 +28,11 @@ describe('DocumentTypeEditDialogComponent', () => {
 | 
			
		||||
        TextComponent,
 | 
			
		||||
        PermissionsFormComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  HttpClientTestingModule,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import { Component } from '@angular/core'
 | 
			
		||||
import {
 | 
			
		||||
@@ -30,6 +30,7 @@ import { UserService } from 'src/app/services/rest/user.service'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  template: `
 | 
			
		||||
@@ -96,6 +97,7 @@ describe('EditDialogComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [TestComponent],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        {
 | 
			
		||||
@@ -114,8 +116,9 @@ describe('EditDialogComponent', () => {
 | 
			
		||||
        },
 | 
			
		||||
        SettingsService,
 | 
			
		||||
        TagService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    tagService = TestBed.inject(TagService)
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
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 { EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
import { GroupEditDialogComponent } from './group-edit-dialog.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('GroupEditDialogComponent', () => {
 | 
			
		||||
  let component: GroupEditDialogComponent
 | 
			
		||||
@@ -29,13 +30,11 @@ describe('GroupEditDialogComponent', () => {
 | 
			
		||||
        PermissionsFormComponent,
 | 
			
		||||
        PermissionsSelectComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  HttpClientTestingModule,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import {
 | 
			
		||||
  ComponentFixture,
 | 
			
		||||
@@ -23,6 +23,7 @@ import { SelectComponent } from '../../input/select/select.component'
 | 
			
		||||
import { TextComponent } from '../../input/text/text.component'
 | 
			
		||||
import { EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
import { MailAccountEditDialogComponent } from './mail-account-edit-dialog.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('MailAccountEditDialogComponent', () => {
 | 
			
		||||
  let component: MailAccountEditDialogComponent
 | 
			
		||||
@@ -42,13 +43,11 @@ describe('MailAccountEditDialogComponent', () => {
 | 
			
		||||
        PermissionsFormComponent,
 | 
			
		||||
        PasswordComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
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 { EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
import { MailRuleEditDialogComponent } from './mail-rule-edit-dialog.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('MailRuleEditDialogComponent', () => {
 | 
			
		||||
  let component: MailRuleEditDialogComponent
 | 
			
		||||
@@ -43,6 +44,7 @@ describe('MailRuleEditDialogComponent', () => {
 | 
			
		||||
        SafeHtmlPipe,
 | 
			
		||||
        CheckComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        {
 | 
			
		||||
@@ -63,13 +65,8 @@ describe('MailRuleEditDialogComponent', () => {
 | 
			
		||||
            listAll: () => of([]),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
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 { EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('StoragePathEditDialogComponent', () => {
 | 
			
		||||
  let component: StoragePathEditDialogComponent
 | 
			
		||||
@@ -29,13 +30,11 @@ describe('StoragePathEditDialogComponent', () => {
 | 
			
		||||
        PermissionsFormComponent,
 | 
			
		||||
        SafeHtmlPipe,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
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 { TagEditDialogComponent } from './tag-edit-dialog.component'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('TagEditDialogComponent', () => {
 | 
			
		||||
  let component: TagEditDialogComponent
 | 
			
		||||
@@ -32,15 +33,19 @@ describe('TagEditDialogComponent', () => {
 | 
			
		||||
        ColorComponent,
 | 
			
		||||
        CheckComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal, SettingsService],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        SettingsService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(TagEditDialogComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
  FormsModule,
 | 
			
		||||
@@ -19,6 +19,7 @@ import { TextComponent } from '../../input/text/text.component'
 | 
			
		||||
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
 | 
			
		||||
import { EditDialogMode } from '../edit-dialog.component'
 | 
			
		||||
import { UserEditDialogComponent } from './user-edit-dialog.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('UserEditDialogComponent', () => {
 | 
			
		||||
  let component: UserEditDialogComponent
 | 
			
		||||
@@ -37,6 +38,7 @@ describe('UserEditDialogComponent', () => {
 | 
			
		||||
        PermissionsFormComponent,
 | 
			
		||||
        PermissionsSelectComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        {
 | 
			
		||||
@@ -54,13 +56,8 @@ describe('UserEditDialogComponent', () => {
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        SettingsService,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
@@ -39,6 +39,7 @@ import {
 | 
			
		||||
} from 'src/app/data/workflow-action'
 | 
			
		||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
 | 
			
		||||
import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const workflow: Workflow = {
 | 
			
		||||
  name: 'Workflow 1',
 | 
			
		||||
@@ -88,6 +89,7 @@ describe('WorkflowEditDialogComponent', () => {
 | 
			
		||||
        SafeHtmlPipe,
 | 
			
		||||
        ConfirmButtonComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        {
 | 
			
		||||
@@ -150,13 +152,8 @@ describe('WorkflowEditDialogComponent', () => {
 | 
			
		||||
              }),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import {
 | 
			
		||||
  ReactiveFormsModule,
 | 
			
		||||
} from '@angular/forms'
 | 
			
		||||
import { DateComponent } from './date.component'
 | 
			
		||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
 | 
			
		||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
 | 
			
		||||
import {
 | 
			
		||||
  NgbDateParserFormatter,
 | 
			
		||||
  NgbDatepickerModule,
 | 
			
		||||
@@ -13,6 +13,7 @@ import {
 | 
			
		||||
import { RouterTestingModule } from '@angular/router/testing'
 | 
			
		||||
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('DateComponent', () => {
 | 
			
		||||
  let component: DateComponent
 | 
			
		||||
@@ -22,19 +23,20 @@ describe('DateComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [DateComponent],
 | 
			
		||||
      imports: [
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgbDatepickerModule,
 | 
			
		||||
        RouterTestingModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: NgbDateParserFormatter,
 | 
			
		||||
          useClass: LocalizedDateParserFormatter,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbDatepickerModule,
 | 
			
		||||
        RouterTestingModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
  FormsModule,
 | 
			
		||||
@@ -10,6 +10,7 @@ import { of, throwError } from 'rxjs'
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service'
 | 
			
		||||
import { DocumentLinkComponent } from './document-link.component'
 | 
			
		||||
import { FILTER_TITLE } from 'src/app/data/filter-rule-type'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const documents = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -38,11 +39,10 @@ describe('DocumentLinkComponent', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [DocumentLinkComponent],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
      imports: [NgSelectModule, FormsModule, ReactiveFormsModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
    documentService = TestBed.inject(DocumentService)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,10 @@
 | 
			
		||||
        }
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="d-flex flex-row 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="mt-2 align-items-center bg-light p-2">
 | 
			
		||||
    <div class="d-flex flex-wrap flex-row gap-2 w-100"
 | 
			
		||||
        cdkDropList #unselectedList="cdkDropList"
 | 
			
		||||
        cdkDropListOrientation="horizontal"
 | 
			
		||||
        cdkDropListOrientation="mixed"
 | 
			
		||||
        (cdkDropListDropped)="drop($event)"
 | 
			
		||||
        [cdkDropListConnectedTo]="[selectedList]">
 | 
			
		||||
        @for (item of unselectedItems; track item.id) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,3 @@
 | 
			
		||||
.badge {
 | 
			
		||||
    cursor: move;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.d-flex {
 | 
			
		||||
    overflow-x: scroll;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
 | 
			
		||||
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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('FileComponent', () => {
 | 
			
		||||
  let component: FileComponent
 | 
			
		||||
@@ -11,7 +12,11 @@ describe('FileComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [FileComponent],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(FileComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,10 @@ import {
 | 
			
		||||
  NG_VALUE_ACCESSOR,
 | 
			
		||||
  ReactiveFormsModule,
 | 
			
		||||
} from '@angular/forms'
 | 
			
		||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
 | 
			
		||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
 | 
			
		||||
import { CurrencyPipe } from '@angular/common'
 | 
			
		||||
import { MonetaryComponent } from './monetary.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('MonetaryComponent', () => {
 | 
			
		||||
  let component: MonetaryComponent
 | 
			
		||||
@@ -15,8 +16,12 @@ describe('MonetaryComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [MonetaryComponent],
 | 
			
		||||
      providers: [CurrencyPipe],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        CurrencyPipe,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(MonetaryComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,9 @@ import {
 | 
			
		||||
} from '@angular/forms'
 | 
			
		||||
import { NumberComponent } from './number.component'
 | 
			
		||||
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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('NumberComponent', () => {
 | 
			
		||||
  let component: NumberComponent
 | 
			
		||||
@@ -18,8 +19,12 @@ describe('NumberComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [NumberComponent],
 | 
			
		||||
      providers: [DocumentService],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        DocumentService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(NumberComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,9 @@ import { SelectComponent } from '../../select/select.component'
 | 
			
		||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { PermissionsGroupComponent } from '../permissions-group/permissions-group.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 { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('PermissionsFormComponent', () => {
 | 
			
		||||
  let component: PermissionsFormComponent
 | 
			
		||||
@@ -24,14 +25,16 @@ describe('PermissionsFormComponent', () => {
 | 
			
		||||
        PermissionsGroupComponent,
 | 
			
		||||
        PermissionsUserComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [],
 | 
			
		||||
      imports: [
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgbAccordionModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(PermissionsFormComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,11 @@ import {
 | 
			
		||||
  ReactiveFormsModule,
 | 
			
		||||
} from '@angular/forms'
 | 
			
		||||
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 { GroupService } from 'src/app/services/rest/group.service'
 | 
			
		||||
import { of } from 'rxjs'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('PermissionsGroupComponent', () => {
 | 
			
		||||
  let component: PermissionsGroupComponent
 | 
			
		||||
@@ -19,12 +20,11 @@ describe('PermissionsGroupComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [PermissionsGroupComponent],
 | 
			
		||||
      providers: [GroupService],
 | 
			
		||||
      imports: [
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        GroupService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,12 @@ import {
 | 
			
		||||
  ReactiveFormsModule,
 | 
			
		||||
} from '@angular/forms'
 | 
			
		||||
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 { GroupService } from 'src/app/services/rest/group.service'
 | 
			
		||||
import { of } from 'rxjs'
 | 
			
		||||
import { UserService } from 'src/app/services/rest/user.service'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('PermissionsUserComponent', () => {
 | 
			
		||||
  let component: PermissionsUserComponent
 | 
			
		||||
@@ -20,12 +21,11 @@ describe('PermissionsUserComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [PermissionsUserComponent],
 | 
			
		||||
      providers: [UserService],
 | 
			
		||||
      imports: [
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
      imports: [FormsModule, ReactiveFormsModule, NgSelectModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        UserService,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -132,4 +132,12 @@ describe('SelectComponent', () => {
 | 
			
		||||
    const expectedTitle = `Filter documents with this ${component.title}`
 | 
			
		||||
    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' },
 | 
			
		||||
    ])
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,11 @@ export class SelectComponent extends AbstractInputComponent<number> {
 | 
			
		||||
    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 {
 | 
			
		||||
    if (newValue && this._items) {
 | 
			
		||||
      this.checkForPrivateItems(newValue)
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@
 | 
			
		||||
      @if (getSuggestions().length > 0) {
 | 
			
		||||
        <small class="position-absolute top-100">
 | 
			
		||||
          <span i18n>Suggestions:</span> 
 | 
			
		||||
          @for (tag of getSuggestions(); track tag) {
 | 
			
		||||
          @for (tag of getSuggestions(); track tag.id) {
 | 
			
		||||
            <a (click)="addTag(tag.id)" [routerLink]="[]">{{tag?.name}}</a> 
 | 
			
		||||
          }
 | 
			
		||||
        </small>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import {
 | 
			
		||||
} from 'src/app/data/matching-model'
 | 
			
		||||
import { NgSelectModule } from '@ng-select/ng-select'
 | 
			
		||||
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 { TagService } from 'src/app/services/rest/tag.service'
 | 
			
		||||
import {
 | 
			
		||||
@@ -31,6 +31,7 @@ import { PermissionsFormComponent } from '../permissions/permissions-form/permis
 | 
			
		||||
import { SelectComponent } from '../select/select.component'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const tags: Tag[] = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -74,6 +75,16 @@ describe('TagsComponent', () => {
 | 
			
		||||
        ColorComponent,
 | 
			
		||||
        CheckComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        RouterTestingModule,
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
        NgbAccordionModule,
 | 
			
		||||
        NgbPopoverModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: TagService,
 | 
			
		||||
@@ -90,17 +101,8 @@ describe('TagsComponent', () => {
 | 
			
		||||
              }),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        RouterTestingModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
        NgbAccordionModule,
 | 
			
		||||
        NgbPopoverModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
 | 
			
		||||
import { LogoComponent } from './logo.component'
 | 
			
		||||
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 { SETTINGS_KEYS } from 'src/app/data/ui-settings'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('LogoComponent', () => {
 | 
			
		||||
  let component: LogoComponent
 | 
			
		||||
@@ -14,7 +15,11 @@ describe('LogoComponent', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [LogoComponent],
 | 
			
		||||
      imports: [HttpClientTestingModule],
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
    settingsService = TestBed.inject(SettingsService)
 | 
			
		||||
    fixture = TestBed.createComponent(LogoComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<div class="row pt-3 pb-3 pb-md-2 align-items-center">
 | 
			
		||||
  <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}}
 | 
			
		||||
      @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>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
import { PermissionsDialogComponent } from './permissions-dialog.component'
 | 
			
		||||
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 { UserService } from 'src/app/services/rest/user.service'
 | 
			
		||||
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 { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
 | 
			
		||||
import { SwitchComponent } from '../input/switch/switch.component'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const set_permissions = {
 | 
			
		||||
  owner: 10,
 | 
			
		||||
@@ -43,6 +44,7 @@ describe('PermissionsDialogComponent', () => {
 | 
			
		||||
        PermissionsUserComponent,
 | 
			
		||||
        PermissionsGroupComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [NgSelectModule, FormsModule, ReactiveFormsModule, NgbModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        {
 | 
			
		||||
@@ -63,13 +65,8 @@ describe('PermissionsDialogComponent', () => {
 | 
			
		||||
              }),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
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 { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const currentUserID = 13
 | 
			
		||||
 | 
			
		||||
@@ -30,6 +31,13 @@ describe('PermissionsFilterDropdownComponent', () => {
 | 
			
		||||
        ClearableBadgeComponent,
 | 
			
		||||
        IfPermissionsDirective,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: UserService,
 | 
			
		||||
@@ -63,14 +71,8 @@ describe('PermissionsFilterDropdownComponent', () => {
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,8 @@ import { By } from '@angular/platform-browser'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
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 = [
 | 
			
		||||
  'add_document',
 | 
			
		||||
@@ -36,13 +37,15 @@ describe('PermissionsSelectComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [PermissionsSelectComponent],
 | 
			
		||||
      providers: [],
 | 
			
		||||
      imports: [
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,11 @@ import { By } from '@angular/platform-browser'
 | 
			
		||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
 | 
			
		||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
			
		||||
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 { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const doc = {
 | 
			
		||||
  id: 10,
 | 
			
		||||
@@ -26,10 +27,10 @@ describe('PreviewPopupComponent', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [PreviewPopupComponent, SafeUrlPipe],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        PdfViewerModule,
 | 
			
		||||
      imports: [NgxBootstrapIconsModule.pick(allIcons), PdfViewerModule],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
    settingsService = TestBed.inject(SettingsService)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ import {
 | 
			
		||||
  NgbModalModule,
 | 
			
		||||
  NgbPopoverModule,
 | 
			
		||||
} 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 { PasswordComponent } from '../input/password/password.component'
 | 
			
		||||
import { of, throwError } from 'rxjs'
 | 
			
		||||
@@ -55,9 +55,7 @@ describe('ProfileEditDialogComponent', () => {
 | 
			
		||||
        PasswordComponent,
 | 
			
		||||
        ConfirmButtonComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
@@ -65,6 +63,7 @@ describe('ProfileEditDialogComponent', () => {
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        NgbPopoverModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())],
 | 
			
		||||
    })
 | 
			
		||||
    profileService = TestBed.inject(ProfileService)
 | 
			
		||||
    toastService = TestBed.inject(ToastService)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import {
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  HttpClientTestingModule,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import {
 | 
			
		||||
  ComponentFixture,
 | 
			
		||||
@@ -18,6 +18,7 @@ import { ShareLinksDropdownComponent } from './share-links-dropdown.component'
 | 
			
		||||
import { Clipboard } from '@angular/cdk/clipboard'
 | 
			
		||||
import { By } from '@angular/platform-browser'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('ShareLinksDropdownComponent', () => {
 | 
			
		||||
  let component: ShareLinksDropdownComponent
 | 
			
		||||
@@ -31,11 +32,14 @@ describe('ShareLinksDropdownComponent', () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ShareLinksDropdownComponent],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(ShareLinksDropdownComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@
 | 
			
		||||
              <dt i18n>Media Storage</dt>
 | 
			
		||||
              <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>
 | 
			
		||||
                <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>
 | 
			
		||||
            </dl>
 | 
			
		||||
          </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,10 @@ import {
 | 
			
		||||
  InstallType,
 | 
			
		||||
  SystemStatus,
 | 
			
		||||
} 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 { NgxFilesizeModule } from 'ngx-filesize'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
 | 
			
		||||
 | 
			
		||||
const status: SystemStatus = {
 | 
			
		||||
  pngx_version: '2.4.3',
 | 
			
		||||
@@ -57,17 +58,19 @@ describe('SystemStatusDialogComponent', () => {
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [SystemStatusDialogComponent],
 | 
			
		||||
      providers: [NgbActiveModal],
 | 
			
		||||
      declarations: [SystemStatusDialogComponent, FileSizePipe],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
        ClipboardModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        NgxFilesizeModule,
 | 
			
		||||
        NgbPopoverModule,
 | 
			
		||||
        NgbProgressbarModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        NgbActiveModal,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(SystemStatusDialogComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,11 +8,12 @@ import {
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
import { ToastsComponent } from './toasts.component'
 | 
			
		||||
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 { NgbModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { Clipboard } from '@angular/cdk/clipboard'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const toasts = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -46,11 +47,7 @@ describe('ToastsComponent', () => {
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ToastsComponent],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [NgbModule, NgxBootstrapIconsModule.pick(allIcons)],
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: ToastService,
 | 
			
		||||
@@ -58,6 +55,8 @@ describe('ToastsComponent', () => {
 | 
			
		||||
            getToasts: () => of(toasts),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <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
 | 
			
		||||
      [cdkDropListDisabled]="settingsService.globalDropzoneActive"
 | 
			
		||||
      (cdkDropListDropped)="onDrop($event)"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
 | 
			
		||||
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 { StatisticsWidgetComponent } from './widgets/statistics-widget/statistics-widget.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 { SavedView } from 'src/app/data/saved-view'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const saved_views = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -81,6 +82,13 @@ describe('DashboardComponent', () => {
 | 
			
		||||
        SavedViewWidgetComponent,
 | 
			
		||||
        LogoComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbAlertModule,
 | 
			
		||||
        RouterTestingModule,
 | 
			
		||||
        TourNgBootstrapModule,
 | 
			
		||||
        DragDropModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        {
 | 
			
		||||
@@ -101,14 +109,8 @@ describe('DashboardComponent', () => {
 | 
			
		||||
            dashboardViews: saved_views.filter((v) => v.show_on_dashboard),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbAlertModule,
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        RouterTestingModule,
 | 
			
		||||
        TourNgBootstrapModule,
 | 
			
		||||
        DragDropModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -88,7 +88,7 @@
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  } @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) {
 | 
			
		||||
        <pngx-document-card-small
 | 
			
		||||
          class="p-0"
 | 
			
		||||
@@ -103,7 +103,7 @@
 | 
			
		||||
      }
 | 
			
		||||
    </div>
 | 
			
		||||
  } @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) {
 | 
			
		||||
        <pngx-document-card-large
 | 
			
		||||
          (dblClickDocument)="openDocumentDetail(d)"
 | 
			
		||||
@@ -118,7 +118,7 @@
 | 
			
		||||
      }
 | 
			
		||||
    </div>
 | 
			
		||||
  } @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>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { DatePipe } from '@angular/common'
 | 
			
		||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
 | 
			
		||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
 | 
			
		||||
import {
 | 
			
		||||
  ComponentFixture,
 | 
			
		||||
  TestBed,
 | 
			
		||||
@@ -41,6 +41,7 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service
 | 
			
		||||
import { CustomFieldDataType } from 'src/app/data/custom-field'
 | 
			
		||||
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
 | 
			
		||||
import { DisplayMode, DisplayField } from 'src/app/data/document'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const savedView: SavedView = {
 | 
			
		||||
  id: 1,
 | 
			
		||||
@@ -125,6 +126,12 @@ describe('SavedViewWidgetComponent', () => {
 | 
			
		||||
        PreviewPopupComponent,
 | 
			
		||||
        CustomFieldDisplayComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        DragDropModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        DocumentService,
 | 
			
		||||
@@ -163,13 +170,8 @@ describe('SavedViewWidgetComponent', () => {
 | 
			
		||||
              }),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        DragDropModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ import { TestBed } from '@angular/core/testing'
 | 
			
		||||
import { StatisticsWidgetComponent } from './statistics-widget.component'
 | 
			
		||||
import { ComponentFixture } from '@angular/core/testing'
 | 
			
		||||
import {
 | 
			
		||||
  HttpClientTestingModule,
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
 | 
			
		||||
@@ -18,6 +18,7 @@ import {
 | 
			
		||||
} from 'src/app/services/consumer-status.service'
 | 
			
		||||
import { Subject } from 'rxjs'
 | 
			
		||||
import { DragDropModule } from '@angular/cdk/drag-drop'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
describe('StatisticsWidgetComponent', () => {
 | 
			
		||||
  let component: StatisticsWidgetComponent
 | 
			
		||||
@@ -33,13 +34,16 @@ describe('StatisticsWidgetComponent', () => {
 | 
			
		||||
        WidgetFrameComponent,
 | 
			
		||||
        IfPermissionsDirective,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [PermissionsGuard],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        DragDropModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(StatisticsWidgetComponent)
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
                        <a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
 | 
			
		||||
                      </p>
 | 
			
		||||
                    }
 | 
			
		||||
                    <div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded">
 | 
			
		||||
                    <div #hiddenAlerts="ngbCollapse" [ngbCollapse]="!alertsExpanded" (ngbCollapseChange)="alertsExpanded = $event">
 | 
			
		||||
                      @for (status of getStatusHidden(); track status) {
 | 
			
		||||
                        <div>
 | 
			
		||||
                          <ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
 | 
			
		||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
 | 
			
		||||
import {
 | 
			
		||||
  ComponentFixture,
 | 
			
		||||
  TestBed,
 | 
			
		||||
@@ -27,6 +27,7 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
 | 
			
		||||
import { UploadFileWidgetComponent } from './upload-file-widget.component'
 | 
			
		||||
import { DragDropModule } from '@angular/cdk/drag-drop'
 | 
			
		||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
 | 
			
		||||
const FAILED_STATUSES = [new FileStatus()]
 | 
			
		||||
const WORKING_STATUSES = [new FileStatus(), new FileStatus()]
 | 
			
		||||
@@ -59,6 +60,13 @@ describe('UploadFileWidgetComponent', () => {
 | 
			
		||||
        WidgetFrameComponent,
 | 
			
		||||
        IfPermissionsDirective,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        NgbAlertModule,
 | 
			
		||||
        DragDropModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        {
 | 
			
		||||
@@ -67,14 +75,8 @@ describe('UploadFileWidgetComponent', () => {
 | 
			
		||||
            currentUserCan: () => true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        RouterTestingModule.withRoutes(routes),
 | 
			
		||||
        NgbAlertModule,
 | 
			
		||||
        DragDropModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -186,6 +186,16 @@
 | 
			
		||||
                      [horizontal]="true"
 | 
			
		||||
                      [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>
 | 
			
		||||
              }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { DatePipe } from '@angular/common'
 | 
			
		||||
import {
 | 
			
		||||
  HttpClientTestingModule,
 | 
			
		||||
  HttpTestingController,
 | 
			
		||||
  provideHttpClientTesting,
 | 
			
		||||
} from '@angular/common/http/testing'
 | 
			
		||||
import {
 | 
			
		||||
  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 { PdfViewerModule } from 'ng2-pdf-viewer'
 | 
			
		||||
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 = {
 | 
			
		||||
  id: 3,
 | 
			
		||||
@@ -182,8 +184,51 @@ describe('DocumentDetailComponent', () => {
 | 
			
		||||
        RotateConfirmDialogComponent,
 | 
			
		||||
        DeletePagesConfirmDialogComponent,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        RouterModule.forRoot(routes),
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        PdfViewerModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        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,
 | 
			
		||||
          useValue: {
 | 
			
		||||
@@ -257,17 +302,8 @@ describe('DocumentDetailComponent', () => {
 | 
			
		||||
        PermissionsGuard,
 | 
			
		||||
        CustomDatePipe,
 | 
			
		||||
        DatePipe,
 | 
			
		||||
      ],
 | 
			
		||||
      imports: [
 | 
			
		||||
        RouterModule.forRoot(routes),
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbModule,
 | 
			
		||||
        NgSelectModule,
 | 
			
		||||
        FormsModule,
 | 
			
		||||
        ReactiveFormsModule,
 | 
			
		||||
        NgbModalModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        PdfViewerModule,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
@@ -989,10 +1025,10 @@ describe('DocumentDetailComponent', () => {
 | 
			
		||||
 | 
			
		||||
  it('should get suggestions', () => {
 | 
			
		||||
    const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
 | 
			
		||||
    suggestionsSpy.mockReturnValue(of({ tags: [1, 2] }))
 | 
			
		||||
    suggestionsSpy.mockReturnValue(of({ tags: [42, 43] }))
 | 
			
		||||
    initNormally()
 | 
			
		||||
    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', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
  {{title}}
 | 
			
		||||
</h6>
 | 
			
		||||
 | 
			
		||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expand">
 | 
			
		||||
<div #collapse="ngbCollapse" [ngbCollapse]="!expand" (ngbCollapseChange)="expand = $event">
 | 
			
		||||
  <table class="table table-borderless">
 | 
			
		||||
    <tbody>
 | 
			
		||||
      @for (m of metadata; track m) {
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
                                    @if (change.key === 'content') {
 | 
			
		||||
                                        <code class="text-primary">{{ change.value[1]?.substring(0,100) }}...</code>
 | 
			
		||||
                                    } @else {
 | 
			
		||||
                                        <code class="text-primary">{{ change.value[1] }}</code>
 | 
			
		||||
                                        <code class="text-primary">{{ getPrettyName(change.key, change.value[1]) | async }}</code>
 | 
			
		||||
                                    }
 | 
			
		||||
                                </li>
 | 
			
		||||
                            }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,31 +4,48 @@ import { DocumentHistoryComponent } from './document-history.component'
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service'
 | 
			
		||||
import { of } from 'rxjs'
 | 
			
		||||
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 { DatePipe } from '@angular/common'
 | 
			
		||||
import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
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', () => {
 | 
			
		||||
  let component: DocumentHistoryComponent
 | 
			
		||||
  let fixture: ComponentFixture<DocumentHistoryComponent>
 | 
			
		||||
  let documentService: DocumentService
 | 
			
		||||
  let correspondentService: CorrespondentService
 | 
			
		||||
  let documentTypeService: DocumentTypeService
 | 
			
		||||
  let storagePathService: StoragePathService
 | 
			
		||||
  let userService: UserService
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [DocumentHistoryComponent, CustomDatePipe],
 | 
			
		||||
      providers: [DatePipe],
 | 
			
		||||
      imports: [
 | 
			
		||||
        HttpClientTestingModule,
 | 
			
		||||
        NgbCollapseModule,
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        NgbTooltipModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        DatePipe,
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
        provideHttpClientTesting(),
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents()
 | 
			
		||||
 | 
			
		||||
    fixture = TestBed.createComponent(DocumentHistoryComponent)
 | 
			
		||||
    documentService = TestBed.inject(DocumentService)
 | 
			
		||||
    correspondentService = TestBed.inject(CorrespondentService)
 | 
			
		||||
    documentTypeService = TestBed.inject(DocumentTypeService)
 | 
			
		||||
    storagePathService = TestBed.inject(StoragePathService)
 | 
			
		||||
    userService = TestBed.inject(UserService)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -55,4 +72,91 @@ describe('DocumentHistoryComponent', () => {
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    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
		Reference in New Issue
	
	Block a user