mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-03 18:54:40 -05:00
Compare commits
280 Commits
v2.9.0
...
v2.13.0-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0a61b8e6fc | ||
![]() |
e78d758656 | ||
![]() |
073c42984a | ||
![]() |
2994f3a740 | ||
![]() |
2353f7c2db | ||
![]() |
dcc8d4046a | ||
![]() |
024b60638a | ||
![]() |
8dd355f6bf | ||
![]() |
cf3645c296 | ||
![]() |
facec317ef | ||
![]() |
95d1abd416 | ||
![]() |
7c11a37150 | ||
![]() |
e49ed58f1a | ||
![]() |
54293bedb1 | ||
![]() |
fc683e150a | ||
![]() |
b3487f1843 | ||
![]() |
f8d79b012f | ||
![]() |
2e3637d712 | ||
![]() |
74001bd0da | ||
![]() |
374a1ceb05 | ||
![]() |
59f726b2a2 | ||
![]() |
77bebc861d | ||
![]() |
85e57ede9b | ||
![]() |
a7424a7bfe | ||
![]() |
46b8e536a8 | ||
![]() |
2ab71137b9 | ||
![]() |
0b829cab32 | ||
![]() |
991c9b0ca4 | ||
![]() |
b9c1ba8a1d | ||
![]() |
c9e33a3401 | ||
![]() |
dd9b10bdf8 | ||
![]() |
546fd2740b | ||
![]() |
56e1365b4b | ||
![]() |
e6f59472e4 | ||
![]() |
5e687d9a93 | ||
![]() |
c92c3e224a | ||
![]() |
4adf20af1e | ||
![]() |
a9b7965dcf | ||
![]() |
d7ba6d98d3 | ||
![]() |
f06ff85b7d | ||
![]() |
1b7cacc877 | ||
![]() |
870d6ee782 | ||
![]() |
3aba68c09f | ||
![]() |
609fa9a212 | ||
![]() |
16069cde23 | ||
![]() |
a440c88b81 | ||
![]() |
97030a807f | ||
![]() |
4146b140d3 | ||
![]() |
fa6f013db5 | ||
![]() |
6192c15c4d | ||
![]() |
8aa35540b5 | ||
![]() |
e787055294 | ||
![]() |
045b62ca66 | ||
![]() |
3b7fdb2f37 | ||
![]() |
bd1f05df24 | ||
![]() |
eeeec498d4 | ||
![]() |
0af2b967e4 | ||
![]() |
4193401be7 | ||
![]() |
36df6fd3e5 | ||
![]() |
86a540e68e | ||
![]() |
fb3a881387 | ||
![]() |
8e555cce9e | ||
![]() |
4f8e59030e | ||
![]() |
5075d0bab0 | ||
![]() |
fb3a136b32 | ||
![]() |
66a8057e31 | ||
![]() |
cb6cf7f771 | ||
![]() |
9a7f95865f | ||
![]() |
0d1e0bc70e | ||
![]() |
bee963c23d | ||
![]() |
f1559b7108 | ||
![]() |
a64a182fc3 | ||
![]() |
aeb49898e5 | ||
![]() |
a2c8fcd46b | ||
![]() |
357ae92d88 | ||
![]() |
74330623b3 | ||
![]() |
3813dc2e18 | ||
![]() |
3df8be0bc7 | ||
![]() |
cc25cbc026 | ||
![]() |
e98d52830f | ||
![]() |
4903e4290d | ||
![]() |
e1ba1a1898 | ||
![]() |
b29c1e91d1 | ||
![]() |
a03c7701a5 | ||
![]() |
b8c9d1316c | ||
![]() |
a63ef26d38 | ||
![]() |
96b2884458 | ||
![]() |
8543202723 | ||
![]() |
a63904b7af | ||
![]() |
eb27bc9e7d | ||
![]() |
489b24ad65 | ||
![]() |
5349eb2302 | ||
![]() |
a8fd023398 | ||
![]() |
e34d48d913 | ||
![]() |
ee529c2276 | ||
![]() |
3d5e45c20a | ||
![]() |
b8283047ae | ||
![]() |
dad3a1ff28 | ||
![]() |
ce663398e6 | ||
![]() |
f5ec6de047 | ||
![]() |
807f788f92 | ||
![]() |
eaaaa575b8 | ||
![]() |
e21552e053 | ||
![]() |
6a7274c414 | ||
![]() |
35de04a2ce | ||
![]() |
a0c227fe55 | ||
![]() |
057ce29676 | ||
![]() |
982eeb0d24 | ||
![]() |
dfa25343a3 | ||
![]() |
dcfb4494c9 | ||
![]() |
5a1ef27224 | ||
![]() |
6f79ee9877 | ||
![]() |
bc0e420d67 | ||
![]() |
8e34756e6b | ||
![]() |
76951ea482 | ||
![]() |
b5e4aaa778 | ||
![]() |
39998cb34f | ||
![]() |
a771d2afd9 | ||
![]() |
dac3def6b9 | ||
![]() |
674b4a839c | ||
![]() |
037dcb6a11 | ||
![]() |
ad8c60d153 | ||
![]() |
3ea312e136 | ||
![]() |
d2a04743eb | ||
![]() |
b34f9c3b20 | ||
![]() |
fea7b0ec8c | ||
![]() |
0042e3eca4 | ||
![]() |
36db6f3d4b | ||
![]() |
3c633c2015 | ||
![]() |
dd8b51de67 | ||
![]() |
5fe846de1d | ||
![]() |
4711468598 | ||
![]() |
19dfaf1b94 | ||
![]() |
183ea24c9f | ||
![]() |
99693b6d30 | ||
![]() |
c0ad82b695 | ||
![]() |
fd36323d1c | ||
![]() |
4059c83a21 | ||
![]() |
b25c015516 | ||
![]() |
8fa52046e4 | ||
![]() |
15554322dd | ||
![]() |
0ee85aae21 | ||
![]() |
839fb34c8e | ||
![]() |
928580bf4f | ||
![]() |
9cca7aaa08 | ||
![]() |
38560cf13a | ||
![]() |
474ca08ef9 | ||
![]() |
d4fd529e49 | ||
![]() |
2260617447 | ||
![]() |
7c3ba3e518 | ||
![]() |
849e3a10ac | ||
![]() |
bdceeef3fb | ||
![]() |
8630e5f5b6 | ||
![]() |
2312eba5b6 | ||
![]() |
ad9d654886 | ||
![]() |
fa19a8975e | ||
![]() |
8987cd448f | ||
![]() |
a7536e3ebf | ||
![]() |
9c3bc2eb83 | ||
![]() |
d179efbd48 | ||
![]() |
801df5f7bd | ||
![]() |
d1c3ea7faa | ||
![]() |
dcfc53b7f2 | ||
![]() |
45002f8083 | ||
![]() |
722a2ca1e4 | ||
![]() |
2bd8b67d02 | ||
![]() |
637efd5cb3 | ||
![]() |
ad3dd76c2f | ||
![]() |
75aba12589 | ||
![]() |
ec33edb2f4 | ||
![]() |
4b706fa4dd | ||
![]() |
98799d9a69 | ||
![]() |
b80070ac0b | ||
![]() |
6b2e5559ca | ||
![]() |
82340ad6e3 | ||
![]() |
f45ab723ae | ||
![]() |
8e3ca37b05 | ||
![]() |
aef387ed69 | ||
![]() |
b93c970635 | ||
![]() |
a7d8b5c960 | ||
![]() |
56c9a3f270 | ||
![]() |
0c3dac45b5 | ||
![]() |
6965165c76 | ||
![]() |
73d33ff25a | ||
![]() |
df153be30e | ||
![]() |
9950ff2337 | ||
![]() |
9a9ab85baf | ||
![]() |
eaea42334b | ||
![]() |
de8ac013ee | ||
![]() |
9e2bf4820a | ||
![]() |
9af879a2bf | ||
![]() |
ff4203938b | ||
![]() |
a63f8809fa | ||
![]() |
edcde1f142 | ||
![]() |
6dc094f760 | ||
![]() |
186f520819 | ||
![]() |
0365fc5ac3 | ||
![]() |
61811a4bec | ||
![]() |
bb83c1eb0a | ||
![]() |
4ad4862641 | ||
![]() |
c03aa03ac2 | ||
![]() |
ada283441c | ||
![]() |
3cf73a77ac | ||
![]() |
71fedcb466 | ||
![]() |
1b9cf5121b | ||
![]() |
7fe76656f2 | ||
![]() |
0deb8a11d6 | ||
![]() |
064d384d97 | ||
![]() |
5045d06744 | ||
![]() |
c57aa81d15 | ||
![]() |
d35e350c79 | ||
![]() |
dd878c8d70 | ||
![]() |
9cd1945a89 | ||
![]() |
faab8a5560 | ||
![]() |
fcc9847bc3 | ||
![]() |
a64d457c30 | ||
![]() |
e799d757c2 | ||
![]() |
ac0ed0def8 | ||
![]() |
f01283c309 | ||
![]() |
a3c468a004 | ||
![]() |
3435ffd00c | ||
![]() |
4f1185c65d | ||
![]() |
2b1498cc6d | ||
![]() |
0643db4347 | ||
![]() |
29e6371cd1 | ||
![]() |
80c2d90e74 | ||
![]() |
f3cf608caa | ||
![]() |
c6d0557a3b | ||
![]() |
f3b7ae93f0 | ||
![]() |
e4265d0594 | ||
![]() |
deda49c204 | ||
![]() |
276abc1404 | ||
![]() |
6defe24ae7 | ||
![]() |
6ed5d11758 | ||
![]() |
9d34327a6d | ||
![]() |
63f164d099 | ||
![]() |
0f9710dc8f | ||
![]() |
cccba47bd7 | ||
![]() |
91585a1fa6 | ||
![]() |
3bb6a32ab9 | ||
![]() |
31f592453e | ||
![]() |
56f5f93c48 | ||
![]() |
e6aefd1063 | ||
![]() |
6187ee82af | ||
![]() |
a066ccff4f | ||
![]() |
f73be01897 | ||
![]() |
07ee25be06 | ||
![]() |
4347c87e92 | ||
![]() |
807f0f1345 | ||
![]() |
12857890cc | ||
![]() |
2ad0f8325c | ||
![]() |
6aae8bf440 | ||
![]() |
5c7522b423 | ||
![]() |
37e607abb9 | ||
![]() |
8045f3d58c | ||
![]() |
a796e58a94 | ||
![]() |
9d4e2d4652 | ||
![]() |
28db7e84e6 | ||
![]() |
22a6360edf | ||
![]() |
61485b0f1d | ||
![]() |
fa7a5451db | ||
![]() |
70069cd502 | ||
![]() |
9e8b96cd34 | ||
![]() |
d03058e539 | ||
![]() |
c929a18da2 | ||
![]() |
5bd248578a | ||
![]() |
ebfb72a691 | ||
![]() |
fc440d8317 | ||
![]() |
b6f6d524d6 | ||
![]() |
f225f72145 | ||
![]() |
d9002005b1 | ||
![]() |
6ddb62bf3f | ||
![]() |
d1ac15baa9 | ||
![]() |
81e4092f53 | ||
![]() |
d8c96b6e4a | ||
![]() |
3d6aa8a656 | ||
![]() |
6d2ae3df1f | ||
![]() |
de7c22e8d6 | ||
![]() |
74c44fe418 | ||
![]() |
a6407d64e9 |
14
.codecov.yml
14
.codecov.yml
@@ -14,6 +14,9 @@ flag_management:
|
||||
# codecov will only comment if coverage changes
|
||||
comment:
|
||||
require_changes: true
|
||||
# https://docs.codecov.com/docs/javascript-bundle-analysis
|
||||
require_bundle_changes: true
|
||||
bundle_change_threshold: "50Kb"
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
@@ -22,7 +25,12 @@ coverage:
|
||||
threshold: 1%
|
||||
patch:
|
||||
default:
|
||||
# For the changed lines only, target 75% covered, but
|
||||
# allow as low as 50%
|
||||
target: 75%
|
||||
# For the changed lines only, target 100% covered, but
|
||||
# allow as low as 75%
|
||||
target: 100%
|
||||
threshold: 25%
|
||||
# https://docs.codecov.com/docs/javascript-bundle-analysis
|
||||
bundle_analysis:
|
||||
# Fail if the bundle size increases by more than 1MB
|
||||
warning_threshold: "1MB"
|
||||
status: true
|
||||
|
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/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -9,7 +9,7 @@ body:
|
||||
### ⚠️ Please remember: issues are for *bugs*
|
||||
That is, something you believe affects every single user of Paperless-ngx, not just you. If you're not sure, start with one of the other options below.
|
||||
|
||||
Also, note that **Paperless-ngx does not perform OCR itself**, that is handled by other tools. Problems with OCR of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues
|
||||
Also, note that **Paperless-ngx does not perform OCR or archive file creation itself**, those are handled by other tools. Problems with OCR or archive versions of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
@@ -110,6 +110,8 @@ body:
|
||||
options:
|
||||
- label: I believe this issue is a bug that affects all users of Paperless-ngx, not something specific to my installation.
|
||||
required: true
|
||||
- label: This issue is not about the OCR or archive creation of a specific file(s). Otherwise, please see above regarding OCR tools.
|
||||
required: true
|
||||
- label: I have already searched for relevant existing issues and discussions before opening this report.
|
||||
required: true
|
||||
- label: I have updated the title field above with a concise description.
|
||||
|
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
@@ -16,9 +16,9 @@ on:
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.12.1"
|
||||
DEFAULT_PIP_ENV_VERSION: "2024.0.3"
|
||||
# This is the default version of Python to use in most steps which aren't specific
|
||||
DEFAULT_PYTHON_VERSION: "3.10"
|
||||
DEFAULT_PYTHON_VERSION: "3.11"
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
- pre-commit
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.9', '3.10', '3.11']
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
fail-fast: false
|
||||
steps:
|
||||
-
|
||||
@@ -260,7 +260,7 @@ jobs:
|
||||
retention-days: 7
|
||||
|
||||
tests-coverage-upload:
|
||||
name: "Upload Coverage"
|
||||
name: "Upload to Codecov"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- tests-backend
|
||||
@@ -306,6 +306,30 @@ jobs:
|
||||
# future expansion
|
||||
flags: backend
|
||||
directory: src/
|
||||
-
|
||||
name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
-
|
||||
name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
~/.cache
|
||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
|
||||
-
|
||||
name: Re-link Angular cli
|
||||
run: cd src-ui && npm link @angular/cli
|
||||
-
|
||||
name: Build frontend and upload analysis
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: cd src-ui && ng build --configuration=production
|
||||
|
||||
build-docker-image:
|
||||
name: Build Docker image for ${{ github.ref_name }}
|
||||
@@ -398,7 +422,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 +430,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: |
|
||||
@@ -460,7 +486,7 @@ jobs:
|
||||
name: Patch whitenoise
|
||||
run: |
|
||||
curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch
|
||||
patch -d $(pipenv --venv)/lib/python3.10/site-packages --verbose -p2 < 484.patch
|
||||
patch -d $(pipenv --venv)/lib/python3.11/site-packages --verbose -p2 < 484.patch
|
||||
rm 484.patch
|
||||
-
|
||||
name: Install system dependencies
|
||||
|
4
.github/workflows/cleanup-tags.yml
vendored
4
.github/workflows/cleanup-tags.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
-
|
||||
name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.6.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
-
|
||||
name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.6.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.8.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "${{ github.repository_owner }}"
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,6 +22,7 @@ var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
/src/paperless_mail/templates/node_modules
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
@@ -65,6 +66,8 @@ target/
|
||||
.vscode
|
||||
/src-ui/.vscode
|
||||
/docs/.vscode
|
||||
.vscode-server
|
||||
*CommandMarker
|
||||
|
||||
# Other stuff that doesn't belong
|
||||
.virtualenv
|
||||
|
@@ -36,8 +36,9 @@ repos:
|
||||
exclude_types:
|
||||
- pofile
|
||||
- json
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: 'v3.1.0'
|
||||
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: 'v3.3.3'
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or:
|
||||
@@ -47,7 +48,7 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.4.7'
|
||||
rev: 'v0.6.8'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
@@ -61,6 +62,8 @@ repos:
|
||||
rev: v6.2.1
|
||||
hooks:
|
||||
- id: beautysh
|
||||
additional_dependencies:
|
||||
- setuptools
|
||||
args:
|
||||
- "--tab"
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
|
@@ -1 +1 @@
|
||||
3.9.18
|
||||
3.10.15
|
||||
|
@@ -2,7 +2,7 @@ fix = true
|
||||
line-length = 88
|
||||
respect-gitignore = true
|
||||
src = ["src"]
|
||||
target-version = "py39"
|
||||
target-version = "py310"
|
||||
output-format = "grouped"
|
||||
show-fixes = true
|
||||
|
||||
|
@@ -11,7 +11,7 @@ If you want to implement something big:
|
||||
|
||||
## Python
|
||||
|
||||
Paperless supports python 3.9 - 3.11. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
|
||||
Paperless supports python 3.10 - 3.12 at this time. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
|
||||
|
||||
## Branches
|
||||
|
||||
|
48
Dockerfile
48
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
|
||||
|
||||
@@ -21,7 +31,7 @@ RUN set -eux \
|
||||
# Comments:
|
||||
# - pipenv dependencies are not left in the final image
|
||||
# - pipenv can't touch the final image somehow
|
||||
FROM --platform=$BUILDPLATFORM docker.io/python:3.11-alpine as pipenv-base
|
||||
FROM --platform=$BUILDPLATFORM docker.io/python:3.12-alpine AS pipenv-base
|
||||
|
||||
WORKDIR /usr/src/pipenv
|
||||
|
||||
@@ -29,7 +39,7 @@ COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.12.1 \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2024.0.3 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
@@ -37,7 +47,7 @@ RUN set -eux \
|
||||
# Purpose: The final image
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here
|
||||
FROM docker.io/python:3.11-slim-bookworm as main-app
|
||||
FROM docker.io/python:3.12-slim-bookworm AS main-app
|
||||
|
||||
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/"
|
||||
@@ -128,17 +138,17 @@ RUN set -eux \
|
||||
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
||||
&& echo "Installing Ghostscript ${GS_VERSION}" \
|
||||
&& curl --fail --silent --show-error --location \
|
||||
--output libgs10_${GS_VERSION}.dfsg.git20240518-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg.git20240518-1_${TARGETARCH}.deb \
|
||||
--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.git20240518-1_${TARGETARCH}.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg.git20240518-1_${TARGETARCH}.deb \
|
||||
--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.git20240518-1_all.deb \
|
||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg.git20240518-1_all.deb \
|
||||
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg.git20240518-1_all.deb \
|
||||
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg.git20240518-1_${TARGETARCH}.deb \
|
||||
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg.git20240518-1_${TARGETARCH}.deb \
|
||||
--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 \
|
||||
@@ -223,20 +233,20 @@ 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.2-cp312-cp312-linux_x86_64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.2/psycopg_c-3.2.2-cp312-cp312-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.2-cp312-cp312-linux_aarch64.whl \
|
||||
https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.2/psycopg_c-3.2.2-cp312-cp312-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 \
|
||||
&& patch -d /usr/local/lib/python3.11/site-packages --verbose -p2 < 484.patch \
|
||||
&& patch -d /usr/local/lib/python3.12/site-packages --verbose -p2 < 484.patch \
|
||||
&& rm 484.patch \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" punkt \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" punkt_tab \
|
||||
&& echo "Cleaning up image" \
|
||||
&& apt-get --yes purge ${BUILD_PACKAGES} \
|
||||
&& apt-get --yes autoremove --purge \
|
||||
@@ -265,6 +275,8 @@ RUN set -eux \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/media \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/consume \
|
||||
&& mkdir --parents --verbose /usr/src/paperless/export \
|
||||
&& echo "Creating gnupg directory" \
|
||||
&& mkdir -m700 --verbose /usr/src/paperless/.gnupg \
|
||||
&& echo "Adjusting all permissions" \
|
||||
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
|
||||
&& echo "Collecting static files" \
|
||||
|
17
Pipfile
17
Pipfile
@@ -7,17 +7,18 @@ 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 = "~=5.1.1"
|
||||
django-allauth = {extras = ["socialaccount"], version = "*"}
|
||||
django-auditlog = "*"
|
||||
django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
django-cors-headers = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=24.2"
|
||||
django-filter = "~=24.3"
|
||||
django-guardian = "*"
|
||||
django-multiselectfield = "*"
|
||||
djangorestframework = "==3.14.0"
|
||||
django-soft-delete = "*"
|
||||
djangorestframework = "==3.15.2"
|
||||
djangorestframework-guardian = "*"
|
||||
drf-writable-nested = "*"
|
||||
bleach = "*"
|
||||
@@ -29,12 +30,14 @@ filelock = "*"
|
||||
flower = "*"
|
||||
gotenberg-client = "*"
|
||||
gunicorn = "*"
|
||||
httpx-oauth = "*"
|
||||
imap-tools = "*"
|
||||
inotifyrecursive = "~=0.3"
|
||||
jinja2 = "~=3.1"
|
||||
langdetect = "*"
|
||||
mysqlclient = "*"
|
||||
nltk = "*"
|
||||
ocrmypdf = "~=15.4"
|
||||
ocrmypdf = "~=16.5"
|
||||
pathvalidate = "*"
|
||||
pdf2image = "*"
|
||||
psycopg = {version = "*", extras = ["c"]}
|
||||
@@ -53,8 +56,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]
|
||||
@@ -70,6 +73,7 @@ pytest-httpx = "*"
|
||||
pytest-env = "*"
|
||||
pytest-sugar = "*"
|
||||
pytest-xdist = "*"
|
||||
pytest-mock = "*"
|
||||
pytest-rerunfailures = "*"
|
||||
imagehash = "*"
|
||||
daphne = "*"
|
||||
@@ -92,5 +96,4 @@ types-tqdm = "*"
|
||||
types-Markdown = "*"
|
||||
types-Pygments = "*"
|
||||
types-colorama = "*"
|
||||
types-psycopg2 = "*"
|
||||
types-setuptools = "*"
|
||||
|
4367
Pipfile.lock
generated
4367
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@ Thanks to the generous folks at [DigitalOcean](https://m.do.co/c/8d70b916d462),
|
||||
<a href="https://m.do.co/c/8d70b916d462" style="padding-top: 4px; display: block;">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_white.svg" width="140px">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_black_.svg" width="140px">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="140px">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_black_.svg" width="140px">
|
||||
</picture>
|
||||
</a>
|
||||
|
@@ -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
|
||||
|
@@ -10,8 +10,8 @@ map_uidgid() {
|
||||
local -r usermap_new_gid=${USERMAP_GID:-${usermap_original_gid:-$usermap_new_uid}}
|
||||
if [[ ${usermap_new_uid} != "${usermap_original_uid}" || ${usermap_new_gid} != "${usermap_original_gid}" ]]; then
|
||||
echo "Mapping UID and GID for paperless:paperless to $usermap_new_uid:$usermap_new_gid"
|
||||
usermod -o -u "${usermap_new_uid}" paperless
|
||||
groupmod -o -g "${usermap_new_gid}" paperless
|
||||
usermod --non-unique --uid "${usermap_new_uid}" paperless
|
||||
groupmod --non-unique --gid "${usermap_new_gid}" paperless
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ custom_container_init() {
|
||||
fi
|
||||
|
||||
# Make sure custom init directory has files in it
|
||||
if [ -n "$(/bin/ls -A "${custom_script_dir}" 2>/dev/null)" ]; then
|
||||
if [ -n "$(/bin/ls --almost-all "${custom_script_dir}" 2>/dev/null)" ]; then
|
||||
echo "[custom-init] files found in ${custom_script_dir} executing"
|
||||
# Loop over files in the directory
|
||||
for SCRIPT in "${custom_script_dir}"/*; do
|
||||
@@ -86,13 +86,13 @@ initialize() {
|
||||
"${CONSUME_DIR}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
echo "Creating directory ${dir}"
|
||||
mkdir --parents "${dir}"
|
||||
mkdir --parents --verbose "${dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
local -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}"
|
||||
echo "Creating directory scratch directory ${tmp_dir}"
|
||||
mkdir --parents "${tmp_dir}"
|
||||
mkdir --parents --verbose "${tmp_dir}"
|
||||
|
||||
set +e
|
||||
echo "Adjusting permissions of paperless files. This may take a while."
|
||||
@@ -102,7 +102,7 @@ initialize() {
|
||||
"${DATA_DIR}" \
|
||||
"${MEDIA_ROOT_DIR}" \
|
||||
"${CONSUME_DIR}"; do
|
||||
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown paperless:paperless {} +
|
||||
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
|
||||
done
|
||||
set -e
|
||||
|
||||
@@ -122,33 +122,44 @@ install_languages() {
|
||||
if [ ${#langs[@]} -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
apt-get update
|
||||
|
||||
# Build list of packages to install
|
||||
to_install=()
|
||||
for lang in "${langs[@]}"; do
|
||||
pkg="tesseract-ocr-$lang"
|
||||
|
||||
if dpkg -s "$pkg" &>/dev/null; then
|
||||
if dpkg --status "$pkg" &>/dev/null; then
|
||||
echo "Package $pkg already installed!"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! apt-cache show "$pkg" &>/dev/null; then
|
||||
echo "Package $pkg not found! :("
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Installing package $pkg..."
|
||||
if ! apt-get -y install "$pkg" &>/dev/null; then
|
||||
echo "Could not install $pkg"
|
||||
exit 1
|
||||
else
|
||||
to_install+=("$pkg")
|
||||
fi
|
||||
done
|
||||
|
||||
# Use apt only when we install packages
|
||||
if [ ${#to_install[@]} -gt 0 ]; then
|
||||
apt-get update
|
||||
|
||||
for pkg in "${to_install[@]}"; do
|
||||
|
||||
if ! apt-cache show "$pkg" &>/dev/null; then
|
||||
echo "Skipped $pkg: Package not found! :("
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Installing package $pkg..."
|
||||
if ! apt-get --assume-yes install "$pkg" &>/dev/null; then
|
||||
echo "Could not install $pkg"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Paperless-ngx docker container starting..."
|
||||
|
||||
gosu_cmd=(gosu paperless)
|
||||
if [ "$(id -u)" == "$(id -u paperless)" ]; then
|
||||
if [ "$(id --user)" == "$(id --user paperless)" ]; then
|
||||
gosu_cmd=()
|
||||
fi
|
||||
|
||||
|
@@ -13,7 +13,7 @@ wait_for_postgres() {
|
||||
|
||||
# Disable warning, host and port can't have spaces
|
||||
# shellcheck disable=SC2086
|
||||
while [ ! "$(pg_isready -h ${host} -p ${port})" ]; do
|
||||
while [ ! "$(pg_isready --host ${host} --port ${port})" ]; do
|
||||
|
||||
if [ $attempt_num -eq $max_attempts ]; then
|
||||
echo "Unable to connect to database."
|
||||
@@ -25,6 +25,7 @@ wait_for_postgres() {
|
||||
attempt_num=$(("$attempt_num" + 1))
|
||||
sleep 5
|
||||
done
|
||||
echo "Connected to PostgreSQL"
|
||||
}
|
||||
|
||||
wait_for_mariadb() {
|
||||
@@ -51,6 +52,7 @@ wait_for_mariadb() {
|
||||
attempt_num=$(("$attempt_num" + 1))
|
||||
sleep 5
|
||||
done
|
||||
echo "Connected to MariaDB"
|
||||
}
|
||||
|
||||
wait_for_redis() {
|
||||
|
@@ -248,6 +248,8 @@ optional arguments:
|
||||
-z, --zip
|
||||
-zn, --zip-name
|
||||
--data-only
|
||||
--no-progress-bar
|
||||
--passphrase
|
||||
```
|
||||
|
||||
`target` is a folder to which the data gets written. This includes
|
||||
@@ -309,6 +311,13 @@ value set in `-zn` or `--zip-name`.
|
||||
If `--data-only` is provided, only the database will be exported. This option is intended
|
||||
to facilitate database upgrades without needing to clean documents and thumbnails from the media directory.
|
||||
|
||||
If `--no-progress-bar` is provided, the progress bar will be hidden, rendering the
|
||||
exporter quiet. This option is useful for scripting scenarios, such as when using the
|
||||
exporter with `crontab`.
|
||||
|
||||
If `--passphrase` is provided, it will be used to encrypt certain fields in the export. This value
|
||||
must be provided to import. If this value is lost, the export cannot be imported.
|
||||
|
||||
!!! warning
|
||||
|
||||
If exporting with the file name format, there may be errors due to
|
||||
@@ -327,16 +336,19 @@ and the script does the rest of the work:
|
||||
document_importer source
|
||||
```
|
||||
|
||||
| Option | Required | Default | Description |
|
||||
| ----------- | -------- | ------- | ------------------------------------------------------------------------- |
|
||||
| source | Yes | N/A | The directory containing an export |
|
||||
| --data-only | No | False | If provided, only import data, do not import document files or thumbnails |
|
||||
| Option | Required | Default | Description |
|
||||
| ------------------- | -------- | ------- | ------------------------------------------------------------------------- |
|
||||
| source | Yes | N/A | The directory containing an export |
|
||||
| `--no-progress-bar` | No | False | If provided, the progress bar will be hidden |
|
||||
| `--data-only` | No | False | If provided, only import data, do not import document files or thumbnails |
|
||||
| `--passphrase` | No | N/A | If your export was encrypted with a passphrase, must be provided |
|
||||
|
||||
When you use the provided docker compose script, put the export inside
|
||||
the `export` folder in your paperless source directory. Specify
|
||||
`../export` as the `source`.
|
||||
|
||||
Note that .zip files (as can be generated from the exporter) are not supported.
|
||||
Note that .zip files (as can be generated from the exporter) are not supported. You must unzip them into
|
||||
the target directory first.
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -346,6 +358,7 @@ Note that .zip files (as can be generated from the exporter) are not supported.
|
||||
!!! warning
|
||||
|
||||
The importer should be run against a completely empty installation (database and directories) of Paperless-ngx.
|
||||
If using a data only import, only the database must be empty.
|
||||
|
||||
### Document retagger {#retagger}
|
||||
|
||||
|
@@ -36,7 +36,7 @@ The following algorithms are available:
|
||||
- **Regular expression:** Parses the match as a regular expression and
|
||||
tries to find a match within the document.
|
||||
- **Fuzzy match:** Uses a partial matching based on locating the tag text
|
||||
inside the document, using a [partial ratio](https://maxbachmann.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
|
||||
inside the document, using a [partial ratio](https://rapidfuzz.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
|
||||
- **Auto:** Tries to automatically match new documents. This does not
|
||||
require you to set a match. See the [notes below](#automatic-matching).
|
||||
|
||||
@@ -187,6 +187,7 @@ variables:
|
||||
| `DOCUMENT_THUMBNAIL_PATH` | Path to the generated thumbnail |
|
||||
| `DOCUMENT_DOWNLOAD_URL` | URL for document download |
|
||||
| `DOCUMENT_THUMBNAIL_URL` | URL for the document thumbnail |
|
||||
| `DOCUMENT_OWNER` | Username of the document owner (if any) |
|
||||
| `DOCUMENT_CORRESPONDENT` | Assigned correspondent (if any) |
|
||||
| `DOCUMENT_TAGS` | Comma separated list of tags applied (if any) |
|
||||
| `DOCUMENT_ORIGINAL_FILENAME` | Filename of original document |
|
||||
@@ -264,7 +265,7 @@ This variable allows you to configure the filename (folders are allowed)
|
||||
using placeholders. For example, configuring this to
|
||||
|
||||
```bash
|
||||
PAPERLESS_FILENAME_FORMAT={created_year}/{correspondent}/{title}
|
||||
PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ correspondent }}/{{ title }}
|
||||
```
|
||||
|
||||
will create a directory structure as follows:
|
||||
@@ -297,39 +298,39 @@ will create a directory structure as follows:
|
||||
when changing `PAPERLESS_FILENAME_FORMAT` you will need to manually run the
|
||||
[`document renamer`](administration.md#renamer) to move any existing documents.
|
||||
|
||||
#### Placeholders
|
||||
### Placeholders {#filename-format-variables}
|
||||
|
||||
Paperless provides the following placeholders within filenames:
|
||||
Paperless provides the following variables for use within filenames:
|
||||
|
||||
- `{asn}`: The archive serial number of the document, or "none".
|
||||
- `{correspondent}`: The name of the correspondent, or "none".
|
||||
- `{document_type}`: The name of the document type, or "none".
|
||||
- `{tag_list}`: A comma separated list of all tags assigned to the
|
||||
- `{{ asn }}`: The archive serial number of the document, or "none".
|
||||
- `{{ correspondent }}`: The name of the correspondent, or "none".
|
||||
- `{{ document_type }}`: The name of the document type, or "none".
|
||||
- `{{ tag_list }}`: A comma separated list of all tags assigned to the
|
||||
document.
|
||||
- `{title}`: The title of the document.
|
||||
- `{created}`: The full date (ISO format) the document was created.
|
||||
- `{created_year}`: Year created only, formatted as the year with
|
||||
- `{{ title }}`: The title of the document.
|
||||
- `{{ created }}`: The full date (ISO format) the document was created.
|
||||
- `{{ created_year }}`: Year created only, formatted as the year with
|
||||
century.
|
||||
- `{created_year_short}`: Year created only, formatted as the year
|
||||
- `{{ created_year_short }}`: Year created only, formatted as the year
|
||||
without century, zero padded.
|
||||
- `{created_month}`: Month created only (number 01-12).
|
||||
- `{created_month_name}`: Month created name, as per locale
|
||||
- `{created_month_name_short}`: Month created abbreviated name, as per
|
||||
- `{{ created_month }}`: Month created only (number 01-12).
|
||||
- `{{ created_month_name }}`: Month created name, as per locale
|
||||
- `{{ created_month_name_short }}`: Month created abbreviated name, as per
|
||||
locale
|
||||
- `{created_day}`: Day created only (number 01-31).
|
||||
- `{added}`: The full date (ISO format) the document was added to
|
||||
- `{{ created_day }}`: Day created only (number 01-31).
|
||||
- `{{ added }}`: The full date (ISO format) the document was added to
|
||||
paperless.
|
||||
- `{added_year}`: Year added only.
|
||||
- `{added_year_short}`: Year added only, formatted as the year without
|
||||
- `{{ added_year }}`: Year added only.
|
||||
- `{{ added_year_short }}`: Year added only, formatted as the year without
|
||||
century, zero padded.
|
||||
- `{added_month}`: Month added only (number 01-12).
|
||||
- `{added_month_name}`: Month added name, as per locale
|
||||
- `{added_month_name_short}`: Month added abbreviated name, as per
|
||||
- `{{ added_month }}`: Month added only (number 01-12).
|
||||
- `{{ added_month_name }}`: Month added name, as per locale
|
||||
- `{{ added_month_name_short }}`: Month added abbreviated name, as per
|
||||
locale
|
||||
- `{added_day}`: Day added only (number 01-31).
|
||||
- `{owner_username}`: Username of document owner, if any, or "none"
|
||||
- `{original_name}`: Document original filename, minus the extension, if any, or "none"
|
||||
- `{doc_pk}`: The paperless identifier (primary key) for the document.
|
||||
- `{{ added_day }}`: Day added only (number 01-31).
|
||||
- `{{ owner_username }}`: Username of document owner, if any, or "none"
|
||||
- `{{ original_name }}`: Document original filename, minus the extension, if any, or "none"
|
||||
- `{{ doc_pk }}`: The paperless identifier (primary key) for the document.
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -337,6 +338,11 @@ Paperless provides the following placeholders within filenames:
|
||||
you may run into the limits of your operating system's maximum path lengths.
|
||||
In that case, files will retain the previous path instead and the issue logged.
|
||||
|
||||
!!! tip
|
||||
|
||||
These variables are all simple strings, but the format can be a full template.
|
||||
See [Filename Templates](#filename-templates) for even more advanced formatting.
|
||||
|
||||
Paperless will try to conserve the information from your database as
|
||||
much as possible. However, some characters that you can use in document
|
||||
titles and correspondent names (such as `: \ /` and a couple more) are
|
||||
@@ -362,7 +368,7 @@ paperless will fall back to using the default naming scheme instead.
|
||||
However, keep in mind that inside docker, if files get stored outside of
|
||||
the predefined volumes, they will be lost after a restart.
|
||||
|
||||
##### Empty placeholders
|
||||
#### Empty placeholders
|
||||
|
||||
You can affect how empty placeholders are treated by changing the
|
||||
[`PAPERLESS_FILENAME_FORMAT_REMOVE_NONE`](configuration.md#PAPERLESS_FILENAME_FORMAT_REMOVE_NONE) setting.
|
||||
@@ -389,8 +395,8 @@ For example, you could define the following two storage paths:
|
||||
the correspondence.
|
||||
|
||||
```
|
||||
By Year = {created_year}/{correspondent}/{title}
|
||||
Insurances = Insurances/{correspondent}/{created_year}-{created_month}-{created_day} {title}
|
||||
By Year = {{ created_year }}/{{ correspondent }}/{{ title }}
|
||||
Insurances = Insurances/{{ correspondent }}/{{ created_year }}-{{ created_month }}-{{ created_day }} {{ title }}
|
||||
```
|
||||
|
||||
If you then map these storage paths to the documents, you might get the
|
||||
@@ -417,6 +423,97 @@ Insurances/ # Insurances
|
||||
Defining a storage path is optional. If no storage path is defined for a
|
||||
document, the global [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) is applied.
|
||||
|
||||
### Filename Templates {#filename-templates}
|
||||
|
||||
The filename formatting uses [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/) to build the filename.
|
||||
This allows for complex logic to be included in the format, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
|
||||
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11) to manipulate the [variables](#filename-format-variables)
|
||||
provided. The template is provided as a string, potentially multiline, and rendered into a single line.
|
||||
|
||||
In addition, the entire Document instance is available to be utilized in a more advanced way, as well as some variables which only make sense to be accessed
|
||||
with more complex logic.
|
||||
|
||||
#### Additional Variables
|
||||
|
||||
- `{{ tag_name_list }}`: A list of tag names applied to the document, ordered by the tag name. Note this is a list, not a single string
|
||||
- `{{ custom_fields }}`: A mapping of custom field names to their type and value. A user can access the mapping by field name or check if a field is applied by checking its existence in the variable.
|
||||
|
||||
!!! tip
|
||||
|
||||
To access a custom field which has a space in the name, use the `get_cf_value` filter. See the examples below.
|
||||
This helps get fields by name and handle a default value if the named field is not attached to a Document.
|
||||
|
||||
#### Examples
|
||||
|
||||
This example will construct a path based on the archive serial number range:
|
||||
|
||||
```jinja
|
||||
somepath/
|
||||
{% if document.archive_serial_number >= 0 and document.archive_serial_number <= 200 %}
|
||||
asn-000-200/{{title}}
|
||||
{% elif document.archive_serial_number >= 201 and document.archive_serial_number <= 400 %}
|
||||
asn-201-400
|
||||
{% if document.archive_serial_number >= 201 and document.archive_serial_number < 300 %}
|
||||
/asn-2xx
|
||||
{% elif document.archive_serial_number >= 300 and document.archive_serial_number < 400 %}
|
||||
/asn-3xx
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
/{{ title }}
|
||||
```
|
||||
|
||||
For a document with an ASN of 205, it would result in `somepath/asn-201-400/asn-2xx/Title.pdf`, but
|
||||
a document with an ASN of 355 would be placed in `somepath/asn-201-400/asn-3xx/Title.pdf`.
|
||||
|
||||
```jinja
|
||||
{% if document.mime_type == "application/pdf" %}
|
||||
pdfs
|
||||
{% elif document.mime_type == "image/png" %}
|
||||
pngs
|
||||
{% else %}
|
||||
others
|
||||
{% endif %}
|
||||
/{{ title }}
|
||||
```
|
||||
|
||||
For a PDF document, it would result in `pdfs/Title.pdf`, but for a PNG document, the path would be `pngs/Title.pdf`.
|
||||
|
||||
To use custom fields:
|
||||
|
||||
```jinja
|
||||
{% if "Invoice" in custom_fields %}
|
||||
invoices/{{ custom_fields.Invoice.value }}
|
||||
{% else %}
|
||||
not-invoices/{{ title }}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
If the document has a custom field named "Invoice" with a value of 123, it would be filed into the `invoices/123.pdf`, but a document without the custom field
|
||||
would be filed to `not-invoices/Title.pdf`
|
||||
|
||||
If the custom field is named "Invoice Number", you would access the value of it via the `get_cf_value` filter due to quirks of the Django Template Language:
|
||||
|
||||
```jinja
|
||||
"invoices/{{ custom_fields|get_cf_value('Invoice Number') }}"
|
||||
```
|
||||
|
||||
You can also use a custom `datetime` filter to format dates:
|
||||
|
||||
```jinja
|
||||
invoices/
|
||||
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%Y') }}/
|
||||
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%m') }}/
|
||||
{{ custom_fields|get_cf_value("Date Field","2024-01-01")|datetime('%d') }}/
|
||||
Invoice_{{ custom_fields|get_cf_value("Select Field") }}_{{ custom_fields|get_cf_value("Date Field","2024-01-01")|replace("-", "") }}.pdf
|
||||
```
|
||||
|
||||
This will create a path like `invoices/2022/01/01/Invoice_OptionTwo_20220101.pdf` if the custom field "Date Field" is set to January 1, 2022 and "Select Field" is set to `OptionTwo`.
|
||||
|
||||
## Automatic recovery of invalid PDFs {#pdf-recovery}
|
||||
|
||||
Paperless will attempt to "clean" certain invalid PDFs with `qpdf` before processing if, for example, the mime_type
|
||||
detection is incorrect. This can happen if the PDF is not properly formatted or contains errors.
|
||||
|
||||
## Celery Monitoring {#celery-monitoring}
|
||||
|
||||
The monitoring tool
|
||||
@@ -687,4 +784,59 @@ 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.
|
||||
|
||||
## Decryption of encrypted emails before consumption {#gpg-decryptor}
|
||||
|
||||
Paperless-ngx can be configured to decrypt gpg encrypted emails before consumption.
|
||||
|
||||
### Requirements
|
||||
|
||||
You need a recent version of `gpg-agent >= 2.1.1` installed on your host.
|
||||
Your host needs to be setup for decrypting your emails via `gpg-agent`, see this [tutorial](https://www.digitalocean.com/community/tutorials/how-to-use-gpg-to-encrypt-and-sign-messages#encrypt-and-decrypt-messages-with-gpg) for instance.
|
||||
Test your setup and make sure that you can encrypt and decrypt files using your key
|
||||
|
||||
```
|
||||
gpg --encrypt --armor -r person@email.com name_of_file
|
||||
gpg --decrypt name_of_file.asc
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
First, enable the [PAPERLESS_ENABLE_GPG_DECRYPTOR environment variable](configuration.md#PAPERLESS_ENABLE_GPG_DECRYPTOR).
|
||||
|
||||
Then determine your local `gpg-agent.extra` socket by invoking
|
||||
|
||||
```
|
||||
gpgconf --list-dir agent-extra-socket
|
||||
```
|
||||
|
||||
on your host. A possible output is `~/.gnupg/S.gpg-agent.extra`.
|
||||
Also find the location of your public keyring.
|
||||
|
||||
If using docker, you'll need to add the following volume mounts to your `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
webserver:
|
||||
volumes:
|
||||
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
|
||||
- <path to gpg-agent.extra socket>:/usr/src/paperless/.gnupg/S.gpg-agent
|
||||
```
|
||||
|
||||
For a 'bare-metal' installation no further configuration is necessary. If you
|
||||
want to use a separate `GNUPG_HOME`, you can do so by configuring the [PAPERLESS_EMAIL_GNUPG_HOME environment variable](configuration.md#PAPERLESS_EMAIL_GNUPG_HOME).
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- Make sure, that `gpg-agent` is running on your host machine
|
||||
- Make sure, that encryption and decryption works from inside the container using the `gpg` commands from above.
|
||||
- Check that all files in `/usr/src/paperless/.gnupg` have correct permissions
|
||||
|
||||
```shell
|
||||
paperless@9da1865df327:~/.gnupg$ ls -al
|
||||
drwx------ 1 paperless paperless 4096 Aug 18 17:52 .
|
||||
drwxr-xr-x 1 paperless paperless 4096 Aug 18 17:52 ..
|
||||
srw------- 1 paperless paperless 0 Aug 18 17:22 S.gpg-agent
|
||||
-rw------- 1 paperless paperless 147940 Jul 24 10:23 pubring.gpg
|
||||
```
|
||||
|
57
docs/api.md
57
docs/api.md
@@ -54,6 +54,7 @@ fields:
|
||||
- `archived_file_name`: Verbose filename of the archived document.
|
||||
Read-only. Null if no archived document is available.
|
||||
- `notes`: Array of notes associated with the document.
|
||||
- `page_count`: Number of pages.
|
||||
- `set_permissions`: Allows setting document permissions. Optional,
|
||||
write-only. See [below](#permissions).
|
||||
- `custom_fields`: Array of custom fields & values, specified as
|
||||
@@ -235,12 +236,6 @@ results:
|
||||
Pagination works exactly the same as it does for normal requests on this
|
||||
endpoint.
|
||||
|
||||
Certain limitations apply to full text queries:
|
||||
|
||||
- Results are always sorted by search score. The results matching the
|
||||
query best will show up first.
|
||||
- Only a small subset of filtering parameters are supported.
|
||||
|
||||
Furthermore, each returned document has an additional `__search_hit__`
|
||||
attribute with various information about the search results:
|
||||
|
||||
@@ -280,6 +275,51 @@ attribute with various information about the search results:
|
||||
- `rank` is the index of the search results. The first result will
|
||||
have rank 0.
|
||||
|
||||
### Filtering by custom fields
|
||||
|
||||
You can filter documents by their custom field values by specifying the
|
||||
`custom_field_query` query parameter. Here are some recipes for common
|
||||
use cases:
|
||||
|
||||
1. Documents with a custom field "due" (date) between Aug 1, 2024 and
|
||||
Sept 1, 2024 (inclusive):
|
||||
|
||||
`?custom_field_query=["due", "range", ["2024-08-01", "2024-09-01"]]`
|
||||
|
||||
2. Documents with a custom field "customer" (text) that equals "bob"
|
||||
(case sensitive):
|
||||
|
||||
`?custom_field_query=["customer", "exact", "bob"]`
|
||||
|
||||
3. Documents with a custom field "answered" (boolean) set to `true`:
|
||||
|
||||
`?custom_field_query=["answered", "exact", true]`
|
||||
|
||||
4. Documents with a custom field "favorite animal" (select) set to either
|
||||
"cat" or "dog":
|
||||
|
||||
`?custom_field_query=["favorite animal", "in", ["cat", "dog"]]`
|
||||
|
||||
5. Documents with a custom field "address" (text) that is empty:
|
||||
|
||||
`?custom_field_query=["OR", ["address", "isnull", true], ["address", "exact", ""]]`
|
||||
|
||||
6. Documents that don't have a field called "foo":
|
||||
|
||||
`?custom_field_query=["foo", "exists", false]`
|
||||
|
||||
7. Documents that have document links "references" to both document 3 and 7:
|
||||
|
||||
`?custom_field_query=["references", "contains", [3, 7]]`
|
||||
|
||||
All field types support basic operations including `exact`, `in`, `isnull`,
|
||||
and `exists`. String, URL, and monetary fields support case-insensitive
|
||||
substring matching operations including `icontains`, `istartswith`, and
|
||||
`iendswith`. Integer, float, and date fields support arithmetic comparisons
|
||||
including `gt` (>), `gte` (>=), `lt` (<), `lte` (<=), and `range`.
|
||||
Lastly, document link fields support a `contains` operator that behaves
|
||||
like a "is superset of" check.
|
||||
|
||||
### `/api/search/autocomplete/`
|
||||
|
||||
Get auto completions for a partial search term.
|
||||
@@ -417,9 +457,14 @@ The following methods are supported:
|
||||
- The ordering of the merged document is determined by the list of IDs.
|
||||
- Optional `parameters`:
|
||||
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
|
||||
- `"delete_originals": true` to delete the original documents. This requires the calling user being the owner of
|
||||
all documents that are merged.
|
||||
- `split`
|
||||
- Requires `parameters`:
|
||||
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
|
||||
- Optional `parameters`:
|
||||
- `"delete_originals": true` to delete the original document after consumption. This requires the calling user being the owner of
|
||||
the document.
|
||||
- The split operation only accepts a single document.
|
||||
- `rotate`
|
||||
- Requires `parameters`:
|
||||
|
@@ -1,5 +1,523 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.12.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: wait to apply tag changes until other changes saved with multiple workflow actions [@shamoon](https://github.com/shamoon) ([#7711](https://github.com/paperless-ngx/paperless-ngx/pull/7711))
|
||||
- Fix: delete_pages should require ownership (not just change perms) [@shamoon](https://github.com/shamoon) ([#7714](https://github.com/paperless-ngx/paperless-ngx/pull/7714))
|
||||
- Fix: filter out shown custom fields that have been deleted from saved… [@shamoon](https://github.com/shamoon) ([#7710](https://github.com/paperless-ngx/paperless-ngx/pull/7710))
|
||||
- Fix: only filter by string or number properties for filter pipe [@shamoon](https://github.com/shamoon) ([#7699](https://github.com/paperless-ngx/paperless-ngx/pull/7699))
|
||||
- Fix: saved view permissions fixes [@shamoon](https://github.com/shamoon) ([#7672](https://github.com/paperless-ngx/paperless-ngx/pull/7672))
|
||||
- Fix: add permissions for OPTIONS requests for notes [@shamoon](https://github.com/shamoon) ([#7661](https://github.com/paperless-ngx/paperless-ngx/pull/7661))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>7 changes</summary>
|
||||
|
||||
- Fix: wait to apply tag changes until other changes saved with multiple workflow actions [@shamoon](https://github.com/shamoon) ([#7711](https://github.com/paperless-ngx/paperless-ngx/pull/7711))
|
||||
- Fix: delete_pages should require ownership (not just change perms) [@shamoon](https://github.com/shamoon) ([#7714](https://github.com/paperless-ngx/paperless-ngx/pull/7714))
|
||||
- Enhancement: improve text contrast for selected documents in list view dark mode [@shamoon](https://github.com/shamoon) ([#7712](https://github.com/paperless-ngx/paperless-ngx/pull/7712))
|
||||
- Fix: filter out shown custom fields that have been deleted from saved… [@shamoon](https://github.com/shamoon) ([#7710](https://github.com/paperless-ngx/paperless-ngx/pull/7710))
|
||||
- Fix: only filter by string or number properties for filter pipe [@shamoon](https://github.com/shamoon) ([#7699](https://github.com/paperless-ngx/paperless-ngx/pull/7699))
|
||||
- Fix: saved view permissions fixes [@shamoon](https://github.com/shamoon) ([#7672](https://github.com/paperless-ngx/paperless-ngx/pull/7672))
|
||||
- Fix: add permissions for OPTIONS requests for notes [@shamoon](https://github.com/shamoon) ([#7661](https://github.com/paperless-ngx/paperless-ngx/pull/7661))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.12.0
|
||||
|
||||
### Features / Enhancements
|
||||
|
||||
- Enhancement: re-work mail rule dialog, support multiple include patterns [@shamoon](https://github.com/shamoon) ([#7635](https://github.com/paperless-ngx/paperless-ngx/pull/7635))
|
||||
- Enhancement: add Korean language [@shamoon](https://github.com/shamoon) ([#7573](https://github.com/paperless-ngx/paperless-ngx/pull/7573))
|
||||
- Enhancement: allow multiple filename attachment exclusion patterns for a mail rule [@MelleD](https://github.com/MelleD) ([#5524](https://github.com/paperless-ngx/paperless-ngx/pull/5524))
|
||||
- Refactor: Use django-filter logic for filtering full text search queries [@yichi-yang](https://github.com/yichi-yang) ([#7507](https://github.com/paperless-ngx/paperless-ngx/pull/7507))
|
||||
- Refactor: Reduce number of SQL queries when serializing List[Document] [@yichi-yang](https://github.com/yichi-yang) ([#7505](https://github.com/paperless-ngx/paperless-ngx/pull/7505))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: use JSON for note audit log entries [@shamoon](https://github.com/shamoon) ([#7650](https://github.com/paperless-ngx/paperless-ngx/pull/7650))
|
||||
- Fix: Rework system check so it won't crash if tesseract is not found [@stumpylog](https://github.com/stumpylog) ([#7640](https://github.com/paperless-ngx/paperless-ngx/pull/7640))
|
||||
- Fix: correct broken pdfjs worker src after upgrade to pdfjs v4 [@shamoon](https://github.com/shamoon) ([#7626](https://github.com/paperless-ngx/paperless-ngx/pull/7626))
|
||||
- Chore: remove unused frontend dependencies [@shamoon](https://github.com/shamoon) ([#7607](https://github.com/paperless-ngx/paperless-ngx/pull/7607))
|
||||
- Fix: fix non-clickable scroll wheel in file uploads list [@shamoon](https://github.com/shamoon) ([#7591](https://github.com/paperless-ngx/paperless-ngx/pull/7591))
|
||||
- Fix: deselect file tasks select all button on dismiss [@shamoon](https://github.com/shamoon) ([#7592](https://github.com/paperless-ngx/paperless-ngx/pull/7592))
|
||||
- Fix: saved view sidebar heading not always visible [@shamoon](https://github.com/shamoon) ([#7584](https://github.com/paperless-ngx/paperless-ngx/pull/7584))
|
||||
- Fix: correct select field wrapping with long text [@shamoon](https://github.com/shamoon) ([#7572](https://github.com/paperless-ngx/paperless-ngx/pull/7572))
|
||||
- Fix: update ng-bootstrap to fix datepicker bug [@shamoon](https://github.com/shamoon) ([#7567](https://github.com/paperless-ngx/paperless-ngx/pull/7567))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>11 changes</summary>
|
||||
|
||||
- Chore(deps): Bump cryptography from 42.0.8 to 43.0.1 [@dependabot](https://github.com/dependabot) ([#7620](https://github.com/paperless-ngx/paperless-ngx/pull/7620))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates [@dependabot](https://github.com/dependabot) ([#7608](https://github.com/paperless-ngx/paperless-ngx/pull/7608))
|
||||
- Chore(deps): Bump rapidfuzz from 3.9.6 to 3.9.7 in the small-changes group [@dependabot](https://github.com/dependabot) ([#7611](https://github.com/paperless-ngx/paperless-ngx/pull/7611))
|
||||
- Chore(deps): Bump tslib from 2.6.3 to 2.7.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#7606](https://github.com/paperless-ngx/paperless-ngx/pull/7606))
|
||||
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.45.3 to 1.46.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.45.3 to 1.46.1 in /src-ui @dependabot) ([#7603](https://github.com/paperless-ngx/paperless-ngx/pull/7603))
|
||||
- Chore(deps-dev): Bump typescript from 5.4.5 to 5.5.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#7604](https://github.com/paperless-ngx/paperless-ngx/pull/7604))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#7600](https://github.com/paperless-ngx/paperless-ngx/pull/7600))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 21 updates [@dependabot](https://github.com/dependabot) ([#7599](https://github.com/paperless-ngx/paperless-ngx/pull/7599))
|
||||
- Chore(deps): Bump pathvalidate from 3.2.0 to 3.2.1 in the small-changes group [@dependabot](https://github.com/dependabot) ([#7548](https://github.com/paperless-ngx/paperless-ngx/pull/7548))
|
||||
- Chore(deps): Bump micromatch from 4.0.5 to 4.0.8 in /src-ui [@dependabot](https://github.com/dependabot) ([#7551](https://github.com/paperless-ngx/paperless-ngx/pull/7551))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7545](https://github.com/paperless-ngx/paperless-ngx/pull/7545))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>27 changes</summary>
|
||||
|
||||
- Chore: Update backend dependencies in bulk [@stumpylog](https://github.com/stumpylog) ([#7656](https://github.com/paperless-ngx/paperless-ngx/pull/7656))
|
||||
- Fix: Rework system check so it won't crash if tesseract is not found [@stumpylog](https://github.com/stumpylog) ([#7640](https://github.com/paperless-ngx/paperless-ngx/pull/7640))
|
||||
- Refactor: performance and storage optimization of barcode scanning [@loewexy](https://github.com/loewexy) ([#7646](https://github.com/paperless-ngx/paperless-ngx/pull/7646))
|
||||
- Fix: use JSON for note audit log entries [@shamoon](https://github.com/shamoon) ([#7650](https://github.com/paperless-ngx/paperless-ngx/pull/7650))
|
||||
- Enhancement: re-work mail rule dialog, support multiple include patterns [@shamoon](https://github.com/shamoon) ([#7635](https://github.com/paperless-ngx/paperless-ngx/pull/7635))
|
||||
- Fix: correct broken pdfjs worker src after upgrade to pdfjs v4 [@shamoon](https://github.com/shamoon) ([#7626](https://github.com/paperless-ngx/paperless-ngx/pull/7626))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates [@dependabot](https://github.com/dependabot) ([#7608](https://github.com/paperless-ngx/paperless-ngx/pull/7608))
|
||||
- Chore(deps): Bump rapidfuzz from 3.9.6 to 3.9.7 in the small-changes group [@dependabot](https://github.com/dependabot) ([#7611](https://github.com/paperless-ngx/paperless-ngx/pull/7611))
|
||||
- Chore: remove unused frontend dependencies [@shamoon](https://github.com/shamoon) ([#7607](https://github.com/paperless-ngx/paperless-ngx/pull/7607))
|
||||
- Chore(deps): Bump tslib from 2.6.3 to 2.7.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#7606](https://github.com/paperless-ngx/paperless-ngx/pull/7606))
|
||||
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.45.3 to 1.46.1 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.45.3 to 1.46.1 in /src-ui @dependabot) ([#7603](https://github.com/paperless-ngx/paperless-ngx/pull/7603))
|
||||
- Chore(deps-dev): Bump typescript from 5.4.5 to 5.5.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#7604](https://github.com/paperless-ngx/paperless-ngx/pull/7604))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#7600](https://github.com/paperless-ngx/paperless-ngx/pull/7600))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 21 updates [@dependabot](https://github.com/dependabot) ([#7599](https://github.com/paperless-ngx/paperless-ngx/pull/7599))
|
||||
- Fix: fix non-clickable scroll wheel in file uploads list [@shamoon](https://github.com/shamoon) ([#7591](https://github.com/paperless-ngx/paperless-ngx/pull/7591))
|
||||
- Fix: deselect file tasks select all button on dismiss [@shamoon](https://github.com/shamoon) ([#7592](https://github.com/paperless-ngx/paperless-ngx/pull/7592))
|
||||
- Fix: saved view sidebar heading not always visible [@shamoon](https://github.com/shamoon) ([#7584](https://github.com/paperless-ngx/paperless-ngx/pull/7584))
|
||||
- Enhancement: add Korean language [@shamoon](https://github.com/shamoon) ([#7573](https://github.com/paperless-ngx/paperless-ngx/pull/7573))
|
||||
- Enhancement: mail message preprocessor for gpg encrypted mails [@dbankmann](https://github.com/dbankmann) ([#7456](https://github.com/paperless-ngx/paperless-ngx/pull/7456))
|
||||
- Fix: correct select field wrapping with long text [@shamoon](https://github.com/shamoon) ([#7572](https://github.com/paperless-ngx/paperless-ngx/pull/7572))
|
||||
- Fix: update ng-bootstrap to fix datepicker bug [@shamoon](https://github.com/shamoon) ([#7567](https://github.com/paperless-ngx/paperless-ngx/pull/7567))
|
||||
- Enhancement: allow multiple filename attachment exclusion patterns for a mail rule [@MelleD](https://github.com/MelleD) ([#5524](https://github.com/paperless-ngx/paperless-ngx/pull/5524))
|
||||
- Chore(deps): Bump pathvalidate from 3.2.0 to 3.2.1 in the small-changes group [@dependabot](https://github.com/dependabot) ([#7548](https://github.com/paperless-ngx/paperless-ngx/pull/7548))
|
||||
- Chore(deps): Bump micromatch from 4.0.5 to 4.0.8 in /src-ui [@dependabot](https://github.com/dependabot) ([#7551](https://github.com/paperless-ngx/paperless-ngx/pull/7551))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7545](https://github.com/paperless-ngx/paperless-ngx/pull/7545))
|
||||
- Refactor: Use django-filter logic for filtering full text search queries [@yichi-yang](https://github.com/yichi-yang) ([#7507](https://github.com/paperless-ngx/paperless-ngx/pull/7507))
|
||||
- Refactor: Reduce number of SQL queries when serializing List[Document] [@yichi-yang](https://github.com/yichi-yang) ([#7505](https://github.com/paperless-ngx/paperless-ngx/pull/7505))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.11.6
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: fix nltk tokenizer breaking change [@shamoon](https://github.com/shamoon) ([#7522](https://github.com/paperless-ngx/paperless-ngx/pull/7522))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>1 change</summary>
|
||||
|
||||
- Fix: fix nltk tokenizer breaking change [@shamoon](https://github.com/shamoon) ([#7522](https://github.com/paperless-ngx/paperless-ngx/pull/7522))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.11.5
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: use JSON for update archive file auditlog entries [@shamoon](https://github.com/shamoon) ([#7503](https://github.com/paperless-ngx/paperless-ngx/pull/7503))
|
||||
- Fix: respect deskew / rotate pages from AppConfig if set [@shamoon](https://github.com/shamoon) ([#7501](https://github.com/paperless-ngx/paperless-ngx/pull/7501))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 6 updates [@dependabot](https://github.com/dependabot) ([#7502](https://github.com/paperless-ngx/paperless-ngx/pull/7502))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7497](https://github.com/paperless-ngx/paperless-ngx/pull/7497))
|
||||
- Chore(deps-dev): Bump axios from 1.6.7 to 1.7.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#7472](https://github.com/paperless-ngx/paperless-ngx/pull/7472))
|
||||
- Chore(deps-dev): Bump ruff from 0.5.6 to 0.5.7 in the development group [@dependabot](https://github.com/dependabot) ([#7457](https://github.com/paperless-ngx/paperless-ngx/pull/7457))
|
||||
- Chore(deps): Bump the small-changes group with 3 updates [@dependabot](https://github.com/dependabot) ([#7460](https://github.com/paperless-ngx/paperless-ngx/pull/7460))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>7 changes</summary>
|
||||
|
||||
- Fix: use JSON for update archive file auditlog entries [@shamoon](https://github.com/shamoon) ([#7503](https://github.com/paperless-ngx/paperless-ngx/pull/7503))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 6 updates [@dependabot](https://github.com/dependabot) ([#7502](https://github.com/paperless-ngx/paperless-ngx/pull/7502))
|
||||
- Fix: respect deskew / rotate pages from AppConfig if set [@shamoon](https://github.com/shamoon) ([#7501](https://github.com/paperless-ngx/paperless-ngx/pull/7501))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7497](https://github.com/paperless-ngx/paperless-ngx/pull/7497))
|
||||
- Chore(deps-dev): Bump axios from 1.6.7 to 1.7.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#7472](https://github.com/paperless-ngx/paperless-ngx/pull/7472))
|
||||
- Chore(deps-dev): Bump ruff from 0.5.6 to 0.5.7 in the development group [@dependabot](https://github.com/dependabot) ([#7457](https://github.com/paperless-ngx/paperless-ngx/pull/7457))
|
||||
- Chore(deps): Bump the small-changes group with 3 updates [@dependabot](https://github.com/dependabot) ([#7460](https://github.com/paperless-ngx/paperless-ngx/pull/7460))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.11.4
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: initial upload message not being dismissed [@shamoon](https://github.com/shamoon) ([#7438](https://github.com/paperless-ngx/paperless-ngx/pull/7438))
|
||||
|
||||
### All App Changes
|
||||
|
||||
- Fix: initial upload message not being dismissed [@shamoon](https://github.com/shamoon) ([#7438](https://github.com/paperless-ngx/paperless-ngx/pull/7438))
|
||||
|
||||
## paperless-ngx 2.11.3
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: optimize tasks / stats reload [@shamoon](https://github.com/shamoon) ([#7402](https://github.com/paperless-ngx/paperless-ngx/pull/7402))
|
||||
- Enhancement: allow specifying default currency for Monetary custom field [@shamoon](https://github.com/shamoon) ([#7381](https://github.com/paperless-ngx/paperless-ngx/pull/7381))
|
||||
- Enhancement: specify when pre-check fails for documents in trash [@shamoon](https://github.com/shamoon) ([#7355](https://github.com/paperless-ngx/paperless-ngx/pull/7355))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: clear selection after reload for management lists [@shamoon](https://github.com/shamoon) ([#7421](https://github.com/paperless-ngx/paperless-ngx/pull/7421))
|
||||
- Fix: disable inline create buttons if insufficient permissions [@shamoon](https://github.com/shamoon) ([#7401](https://github.com/paperless-ngx/paperless-ngx/pull/7401))
|
||||
- Fix: use entire document for dropzone [@shamoon](https://github.com/shamoon) ([#7342](https://github.com/paperless-ngx/paperless-ngx/pull/7342))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore(deps): Bump stumpylog/image-cleaner-action from 0.7.0 to 0.8.0 in the actions group [@dependabot](https://github.com/dependabot) ([#7371](https://github.com/paperless-ngx/paperless-ngx/pull/7371))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>11 changes</summary>
|
||||
|
||||
- Chore(deps): Bump django from 4.2.14 to 4.2.15 [@dependabot](https://github.com/dependabot) ([#7412](https://github.com/paperless-ngx/paperless-ngx/pull/7412))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates [@dependabot](https://github.com/dependabot) ([#7394](https://github.com/paperless-ngx/paperless-ngx/pull/7394))
|
||||
- Chore(deps): Bump the small-changes group with 5 updates [@dependabot](https://github.com/dependabot) ([#7397](https://github.com/paperless-ngx/paperless-ngx/pull/7397))
|
||||
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.42.1 to 1.45.3 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.42.1 to 1.45.3 in /src-ui @dependabot) ([#7367](https://github.com/paperless-ngx/paperless-ngx/pull/7367))
|
||||
- Chore(deps-dev): Bump [@<!---->types/node from 20.12.2 to 22.0.2 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.12.2 to 22.0.2 in /src-ui @dependabot) ([#7366](https://github.com/paperless-ngx/paperless-ngx/pull/7366))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#7365](https://github.com/paperless-ngx/paperless-ngx/pull/7365))
|
||||
- Chore(deps): Bump uuid from 9.0.1 to 10.0.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#7370](https://github.com/paperless-ngx/paperless-ngx/pull/7370))
|
||||
- Chore(deps): Bump stumpylog/image-cleaner-action from 0.7.0 to 0.8.0 in the actions group [@dependabot](https://github.com/dependabot) ([#7371](https://github.com/paperless-ngx/paperless-ngx/pull/7371))
|
||||
- Chore(deps): Bump zone.js from 0.14.4 to 0.14.8 in /src-ui [@dependabot](https://github.com/dependabot) ([#7368](https://github.com/paperless-ngx/paperless-ngx/pull/7368))
|
||||
- Chore(deps-dev): Bump jest-preset-angular from 14.1.1 to 14.2.2 in /src-ui in the frontend-jest-dependencies group [@dependabot](https://github.com/dependabot) ([#7364](https://github.com/paperless-ngx/paperless-ngx/pull/7364))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates [@dependabot](https://github.com/dependabot) ([#7363](https://github.com/paperless-ngx/paperless-ngx/pull/7363))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>15 changes</summary>
|
||||
|
||||
- Fix: clear selection after reload for management lists [@shamoon](https://github.com/shamoon) ([#7421](https://github.com/paperless-ngx/paperless-ngx/pull/7421))
|
||||
- Enhancement: optimize tasks / stats reload [@shamoon](https://github.com/shamoon) ([#7402](https://github.com/paperless-ngx/paperless-ngx/pull/7402))
|
||||
- Enhancement: allow specifying default currency for Monetary custom field [@shamoon](https://github.com/shamoon) ([#7381](https://github.com/paperless-ngx/paperless-ngx/pull/7381))
|
||||
- Enhancement: specify when pre-check fails for documents in trash [@shamoon](https://github.com/shamoon) ([#7355](https://github.com/paperless-ngx/paperless-ngx/pull/7355))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates [@dependabot](https://github.com/dependabot) ([#7394](https://github.com/paperless-ngx/paperless-ngx/pull/7394))
|
||||
- Fix: disable inline create buttons if insufficient permissions [@shamoon](https://github.com/shamoon) ([#7401](https://github.com/paperless-ngx/paperless-ngx/pull/7401))
|
||||
- Chore(deps): Bump the small-changes group with 5 updates [@dependabot](https://github.com/dependabot) ([#7397](https://github.com/paperless-ngx/paperless-ngx/pull/7397))
|
||||
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.42.1 to 1.45.3 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.42.1 to 1.45.3 in /src-ui @dependabot) ([#7367](https://github.com/paperless-ngx/paperless-ngx/pull/7367))
|
||||
- Chore(deps-dev): Bump [@<!---->types/node from 20.12.2 to 22.0.2 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.12.2 to 22.0.2 in /src-ui @dependabot) ([#7366](https://github.com/paperless-ngx/paperless-ngx/pull/7366))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates [@dependabot](https://github.com/dependabot) ([#7365](https://github.com/paperless-ngx/paperless-ngx/pull/7365))
|
||||
- Chore(deps): Bump uuid from 9.0.1 to 10.0.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#7370](https://github.com/paperless-ngx/paperless-ngx/pull/7370))
|
||||
- Chore(deps): Bump zone.js from 0.14.4 to 0.14.8 in /src-ui [@dependabot](https://github.com/dependabot) ([#7368](https://github.com/paperless-ngx/paperless-ngx/pull/7368))
|
||||
- Chore(deps-dev): Bump jest-preset-angular from 14.1.1 to 14.2.2 in /src-ui in the frontend-jest-dependencies group [@dependabot](https://github.com/dependabot) ([#7364](https://github.com/paperless-ngx/paperless-ngx/pull/7364))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates [@dependabot](https://github.com/dependabot) ([#7363](https://github.com/paperless-ngx/paperless-ngx/pull/7363))
|
||||
- Fix: use entire document for dropzone [@shamoon](https://github.com/shamoon) ([#7342](https://github.com/paperless-ngx/paperless-ngx/pull/7342))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.11.2
|
||||
|
||||
### Changes
|
||||
|
||||
- Change: more clearly handle init permissions error [@shamoon](https://github.com/shamoon) ([#7334](https://github.com/paperless-ngx/paperless-ngx/pull/7334))
|
||||
- Chore: add permissions info link from webUI [@shamoon](https://github.com/shamoon) ([#7310](https://github.com/paperless-ngx/paperless-ngx/pull/7310))
|
||||
- Fix: increase search input text contrast with light custom theme colors [@JayBkr](https://github.com/JayBkr) ([#7303](https://github.com/paperless-ngx/paperless-ngx/pull/7303))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7296](https://github.com/paperless-ngx/paperless-ngx/pull/7296))
|
||||
- Chore(deps): Bump tika-client from 0.5.0 to 0.6.0 in the small-changes group [@dependabot](https://github.com/dependabot) ([#7297](https://github.com/paperless-ngx/paperless-ngx/pull/7297))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Change: more clearly handle init permissions error [@shamoon](https://github.com/shamoon) ([#7334](https://github.com/paperless-ngx/paperless-ngx/pull/7334))
|
||||
- Chore: add permissions info link from webUI [@shamoon](https://github.com/shamoon) ([#7310](https://github.com/paperless-ngx/paperless-ngx/pull/7310))
|
||||
- Fix: increase search input text contrast with light custom theme colors [@JayBkr](https://github.com/JayBkr) ([#7303](https://github.com/paperless-ngx/paperless-ngx/pull/7303))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7296](https://github.com/paperless-ngx/paperless-ngx/pull/7296))
|
||||
- Chore(deps): Bump tika-client from 0.5.0 to 0.6.0 in the small-changes group [@dependabot](https://github.com/dependabot) ([#7297](https://github.com/paperless-ngx/paperless-ngx/pull/7297))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.11.1
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: include owner username in post-consumption variables [@Freddy-0](https://github.com/Freddy-0) ([#7270](https://github.com/paperless-ngx/paperless-ngx/pull/7270))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: support multiple inbox tags from stats widget [@shamoon](https://github.com/shamoon) ([#7281](https://github.com/paperless-ngx/paperless-ngx/pull/7281))
|
||||
- Fix: Removes Turkish from the NLTK languages [@stumpylog](https://github.com/stumpylog) ([#7246](https://github.com/paperless-ngx/paperless-ngx/pull/7246))
|
||||
- Fix: include trashed docs in existing doc check [@shamoon](https://github.com/shamoon) ([#7229](https://github.com/paperless-ngx/paperless-ngx/pull/7229))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7261](https://github.com/paperless-ngx/paperless-ngx/pull/7261))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 2 updates [@dependabot](https://github.com/dependabot) ([#7266](https://github.com/paperless-ngx/paperless-ngx/pull/7266))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>7 changes</summary>
|
||||
|
||||
- Fix: support multiple inbox tags from stats widget [@shamoon](https://github.com/shamoon) ([#7281](https://github.com/paperless-ngx/paperless-ngx/pull/7281))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7261](https://github.com/paperless-ngx/paperless-ngx/pull/7261))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 2 updates [@dependabot](https://github.com/dependabot) ([#7266](https://github.com/paperless-ngx/paperless-ngx/pull/7266))
|
||||
- Enhancement: include owner username in post-consumption variables [@Freddy-0](https://github.com/Freddy-0) ([#7270](https://github.com/paperless-ngx/paperless-ngx/pull/7270))
|
||||
- Chore: Squash older automatic migrations [@stumpylog](https://github.com/stumpylog) ([#7267](https://github.com/paperless-ngx/paperless-ngx/pull/7267))
|
||||
- Fix: Removes Turkish from the NLTK languages [@stumpylog](https://github.com/stumpylog) ([#7246](https://github.com/paperless-ngx/paperless-ngx/pull/7246))
|
||||
- Fix: include trashed docs in existing doc check [@shamoon](https://github.com/shamoon) ([#7229](https://github.com/paperless-ngx/paperless-ngx/pull/7229))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.11.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Feature: Upgrade Gotenberg to v8 [@stumpylog](https://github.com/stumpylog) ([#7094](https://github.com/paperless-ngx/paperless-ngx/pull/7094))
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: disable add split button when appropriate [@shamoon](https://github.com/shamoon) ([#7215](https://github.com/paperless-ngx/paperless-ngx/pull/7215))
|
||||
- Enhancement: wrapping of saved view fields d-n-d UI [@shamoon](https://github.com/shamoon) ([#7216](https://github.com/paperless-ngx/paperless-ngx/pull/7216))
|
||||
- Enhancement: support custom field icontains filter for select type [@shamoon](https://github.com/shamoon) ([#7199](https://github.com/paperless-ngx/paperless-ngx/pull/7199))
|
||||
- Feature: select custom field type [@shamoon](https://github.com/shamoon) ([#7167](https://github.com/paperless-ngx/paperless-ngx/pull/7167))
|
||||
- Feature: automatic sso redirect [@shamoon](https://github.com/shamoon) ([#7168](https://github.com/paperless-ngx/paperless-ngx/pull/7168))
|
||||
- Enhancement: show more columns in mail frontend admin [@shamoon](https://github.com/shamoon) ([#7158](https://github.com/paperless-ngx/paperless-ngx/pull/7158))
|
||||
- Enhancement: use request user as owner of split / merge docs [@shamoon](https://github.com/shamoon) ([#7112](https://github.com/paperless-ngx/paperless-ngx/pull/7112))
|
||||
- Enhancement: improve date parsing with accented characters [@fdubuy](https://github.com/fdubuy) ([#7100](https://github.com/paperless-ngx/paperless-ngx/pull/7100))
|
||||
- Feature: improve history display of object names etc [@shamoon](https://github.com/shamoon) ([#7102](https://github.com/paperless-ngx/paperless-ngx/pull/7102))
|
||||
- Feature: Upgrade Gotenberg to v8 [@stumpylog](https://github.com/stumpylog) ([#7094](https://github.com/paperless-ngx/paperless-ngx/pull/7094))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: include documents in trash for existing asn check [@shamoon](https://github.com/shamoon) ([#7189](https://github.com/paperless-ngx/paperless-ngx/pull/7189))
|
||||
- Fix: include documents in trash in sanity check [@shamoon](https://github.com/shamoon) ([#7133](https://github.com/paperless-ngx/paperless-ngx/pull/7133))
|
||||
- Fix: handle errors for trash actions and only show documents user can restore or delete [@shamoon](https://github.com/shamoon) ([#7119](https://github.com/paperless-ngx/paperless-ngx/pull/7119))
|
||||
- Fix: dont include documents in trash in counts [@shamoon](https://github.com/shamoon) ([#7111](https://github.com/paperless-ngx/paperless-ngx/pull/7111))
|
||||
- Fix: use temp dir for split / merge [@shamoon](https://github.com/shamoon) ([#7105](https://github.com/paperless-ngx/paperless-ngx/pull/7105))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: upgrade to DRF 3.15 [@shamoon](https://github.com/shamoon) ([#7134](https://github.com/paperless-ngx/paperless-ngx/pull/7134))
|
||||
- Chore(deps): Bump docker/build-push-action from 5 to 6 in the actions group [@dependabot](https://github.com/dependabot) ([#7125](https://github.com/paperless-ngx/paperless-ngx/pull/7125))
|
||||
- Chore: Ignores DRF 3.15.2 [@stumpylog](https://github.com/stumpylog) ([#7122](https://github.com/paperless-ngx/paperless-ngx/pull/7122))
|
||||
- Chore: show docker tag in UI for ci test builds [@shamoon](https://github.com/shamoon) ([#7083](https://github.com/paperless-ngx/paperless-ngx/pull/7083))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>11 changes</summary>
|
||||
|
||||
- Chore: Bulk backend updates [@stumpylog](https://github.com/stumpylog) ([#7209](https://github.com/paperless-ngx/paperless-ngx/pull/7209))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 14 updates [@dependabot](https://github.com/dependabot) ([#7200](https://github.com/paperless-ngx/paperless-ngx/pull/7200))
|
||||
- Chore(deps): Bump certifi from 2024.6.2 to 2024.7.4 [@dependabot](https://github.com/dependabot) ([#7166](https://github.com/paperless-ngx/paperless-ngx/pull/7166))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 6 updates [@dependabot](https://github.com/dependabot) ([#7148](https://github.com/paperless-ngx/paperless-ngx/pull/7148))
|
||||
- Chore(deps): Bump django-multiselectfield from 0.1.12 to 0.1.13 in the django group [@dependabot](https://github.com/dependabot) ([#7147](https://github.com/paperless-ngx/paperless-ngx/pull/7147))
|
||||
- Chore(deps): Bump docker/build-push-action from 5 to 6 in the actions group [@dependabot](https://github.com/dependabot) ([#7125](https://github.com/paperless-ngx/paperless-ngx/pull/7125))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 4 updates [@dependabot](https://github.com/dependabot) ([#7128](https://github.com/paperless-ngx/paperless-ngx/pull/7128))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#7126](https://github.com/paperless-ngx/paperless-ngx/pull/7126))
|
||||
- Chore(deps-dev): Bump ruff from 0.4.9 to 0.5.0 in the development group across 1 directory [@dependabot](https://github.com/dependabot) ([#7120](https://github.com/paperless-ngx/paperless-ngx/pull/7120))
|
||||
- Chore(deps-dev): Bump ws from 8.17.0 to 8.17.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#7114](https://github.com/paperless-ngx/paperless-ngx/pull/7114))
|
||||
- Chore: update to Angular v18 [@shamoon](https://github.com/shamoon) ([#7106](https://github.com/paperless-ngx/paperless-ngx/pull/7106))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>25 changes</summary>
|
||||
|
||||
- Enhancement: disable add split button when appropriate [@shamoon](https://github.com/shamoon) ([#7215](https://github.com/paperless-ngx/paperless-ngx/pull/7215))
|
||||
- Enhancement: wrapping of saved view fields d-n-d UI [@shamoon](https://github.com/shamoon) ([#7216](https://github.com/paperless-ngx/paperless-ngx/pull/7216))
|
||||
- Chore: Bulk backend updates [@stumpylog](https://github.com/stumpylog) ([#7209](https://github.com/paperless-ngx/paperless-ngx/pull/7209))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 14 updates [@dependabot](https://github.com/dependabot) ([#7200](https://github.com/paperless-ngx/paperless-ngx/pull/7200))
|
||||
- Enhancement: support custom field icontains filter for select type [@shamoon](https://github.com/shamoon) ([#7199](https://github.com/paperless-ngx/paperless-ngx/pull/7199))
|
||||
- Chore: upgrade to DRF 3.15 [@shamoon](https://github.com/shamoon) ([#7134](https://github.com/paperless-ngx/paperless-ngx/pull/7134))
|
||||
- Feature: select custom field type [@shamoon](https://github.com/shamoon) ([#7167](https://github.com/paperless-ngx/paperless-ngx/pull/7167))
|
||||
- Feature: automatic sso redirect [@shamoon](https://github.com/shamoon) ([#7168](https://github.com/paperless-ngx/paperless-ngx/pull/7168))
|
||||
- Fix: include documents in trash for existing asn check [@shamoon](https://github.com/shamoon) ([#7189](https://github.com/paperless-ngx/paperless-ngx/pull/7189))
|
||||
- Chore: Initial conversion to pytest fixtures [@stumpylog](https://github.com/stumpylog) ([#7110](https://github.com/paperless-ngx/paperless-ngx/pull/7110))
|
||||
- Enhancement: show more columns in mail frontend admin [@shamoon](https://github.com/shamoon) ([#7158](https://github.com/paperless-ngx/paperless-ngx/pull/7158))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 6 updates [@dependabot](https://github.com/dependabot) ([#7148](https://github.com/paperless-ngx/paperless-ngx/pull/7148))
|
||||
- Chore(deps): Bump django-multiselectfield from 0.1.12 to 0.1.13 in the django group [@dependabot](https://github.com/dependabot) ([#7147](https://github.com/paperless-ngx/paperless-ngx/pull/7147))
|
||||
- Fix: include documents in trash in sanity check [@shamoon](https://github.com/shamoon) ([#7133](https://github.com/paperless-ngx/paperless-ngx/pull/7133))
|
||||
- Chore(deps): Bump the small-changes group across 1 directory with 4 updates [@dependabot](https://github.com/dependabot) ([#7128](https://github.com/paperless-ngx/paperless-ngx/pull/7128))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 16 updates [@dependabot](https://github.com/dependabot) ([#7126](https://github.com/paperless-ngx/paperless-ngx/pull/7126))
|
||||
- Enhancement: use request user as owner of split / merge docs [@shamoon](https://github.com/shamoon) ([#7112](https://github.com/paperless-ngx/paperless-ngx/pull/7112))
|
||||
- Fix: handle errors for trash actions and only show documents user can restore or delete [@shamoon](https://github.com/shamoon) ([#7119](https://github.com/paperless-ngx/paperless-ngx/pull/7119))
|
||||
- Chore(deps-dev): Bump ruff from 0.4.9 to 0.5.0 in the development group across 1 directory [@dependabot](https://github.com/dependabot) ([#7120](https://github.com/paperless-ngx/paperless-ngx/pull/7120))
|
||||
- Chore(deps-dev): Bump ws from 8.17.0 to 8.17.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#7114](https://github.com/paperless-ngx/paperless-ngx/pull/7114))
|
||||
- Chore: update to Angular v18 [@shamoon](https://github.com/shamoon) ([#7106](https://github.com/paperless-ngx/paperless-ngx/pull/7106))
|
||||
- Enhancement: improve date parsing with accented characters [@fdubuy](https://github.com/fdubuy) ([#7100](https://github.com/paperless-ngx/paperless-ngx/pull/7100))
|
||||
- Feature: improve history display of object names etc [@shamoon](https://github.com/shamoon) ([#7102](https://github.com/paperless-ngx/paperless-ngx/pull/7102))
|
||||
- Fix: dont include documents in trash in counts [@shamoon](https://github.com/shamoon) ([#7111](https://github.com/paperless-ngx/paperless-ngx/pull/7111))
|
||||
- Fix: use temp dir for split / merge [@shamoon](https://github.com/shamoon) ([#7105](https://github.com/paperless-ngx/paperless-ngx/pull/7105))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.10.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: always update document modified property on bulk edit operations [@shamoon](https://github.com/shamoon) ([#7079](https://github.com/paperless-ngx/paperless-ngx/pull/7079))
|
||||
- Fix: correct frontend retrieval of trash delay setting [@shamoon](https://github.com/shamoon) ([#7067](https://github.com/paperless-ngx/paperless-ngx/pull/7067))
|
||||
- Fix: index fresh document data after update archive file [@shamoon](https://github.com/shamoon) ([#7057](https://github.com/paperless-ngx/paperless-ngx/pull/7057))
|
||||
- Fix: Safari browser PDF viewer not loading in 2.10.x [@shamoon](https://github.com/shamoon) ([#7056](https://github.com/paperless-ngx/paperless-ngx/pull/7056))
|
||||
- Fix: Prefer the exporter metadata JSON file over the version JSON file [@stumpylog](https://github.com/stumpylog) ([#7048](https://github.com/paperless-ngx/paperless-ngx/pull/7048))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Fix: always update document modified property on bulk edit operations [@shamoon](https://github.com/shamoon) ([#7079](https://github.com/paperless-ngx/paperless-ngx/pull/7079))
|
||||
- Fix: correct frontend retrieval of trash delay setting [@shamoon](https://github.com/shamoon) ([#7067](https://github.com/paperless-ngx/paperless-ngx/pull/7067))
|
||||
- Fix: index fresh document data after update archive file [@shamoon](https://github.com/shamoon) ([#7057](https://github.com/paperless-ngx/paperless-ngx/pull/7057))
|
||||
- Fix: Safari browser PDF viewer not loading in 2.10.x [@shamoon](https://github.com/shamoon) ([#7056](https://github.com/paperless-ngx/paperless-ngx/pull/7056))
|
||||
- Fix: Prefer the exporter metadata JSON file over the version JSON file [@stumpylog](https://github.com/stumpylog) ([#7048](https://github.com/paperless-ngx/paperless-ngx/pull/7048))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.10.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: dont require admin perms to view trash on frontend @shamoon ([#7028](https://github.com/paperless-ngx/paperless-ngx/pull/7028))
|
||||
|
||||
## paperless-ngx 2.10.0
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: documents trash aka soft delete [@shamoon](https://github.com/shamoon) ([#6944](https://github.com/paperless-ngx/paperless-ngx/pull/6944))
|
||||
- Enhancement: better boolean custom field display [@shamoon](https://github.com/shamoon) ([#7001](https://github.com/paperless-ngx/paperless-ngx/pull/7001))
|
||||
- Feature: Allow encrypting sensitive fields in export [@stumpylog](https://github.com/stumpylog) ([#6927](https://github.com/paperless-ngx/paperless-ngx/pull/6927))
|
||||
- Enhancement: allow consumption of odg files [@daniel-boehme](https://github.com/daniel-boehme) ([#6940](https://github.com/paperless-ngx/paperless-ngx/pull/6940))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Document history could include extra fields [@stumpylog](https://github.com/stumpylog) ([#6989](https://github.com/paperless-ngx/paperless-ngx/pull/6989))
|
||||
- Fix: use local pdf worker js [@shamoon](https://github.com/shamoon) ([#6990](https://github.com/paperless-ngx/paperless-ngx/pull/6990))
|
||||
- Fix: Revert masking the content field from auditlog [@tribut](https://github.com/tribut) ([#6981](https://github.com/paperless-ngx/paperless-ngx/pull/6981))
|
||||
- Fix: respect model permissions for tasks API endpoint [@shamoon](https://github.com/shamoon) ([#6958](https://github.com/paperless-ngx/paperless-ngx/pull/6958))
|
||||
- Fix: Make the logging of an email message to be something useful [@stumpylog](https://github.com/stumpylog) ([#6901](https://github.com/paperless-ngx/paperless-ngx/pull/6901))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Documentation: Corrections and clarifications for Python support [@stumpylog](https://github.com/stumpylog) ([#6995](https://github.com/paperless-ngx/paperless-ngx/pull/6995))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore(deps): Bump stumpylog/image-cleaner-action from 0.6.0 to 0.7.0 in the actions group [@dependabot](https://github.com/dependabot) ([#6968](https://github.com/paperless-ngx/paperless-ngx/pull/6968))
|
||||
- Chore: Configures dependabot to ignore djangorestframework [@stumpylog](https://github.com/stumpylog) ([#6967](https://github.com/paperless-ngx/paperless-ngx/pull/6967))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>10 changes</summary>
|
||||
|
||||
- Chore(deps): Bump pipenv from 2023.12.1 to 2024.0.1 [@stumpylog](https://github.com/stumpylog) ([#7019](https://github.com/paperless-ngx/paperless-ngx/pull/7019))
|
||||
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#7013](https://github.com/paperless-ngx/paperless-ngx/pull/7013))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7012](https://github.com/paperless-ngx/paperless-ngx/pull/7012))
|
||||
- Chore(deps-dev): Bump ws from 8.15.1 to 8.17.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#7015](https://github.com/paperless-ngx/paperless-ngx/pull/7015))
|
||||
- Chore(deps): Bump urllib3 from 2.2.1 to 2.2.2 [@dependabot](https://github.com/dependabot) ([#7014](https://github.com/paperless-ngx/paperless-ngx/pull/7014))
|
||||
- Chore: update packages used by mail parser html template [@shamoon](https://github.com/shamoon) ([#6970](https://github.com/paperless-ngx/paperless-ngx/pull/6970))
|
||||
- Chore(deps): Bump stumpylog/image-cleaner-action from 0.6.0 to 0.7.0 in the actions group [@dependabot](https://github.com/dependabot) ([#6968](https://github.com/paperless-ngx/paperless-ngx/pull/6968))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates [@dependabot](https://github.com/dependabot) ([#6953](https://github.com/paperless-ngx/paperless-ngx/pull/6953))
|
||||
- Chore: Updates to latest Trixie version of Ghostscript 10.03.1 [@stumpylog](https://github.com/stumpylog) ([#6956](https://github.com/paperless-ngx/paperless-ngx/pull/6956))
|
||||
- Chore(deps): Bump tornado from 6.4 to 6.4.1 [@dependabot](https://github.com/dependabot) ([#6930](https://github.com/paperless-ngx/paperless-ngx/pull/6930))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>17 changes</summary>
|
||||
|
||||
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#7013](https://github.com/paperless-ngx/paperless-ngx/pull/7013))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#7012](https://github.com/paperless-ngx/paperless-ngx/pull/7012))
|
||||
- Chore(deps-dev): Bump ws from 8.15.1 to 8.17.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#7015](https://github.com/paperless-ngx/paperless-ngx/pull/7015))
|
||||
- Feature: documents trash aka soft delete [@shamoon](https://github.com/shamoon) ([#6944](https://github.com/paperless-ngx/paperless-ngx/pull/6944))
|
||||
- Enhancement: better boolean custom field display [@shamoon](https://github.com/shamoon) ([#7001](https://github.com/paperless-ngx/paperless-ngx/pull/7001))
|
||||
- Fix: default order of documents gets lost in QuerySet pipeline [@madduck](https://github.com/madduck) ([#6982](https://github.com/paperless-ngx/paperless-ngx/pull/6982))
|
||||
- Fix: Document history could include extra fields [@stumpylog](https://github.com/stumpylog) ([#6989](https://github.com/paperless-ngx/paperless-ngx/pull/6989))
|
||||
- Fix: use local pdf worker js [@shamoon](https://github.com/shamoon) ([#6990](https://github.com/paperless-ngx/paperless-ngx/pull/6990))
|
||||
- Fix: Revert masking the content field from auditlog [@tribut](https://github.com/tribut) ([#6981](https://github.com/paperless-ngx/paperless-ngx/pull/6981))
|
||||
- Chore: update packages used by mail parser html template [@shamoon](https://github.com/shamoon) ([#6970](https://github.com/paperless-ngx/paperless-ngx/pull/6970))
|
||||
- Chore(deps-dev): Bump the development group with 3 updates [@dependabot](https://github.com/dependabot) ([#6953](https://github.com/paperless-ngx/paperless-ngx/pull/6953))
|
||||
- Fix: respect model permissions for tasks API endpoint [@shamoon](https://github.com/shamoon) ([#6958](https://github.com/paperless-ngx/paperless-ngx/pull/6958))
|
||||
- Feature: Allow encrypting sensitive fields in export [@stumpylog](https://github.com/stumpylog) ([#6927](https://github.com/paperless-ngx/paperless-ngx/pull/6927))
|
||||
- Enhancement: allow consumption of odg files [@daniel-boehme](https://github.com/daniel-boehme) ([#6940](https://github.com/paperless-ngx/paperless-ngx/pull/6940))
|
||||
- Enhancement: use note model permissions for notes [@shamoon](https://github.com/shamoon) ([#6913](https://github.com/paperless-ngx/paperless-ngx/pull/6913))
|
||||
- Chore: Resolves test issues with Python 3.12 [@stumpylog](https://github.com/stumpylog) ([#6902](https://github.com/paperless-ngx/paperless-ngx/pull/6902))
|
||||
- Fix: Make the logging of an email message to be something useful [@stumpylog](https://github.com/stumpylog) ([#6901](https://github.com/paperless-ngx/paperless-ngx/pull/6901))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.9.0
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: Allow a data only export/import cycle [@stumpylog](https://github.com/stumpylog) ([#6871](https://github.com/paperless-ngx/paperless-ngx/pull/6871))
|
||||
- Change: rename 'redo OCR' to 'reprocess' to clarify behavior [@shamoon](https://github.com/shamoon) ([#6866](https://github.com/paperless-ngx/paperless-ngx/pull/6866))
|
||||
- Enhancement: Support custom path for the classification file [@lino-b](https://github.com/lino-b) ([#6858](https://github.com/paperless-ngx/paperless-ngx/pull/6858))
|
||||
- Enhancement: default to title/content search, allow choosing full search link from global search [@shamoon](https://github.com/shamoon) ([#6805](https://github.com/paperless-ngx/paperless-ngx/pull/6805))
|
||||
- Enhancement: only include correspondent 'last_correspondence' if requested [@shamoon](https://github.com/shamoon) ([#6792](https://github.com/paperless-ngx/paperless-ngx/pull/6792))
|
||||
- Enhancement: delete pages PDF action [@shamoon](https://github.com/shamoon) ([#6772](https://github.com/paperless-ngx/paperless-ngx/pull/6772))
|
||||
- Enhancement: support custom logo / title on login page [@shamoon](https://github.com/shamoon) ([#6775](https://github.com/paperless-ngx/paperless-ngx/pull/6775))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: including ordering param for id\_\_in retrievals [@shamoon](https://github.com/shamoon) ([#6875](https://github.com/paperless-ngx/paperless-ngx/pull/6875))
|
||||
- Fix: Don't allow the workflow save to override other process updates [@stumpylog](https://github.com/stumpylog) ([#6849](https://github.com/paperless-ngx/paperless-ngx/pull/6849))
|
||||
- Fix: consistently use created_date for doc display [@shamoon](https://github.com/shamoon) ([#6758](https://github.com/paperless-ngx/paperless-ngx/pull/6758))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Change the code formatter to Ruff [@stumpylog](https://github.com/stumpylog) ([#6756](https://github.com/paperless-ngx/paperless-ngx/pull/6756))
|
||||
- Chore: Backend updates [@stumpylog](https://github.com/stumpylog) ([#6755](https://github.com/paperless-ngx/paperless-ngx/pull/6755))
|
||||
- Chore(deps): Bump crowdin/github-action from 1 to 2 in the actions group [@dependabot](https://github.com/dependabot) ([#6881](https://github.com/paperless-ngx/paperless-ngx/pull/6881))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>12 changes</summary>
|
||||
|
||||
- Chore(deps-dev): Bump jest-preset-angular from 14.0.4 to 14.1.0 in /src-ui in the frontend-jest-dependencies group [@dependabot](https://github.com/dependabot) ([#6879](https://github.com/paperless-ngx/paperless-ngx/pull/6879))
|
||||
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#6892](https://github.com/paperless-ngx/paperless-ngx/pull/6892))
|
||||
- Chore(deps): Bump crowdin/github-action from 1 to 2 in the actions group [@dependabot](https://github.com/dependabot) ([#6881](https://github.com/paperless-ngx/paperless-ngx/pull/6881))
|
||||
- Chore: Updates Ghostscript to 10.03.1 [@stumpylog](https://github.com/stumpylog) ([#6854](https://github.com/paperless-ngx/paperless-ngx/pull/6854))
|
||||
- Chore(deps-dev): Bump the development group across 1 directory with 2 updates [@dependabot](https://github.com/dependabot) ([#6851](https://github.com/paperless-ngx/paperless-ngx/pull/6851))
|
||||
- Chore(deps): Bump the small-changes group with 3 updates [@dependabot](https://github.com/dependabot) ([#6843](https://github.com/paperless-ngx/paperless-ngx/pull/6843))
|
||||
- Chore(deps): Use psycopg as recommended [@stumpylog](https://github.com/stumpylog) ([#6811](https://github.com/paperless-ngx/paperless-ngx/pull/6811))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#6793](https://github.com/paperless-ngx/paperless-ngx/pull/6793))
|
||||
- Chore(deps): Bump requests from 2.31.0 to 2.32.0 [@dependabot](https://github.com/dependabot) ([#6795](https://github.com/paperless-ngx/paperless-ngx/pull/6795))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 19 updates [@dependabot](https://github.com/dependabot) ([#6761](https://github.com/paperless-ngx/paperless-ngx/pull/6761))
|
||||
- Chore: Backend updates [@stumpylog](https://github.com/stumpylog) ([#6755](https://github.com/paperless-ngx/paperless-ngx/pull/6755))
|
||||
- Chore: revert pngx pdf viewer to third party package [@shamoon](https://github.com/shamoon) ([#6741](https://github.com/paperless-ngx/paperless-ngx/pull/6741))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>19 changes</summary>
|
||||
|
||||
- Chore(deps-dev): Bump jest-preset-angular from 14.0.4 to 14.1.0 in /src-ui in the frontend-jest-dependencies group [@dependabot](https://github.com/dependabot) ([#6879](https://github.com/paperless-ngx/paperless-ngx/pull/6879))
|
||||
- Fix: including ordering param for id\_\_in retrievals [@shamoon](https://github.com/shamoon) ([#6875](https://github.com/paperless-ngx/paperless-ngx/pull/6875))
|
||||
- Feature: Allow a data only export/import cycle [@stumpylog](https://github.com/stumpylog) ([#6871](https://github.com/paperless-ngx/paperless-ngx/pull/6871))
|
||||
- Change: rename 'redo OCR' to 'reprocess' to clarify behavior [@shamoon](https://github.com/shamoon) ([#6866](https://github.com/paperless-ngx/paperless-ngx/pull/6866))
|
||||
- Enhancement: Support custom path for the classification file [@lino-b](https://github.com/lino-b) ([#6858](https://github.com/paperless-ngx/paperless-ngx/pull/6858))
|
||||
- Chore(deps-dev): Bump the development group across 1 directory with 2 updates [@dependabot](https://github.com/dependabot) ([#6851](https://github.com/paperless-ngx/paperless-ngx/pull/6851))
|
||||
- Chore(deps): Bump the small-changes group with 3 updates [@dependabot](https://github.com/dependabot) ([#6843](https://github.com/paperless-ngx/paperless-ngx/pull/6843))
|
||||
- Fix: Don't allow the workflow save to override other process updates [@stumpylog](https://github.com/stumpylog) ([#6849](https://github.com/paperless-ngx/paperless-ngx/pull/6849))
|
||||
- Chore(deps): Use psycopg as recommended [@stumpylog](https://github.com/stumpylog) ([#6811](https://github.com/paperless-ngx/paperless-ngx/pull/6811))
|
||||
- Enhancement: default to title/content search, allow choosing full search link from global search [@shamoon](https://github.com/shamoon) ([#6805](https://github.com/paperless-ngx/paperless-ngx/pull/6805))
|
||||
- Enhancement: only include correspondent 'last_correspondence' if requested [@shamoon](https://github.com/shamoon) ([#6792](https://github.com/paperless-ngx/paperless-ngx/pull/6792))
|
||||
- Enhancement: accessibility improvements for tags, doc links, dashboard views [@shamoon](https://github.com/shamoon) ([#6786](https://github.com/paperless-ngx/paperless-ngx/pull/6786))
|
||||
- Enhancement: delete pages PDF action [@shamoon](https://github.com/shamoon) ([#6772](https://github.com/paperless-ngx/paperless-ngx/pull/6772))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#6793](https://github.com/paperless-ngx/paperless-ngx/pull/6793))
|
||||
- Enhancement: support custom logo / title on login page [@shamoon](https://github.com/shamoon) ([#6775](https://github.com/paperless-ngx/paperless-ngx/pull/6775))
|
||||
- Chore: Change the code formatter to Ruff [@stumpylog](https://github.com/stumpylog) ([#6756](https://github.com/paperless-ngx/paperless-ngx/pull/6756))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 19 updates [@dependabot](https://github.com/dependabot) ([#6761](https://github.com/paperless-ngx/paperless-ngx/pull/6761))
|
||||
- Fix: consistently use created_date for doc display [@shamoon](https://github.com/shamoon) ([#6758](https://github.com/paperless-ngx/paperless-ngx/pull/6758))
|
||||
- Chore: revert pngx pdf viewer to third party package [@shamoon](https://github.com/shamoon) ([#6741](https://github.com/paperless-ngx/paperless-ngx/pull/6741))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.8.6
|
||||
|
||||
### Bug Fixes
|
||||
|
@@ -219,10 +219,10 @@ database, classification model, etc).
|
||||
|
||||
Defaults to "../data/", relative to the "src" directory.
|
||||
|
||||
#### [`PAPERLESS_TRASH_DIR=<path>`](#PAPERLESS_TRASH_DIR) {#PAPERLESS_TRASH_DIR}
|
||||
#### [`PAPERLESS_EMPTY_TRASH_DIR=<path>`](#PAPERLESS_EMPTY_TRASH_DIR) {#PAPERLESS_EMPTY_TRASH_DIR}
|
||||
|
||||
: Instead of removing deleted documents, they are moved to this
|
||||
directory.
|
||||
: When documents are deleted (e.g. after emptying the trash) the original files will be moved here
|
||||
instead of being removed from the filesystem. Only the original version is kept.
|
||||
|
||||
This must be writeable by the user running paperless. When running
|
||||
inside docker, ensure that this path is within a permanent volume
|
||||
@@ -230,7 +230,9 @@ directory.
|
||||
|
||||
Note that the directory must exist prior to using this setting.
|
||||
|
||||
Defaults to empty (i.e. really delete documents).
|
||||
Defaults to empty (i.e. really delete files).
|
||||
|
||||
This setting was previously named PAPERLESS_TRASH_DIR.
|
||||
|
||||
#### [`PAPERLESS_MEDIA_ROOT=<path>`](#PAPERLESS_MEDIA_ROOT) {#PAPERLESS_MEDIA_ROOT}
|
||||
|
||||
@@ -592,15 +594,32 @@ system. See the corresponding
|
||||
|
||||
#### [`PAPERLESS_DISABLE_REGULAR_LOGIN=<bool>`](#PAPERLESS_DISABLE_REGULAR_LOGIN) {#PAPERLESS_DISABLE_REGULAR_LOGIN}
|
||||
|
||||
: 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. To prevent logins directly to Django, consider blocking `/admin/` in your [web server or reverse proxy configuration](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx).
|
||||
: Disables the regular frontend username / password login, i.e. once you have setup SSO. Note that this setting does not disable the Django admin login nor logging in with local credentials via the API. To prevent access to the Django admin, consider blocking `/admin/` in your [web server or reverse proxy configuration](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx).
|
||||
|
||||
You can optionally also automatically redirect users to the SSO login with [PAPERLESS_REDIRECT_LOGIN_TO_SSO](#PAPERLESS_REDIRECT_LOGIN_TO_SSO)
|
||||
|
||||
Defaults to False
|
||||
|
||||
#### [`PAPERLESS_REDIRECT_LOGIN_TO_SSO=<bool>`](#PAPERLESS_REDIRECT_LOGIN_TO_SSO) {#PAPERLESS_REDIRECT_LOGIN_TO_SSO}
|
||||
|
||||
: When this setting is enabled users will automatically be redirected (using javascript) to the first SSO provider login. You may still want to disable the frontend login form for clarity.
|
||||
|
||||
Defaults to False
|
||||
|
||||
#### [`PAPERLESS_ACCOUNT_SESSION_REMEMBER=<bool>`](#PAPERLESS_ACCOUNT_SESSION_REMEMBER) {#PAPERLESS_ACCOUNT_SESSION_REMEMBER}
|
||||
|
||||
: See the corresponding
|
||||
: If false, sessions will expire at browser close, if true will use `PAPERLESS_SESSION_COOKIE_AGE` for expiration. See the corresponding
|
||||
[django-allauth documentation](https://docs.allauth.org/en/latest/account/configuration.html)
|
||||
|
||||
Defaults to True
|
||||
|
||||
#### [`PAPERLESS_SESSION_COOKIE_AGE=<int>`](#PAPERLESS_SESSION_COOKIE_AGE) {#PAPERLESS_SESSION_COOKIE_AGE}
|
||||
|
||||
: Login session cookie expiration. Applies if `PAPERLESS_ACCOUNT_SESSION_REMEMBER` is enabled. See the corresponding
|
||||
[django documentation](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-SESSION_COOKIE_AGE)
|
||||
|
||||
Defaults to 1209600 (2 weeks)
|
||||
|
||||
## OCR settings {#ocr}
|
||||
|
||||
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
|
||||
@@ -1139,6 +1158,12 @@ within your documents.
|
||||
second, and year last order. Characters D, M, or Y can be shuffled
|
||||
to meet the required order.
|
||||
|
||||
#### [`PAPERLESS_ENABLE_GPG_DECRYPTOR=<bool>`](#PAPERLESS_ENABLE_GPG_DECRYPTOR) {#PAPERLESS_ENABLE_GPG_DECRYPTOR}
|
||||
|
||||
: Enable or disable the GPG decryptor for encrypted emails. See [GPG Decryptor](advanced_usage.md#gpg-decryptor) for more information.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
### Polling {#polling}
|
||||
|
||||
#### [`PAPERLESS_CONSUMER_POLLING=<num>`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING}
|
||||
@@ -1182,6 +1207,48 @@ consumers working on the same file. Configure this to prevent that.
|
||||
|
||||
Defaults to 0.5 seconds.
|
||||
|
||||
## Incoming Mail {#incoming_mail}
|
||||
|
||||
### Email OAuth {#email_oauth}
|
||||
|
||||
#### [`PAPERLESS_OAUTH_CALLBACK_BASE_URL=<str>`](#PAPERLESS_OAUTH_CALLBACK_BASE_URL) {#PAPERLESS_OAUTH_CALLBACK_BASE_URL}
|
||||
|
||||
: The base URL for the OAuth callback. This is used to construct the full URL for the OAuth callback. This should be the URL that the Paperless instance is accessible at. If not set, defaults to the `PAPERLESS_URL` setting. At least one of these settings must be set to enable OAuth Email setup.
|
||||
|
||||
Defaults to none (thus will use [PAPERLESS_URL](#PAPERLESS_URL)).
|
||||
|
||||
#### [`PAPERLESS_GMAIL_OAUTH_CLIENT_ID=<str>`](#PAPERLESS_GMAIL_OAUTH_CLIENT_ID) {#PAPERLESS_GMAIL_OAUTH_CLIENT_ID}
|
||||
|
||||
: The OAuth client ID for Gmail. This is required for Gmail OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
|
||||
|
||||
Defaults to none.
|
||||
|
||||
#### [`PAPERLESS_GMAIL_OAUTH_CLIENT_SECRET=<str>`](#PAPERLESS_GMAIL_OAUTH_CLIENT_SECRET) {#PAPERLESS_GMAIL_OAUTH_CLIENT_SECRET}
|
||||
|
||||
: The OAuth client secret for Gmail. This is required for Gmail OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
|
||||
|
||||
Defaults to none.
|
||||
|
||||
#### [`PAPERLESS_OUTLOOK_OAUTH_CLIENT_ID=<str>`](#PAPERLESS_OUTLOOK_OAUTH_CLIENT_ID) {#PAPERLESS_OUTLOOK_OAUTH_CLIENT_ID}
|
||||
|
||||
: The OAuth client ID for Outlook. This is required for Outlook OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
|
||||
|
||||
Defaults to none.
|
||||
|
||||
#### [`PAPERLESS_OUTLOOK_OAUTH_CLIENT_SECRET=<str>`](#PAPERLESS_OUTLOOK_OAUTH_CLIENT_SECRET) {#PAPERLESS_OUTLOOK_OAUTH_CLIENT_SECRET}
|
||||
|
||||
: The OAuth client secret for Outlook. This is required for Outlook OAuth Email setup. See [OAuth Email Setup](usage.md#oauth-email-setup) for more information.
|
||||
|
||||
Defaults to none.
|
||||
|
||||
### Encrypted Emails {#encrypted_emails}
|
||||
|
||||
#### [`PAPERLESS_EMAIL_GNUPG_HOME=<str>`](#PAPERLESS_EMAIL_GNUPG_HOME) {#PAPERLESS_EMAIL_GNUPG_HOME}
|
||||
|
||||
: Optional, sets the `GNUPG_HOME` path to use with GPG decryptor for encrypted emails. See [GPG Decryptor](advanced_usage.md#gpg-decryptor) for more information. If not set, defaults to the default `GNUPG_HOME` path.
|
||||
|
||||
Defaults to <not set>.
|
||||
|
||||
## Barcodes {#barcodes}
|
||||
|
||||
#### [`PAPERLESS_CONSUMER_ENABLE_BARCODES=<bool>`](#PAPERLESS_CONSUMER_ENABLE_BARCODES) {#PAPERLESS_CONSUMER_ENABLE_BARCODES}
|
||||
@@ -1267,6 +1334,15 @@ combination with PAPERLESS_CONSUMER_BARCODE_UPSCALE bigger than 1.0.
|
||||
|
||||
Defaults to "300"
|
||||
|
||||
#### [`PAPERLESS_CONSUMER_BARCODE_MAX_PAGES=<int>`](#PAPERLESS_CONSUMER_BARCODE_MAX_PAGES) {#PAPERLESS_CONSUMER_BARCODE_MAX_PAGES}
|
||||
|
||||
: Because barcode detection is a computationally-intensive operation, this setting
|
||||
limits the detection of barcodes to a number of first pages. If your scanner has
|
||||
a limit for the number of pages that can be scanned it would be sensible to set this
|
||||
as the limit here.
|
||||
|
||||
Defaults to "0", allowing all pages to be checked for barcodes.
|
||||
|
||||
#### [`PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE=<bool>`](#PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE) {#PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE}
|
||||
|
||||
: Enables the detection of barcodes in the scanned document and
|
||||
@@ -1362,6 +1438,20 @@ processing. This only has an effect if
|
||||
|
||||
Defaults to false.
|
||||
|
||||
## Trash
|
||||
|
||||
#### [`PAPERLESS_EMPTY_TRASH_DELAY=<num>`](#PAPERLESS_EMPTY_TRASH_DELAY) {#PAPERLESS_EMPTY_TRASH_DELAY}
|
||||
|
||||
: Sets how long in days documents remain in the 'trash' before they are permanently deleted.
|
||||
|
||||
Defaults to 30 days, minimum of 1 day.
|
||||
|
||||
#### [`PAPERLESS_EMPTY_TRASH_TASK_CRON=<cron expression>`](#PAPERLESS_EMPTY_TRASH_TASK_CRON) {#PAPERLESS_EMPTY_TRASH_TASK_CRON}
|
||||
|
||||
: Configures the schedule to empty the trash of expired deleted documents.
|
||||
|
||||
Defaults to `0 1 * * *`, once per day.
|
||||
|
||||
## Binaries
|
||||
|
||||
There are a few external software packages that Paperless expects to
|
||||
|
@@ -81,10 +81,6 @@ first-time setup.
|
||||
!!! note
|
||||
|
||||
Using a virtual environment is highly recommended. You can spawn one via `pipenv shell`.
|
||||
Make sure you're using Python 3.10.x or lower. Otherwise you might
|
||||
get issues with building dependencies. You can use
|
||||
[pyenv](https://github.com/pyenv/pyenv) to install a specific
|
||||
Python version.
|
||||
|
||||
5. Install pre-commit hooks:
|
||||
|
||||
@@ -364,10 +360,10 @@ If you want to build the documentation locally, this is how you do it:
|
||||
The docker image is primarily built by the GitHub actions workflow, but
|
||||
it can be faster when developing to build and tag an image locally.
|
||||
|
||||
Building the image works as with any image:
|
||||
Make sure you have the `docker-buildx` package installed. Building the image works as with any image:
|
||||
|
||||
```
|
||||
docker build --file Dockerfile --tag paperless:local --progress simple .
|
||||
docker build --file Dockerfile --tag paperless:local .
|
||||
```
|
||||
|
||||
## Extending Paperless-ngx
|
||||
|
@@ -132,3 +132,11 @@ Multiple options for ASGI servers exist:
|
||||
- `daphne` as a standalone server, which is the reference
|
||||
implementation for ASGI.
|
||||
- `uvicorn` as a standalone server
|
||||
|
||||
## _What about the Redis licensing change and using one of the open source forks_?
|
||||
|
||||
Currently (October 2024), forks of Redis such as Valkey or Redirect are not officially supported by our upstream
|
||||
libraries, so using one of these to replace Redis is not officially supported.
|
||||
|
||||
However, they do claim to be compatible with the Redis protocol and will likely work, but we will
|
||||
not be updating from using Redis as the broker officially just yet.
|
||||
|
@@ -250,9 +250,14 @@ a minimal installation of Debian/Buster, which is the current stable
|
||||
release at the time of writing. Windows is not and will never be
|
||||
supported.
|
||||
|
||||
Paperless requires Python 3. At this time, 3.10 - 3.12 are tested versions.
|
||||
Newer versions may work, but some dependencies may not fully support newer versions.
|
||||
Support for older Python versions may be dropped as they reach end of life or as newer versions
|
||||
are released, dependency support is confirmed, etc.
|
||||
|
||||
1. Install dependencies. Paperless requires the following packages.
|
||||
|
||||
- `python3` - 3.9 - 3.11 are supported
|
||||
- `python3`
|
||||
- `python3-pip`
|
||||
- `python3-dev`
|
||||
- `default-libmysqlclient-dev` for MariaDB
|
||||
@@ -264,14 +269,13 @@ supported.
|
||||
- `libpq-dev` for PostgreSQL
|
||||
- `libmagic-dev` for mime type detection
|
||||
- `mariadb-client` for MariaDB compile time
|
||||
- `mime-support` for mime type detection
|
||||
- `libzbar0` for barcode detection
|
||||
- `poppler-utils` for barcode detection
|
||||
|
||||
Use this list for your preferred package management:
|
||||
|
||||
```
|
||||
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev pkg-config libmagic-dev mime-support libzbar0 poppler-utils
|
||||
python3 python3-pip python3-dev imagemagick fonts-liberation gnupg libpq-dev default-libmysqlclient-dev pkg-config libmagic-dev libzbar0 poppler-utils
|
||||
```
|
||||
|
||||
These dependencies are required for OCRmyPDF, which is used for text
|
||||
@@ -299,6 +303,7 @@ supported.
|
||||
|
||||
- `libatlas-base-dev`
|
||||
- `libxslt1-dev`
|
||||
- `mime-support`
|
||||
|
||||
You will also need these for installing some of the python dependencies:
|
||||
|
||||
@@ -410,8 +415,7 @@ supported.
|
||||
sudo chown paperless:paperless /opt/paperless/consume
|
||||
```
|
||||
|
||||
8. Install python requirements from the `requirements.txt` file. It is
|
||||
up to you if you wish to use a virtual environment or not. First you should update your pip, so it gets the actual packages.
|
||||
8. Install python requirements from the `requirements.txt` file.
|
||||
|
||||
```shell-session
|
||||
sudo -Hu paperless pip3 install -r requirements.txt
|
||||
@@ -420,6 +424,12 @@ supported.
|
||||
This will install all python dependencies in the home directory of
|
||||
the new paperless user.
|
||||
|
||||
!!! tip
|
||||
|
||||
It is up to you if you wish to use a virtual environment or not for the Python
|
||||
dependencies. This is an alternative to the above and may require adjusting
|
||||
the example scripts to utilize the virtual environment paths
|
||||
|
||||
9. Go to `/opt/paperless/src`, and execute the following commands:
|
||||
|
||||
```bash
|
||||
@@ -530,8 +540,7 @@ supported.
|
||||
15. Optional: If using the NLTK machine learning processing (see
|
||||
[`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) for details),
|
||||
download the NLTK data for the Snowball
|
||||
Stemmer, Stopwords and Punkt tokenizer to your
|
||||
`PAPERLESS_DATA_DIR/nltk`. Refer to the [NLTK
|
||||
Stemmer, Stopwords and Punkt tokenizer to `/usr/share/nltk_data`. Refer to the [NLTK
|
||||
instructions](https://www.nltk.org/data.html) for details on how to
|
||||
download the data.
|
||||
|
||||
|
@@ -112,7 +112,7 @@ process.
|
||||
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects) for a user-maintained list of related projects and
|
||||
software (e.g. for mobile devices) that is compatible with Paperless-ngx.
|
||||
|
||||
### IMAP (Email) {#usage-email}
|
||||
### Email {#usage-email}
|
||||
|
||||
You can tell paperless-ngx to consume documents from your email
|
||||
accounts. This is a very flexible and powerful feature, if you regularly
|
||||
@@ -136,7 +136,8 @@ These rules perform the following:
|
||||
|
||||
Paperless will check all emails only once and completely ignore messages
|
||||
that do not match your filters. It will also only perform the rule action
|
||||
on e-mails that it has consumed documents from.
|
||||
on e-mails that it has consumed documents from. The filename attachment
|
||||
patterns can include wildcards and multiple patterns separated by a comma.
|
||||
|
||||
The actions all ensure that the same mail is not consumed twice by
|
||||
different means. These are as follows:
|
||||
@@ -199,6 +200,14 @@ different means. These are as follows:
|
||||
Paperless is set up to check your mails every 10 minutes. This can be
|
||||
configured via [`PAPERLESS_EMAIL_TASK_CRON`](configuration.md#PAPERLESS_EMAIL_TASK_CRON)
|
||||
|
||||
#### OAuth Email Setup
|
||||
|
||||
Paperless-ngx supports OAuth2 authentication for Gmail and Outlook email accounts. To set up an email account with OAuth2, you will need to create a 'developer' app with the respective provider and obtain the client ID and client secret and set the appropriate [configuration variables](configuration.md#email_oauth). You will also need to set either [`PAPERLESS_OAUTH_CALLBACK_BASE_URL`](configuration.md#PAPERLESS_OAUTH_CALLBACK_BASE_URL) or [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) to the correct value for the OAuth2 flow to work correctly.
|
||||
|
||||
Specific instructions for setting up the required 'developer' app with Google or Microsoft are beyond the scope of this documentation, but you can find user-maintained instructions in [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Email-OAuth-App-Setup) or by searching the web.
|
||||
|
||||
Once setup, navigating to the email settings page in Paperless-ngx will allow you to add an email account for Gmail or Outlook using OAuth2. After authenticating, you will be presented with the newly-created account where you will need to enter and save your email address. After this, the account will work as any other email account in Paperless-ngx and refreshing tokens will be handled automatically.
|
||||
|
||||
### REST API
|
||||
|
||||
You can also submit a document using the REST API, see [POSTing documents](api.md#file-uploads)
|
||||
@@ -237,9 +246,13 @@ Settings > Users & Groups, assuming the user has access. If a user is designated
|
||||
as a member of a group those permissions will be inherited and this is reflected in the UI. Explicit
|
||||
permissions can be granted to limit access to certain parts of the UI (and corresponding API endpoints).
|
||||
|
||||
!!! note
|
||||
!!! tip
|
||||
|
||||
Superusers can access all parts of the front and backend application as well as any and all objects.
|
||||
By default, new users are not granted any permissions, except those inherited from any group(s) of which they are a member.
|
||||
|
||||
#### Superusers
|
||||
|
||||
Superusers can access all parts of the front and backend application as well as any and all objects.
|
||||
|
||||
#### Admin Status
|
||||
|
||||
@@ -248,29 +261,29 @@ as well as accessing the Django backend.
|
||||
|
||||
#### Detailed Explanation of Global Permissions {#global-permissions}
|
||||
|
||||
Global permissions define what areas of the app and API endpoints the user can access. For example, they
|
||||
Global permissions define what areas of the app and API endpoints users can access. For example, they
|
||||
determine if a user can create, edit, delete or view _any_ documents, but individual documents themselves
|
||||
still have "object-level" permissions.
|
||||
|
||||
| Type | Details |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| AppConfig | _Change_ or higher permissions grants access to the "Application Configuration" area. |
|
||||
| Correspondent | Grants global permissions to add, edit, delete or view Correspondents. |
|
||||
| CustomField | Grants global permissions to add, edit, delete or view Custom Fields. |
|
||||
| Document | Grants global permissions to add, edit, delete or view Documents. |
|
||||
| DocumentType | Grants global permissions to add, edit, delete or view Document Types. |
|
||||
| Group | Grants global permissions to add, edit, delete or view Groups. |
|
||||
| MailAccount | Grants global permissions to add, edit, delete or view Mail Accounts. |
|
||||
| MailRule | Grants global permissions to add, edit, delete or view Mail Rules. |
|
||||
| Note | Grants global permissions to add, edit, delete or view Notes. |
|
||||
| PaperlessTask | Grants global permissions to view or dismiss (_Change_) File Tasks. |
|
||||
| SavedView | Grants global permissions to add, edit, delete or view Saved Views. |
|
||||
| ShareLink | Grants global permissions to add, delete or view Share Links. |
|
||||
| StoragePath | Grants global permissions to add, edit, delete or view Storage Paths. |
|
||||
| Tag | Grants global permissions to add, edit, delete or view Tags. |
|
||||
| UISettings | Grants global permissions to add, edit, delete or view the UI settings that are used by the web app.<br/>Users expected to access the web UI should usually be granted at least _View_ permissions. |
|
||||
| User | Grants global permissions to add, edit, delete or view Users. |
|
||||
| Workflow | Grants global permissions to add, edit, delete or view Workflows.<br/>Note that Workflows are global, in other words all users who can access workflows have access to the same set of them. |
|
||||
| Type | Details |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| AppConfig | _Change_ or higher permissions grants access to the "Application Configuration" area. |
|
||||
| Correspondent | Add, edit, delete or view Correspondents. |
|
||||
| CustomField | Add, edit, delete or view Custom Fields. |
|
||||
| Document | Add, edit, delete or view Documents. |
|
||||
| DocumentType | Add, edit, delete or view Document Types. |
|
||||
| Group | Add, edit, delete or view Groups. |
|
||||
| MailAccount | Add, edit, delete or view Mail Accounts. |
|
||||
| MailRule | Add, edit, delete or view Mail Rules. |
|
||||
| Note | Add, edit, delete or view Notes. |
|
||||
| PaperlessTask | View or dismiss (_Change_) File Tasks. |
|
||||
| SavedView | Add, edit, delete or view Saved Views. |
|
||||
| ShareLink | Add, delete or view Share Links. |
|
||||
| StoragePath | Add, edit, delete or view Storage Paths. |
|
||||
| Tag | Add, edit, delete or view Tags. |
|
||||
| UISettings | Add, edit, delete or view the UI settings that are used by the web app.<br/>:warning: **Users that will access the web UI must be granted at least _View_ permissions.** |
|
||||
| User | Add, edit, delete or view Users. |
|
||||
| Workflow | Add, edit, delete or view Workflows.<br/>Note that Workflows are global, in other words all users who can access workflows have access to the same set of them. |
|
||||
|
||||
#### Detailed Explanation of Object Permissions {#object-permissions}
|
||||
|
||||
@@ -445,6 +458,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
|
||||
|
||||
@@ -478,6 +492,15 @@ As of version 2.7, Paperless-ngx automatically records all changes to a document
|
||||
Changes to documents are visible under the "History" tab. Note that certain changes such as those made by workflows, record the 'actor'
|
||||
as "System".
|
||||
|
||||
## Document Trash
|
||||
|
||||
When you first delete a document it is moved to the 'trash' until either it is explicitly deleted or it is automatically removed after a set amount of time has passed.
|
||||
You can set how long documents remain in the trash before being automatically deleted with [`PAPERLESS_EMPTY_TRASH_DELAY`](configuration.md#PAPERLESS_EMPTY_TRASH_DELAY), which defaults
|
||||
to 30 days. Until the file is actually deleted (e.g. the trash is emptied), all files and database content remains intact and can be restored at any point up until that time.
|
||||
|
||||
Additionally you may configure a directory where deleted files are moved to when they the trash is emptied with [`PAPERLESS_EMPTY_TRASH_DIR`](configuration.md#PAPERLESS_EMPTY_TRASH_DIR).
|
||||
Note that the empty trash directory only stores the original file, the archive file and all database information is permanently removed once a document is fully deleted.
|
||||
|
||||
## Best practices {#basic-searching}
|
||||
|
||||
Paperless offers a couple tools that help you organize your document
|
||||
|
11
mkdocs.yml
11
mkdocs.yml
@@ -6,6 +6,12 @@ theme:
|
||||
text: Roboto
|
||||
code: Roboto Mono
|
||||
palette:
|
||||
# Palette toggle for automatic mode
|
||||
- media: "(prefers-color-scheme)"
|
||||
toggle:
|
||||
icon: material/brightness-auto
|
||||
name: Switch to light mode
|
||||
|
||||
# Palette toggle for light mode
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
@@ -18,7 +24,7 @@ theme:
|
||||
scheme: slate
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
name: Switch to system preference
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.top
|
||||
@@ -49,6 +55,9 @@ markdown_extensions:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
strict: true
|
||||
nav:
|
||||
- index.md
|
||||
|
@@ -19,7 +19,7 @@
|
||||
|
||||
#PAPERLESS_CONSUMPTION_DIR=../consume
|
||||
#PAPERLESS_DATA_DIR=../data
|
||||
#PAPERLESS_TRASH_DIR=
|
||||
#PAPERLESS_EMPTY_TRASH_DIR=
|
||||
#PAPERLESS_MEDIA_ROOT=../media
|
||||
#PAPERLESS_STATICDIR=../static
|
||||
#PAPERLESS_FILENAME_FORMAT=
|
||||
|
@@ -14,6 +14,7 @@ following additional information about it:
|
||||
* Thumbnail Path: ${DOCUMENT_THUMBNAIL_PATH}
|
||||
* Download URL: ${DOCUMENT_DOWNLOAD_URL}
|
||||
* Thumbnail URL: ${DOCUMENT_THUMBNAIL_URL}
|
||||
* Owner Name: ${DOCUMENT_OWNER}
|
||||
* Correspondent: ${DOCUMENT_CORRESPONDENT}
|
||||
* Tags: ${DOCUMENT_TAGS}
|
||||
|
||||
|
@@ -33,6 +33,7 @@
|
||||
"it-IT": "src/locale/messages.it_IT.xlf",
|
||||
"ja-JP": "src/locale/messages.ja_JP.xlf",
|
||||
"lb-LU": "src/locale/messages.lb_LU.xlf",
|
||||
"ko-KR": "src/locale/messages.ko_KR.xlf",
|
||||
"nl-NL": "src/locale/messages.nl_NL.xlf",
|
||||
"no-NO": "src/locale/messages.no_NO.xlf",
|
||||
"pl-PL": "src/locale/messages.pl_PL.xlf",
|
||||
@@ -51,8 +52,11 @@
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"customWebpackConfig": {
|
||||
"path": "./extra-webpack.config.ts"
|
||||
},
|
||||
"outputPath": "dist/paperless-ui",
|
||||
"outputHashing": "none",
|
||||
"index": "src/index.html",
|
||||
@@ -66,8 +70,8 @@
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest",
|
||||
{
|
||||
"glob": "{pdf.worker.min.js,pdf.min.js}",
|
||||
"input": "node_modules/pdfjs-dist/build/",
|
||||
"glob": "{pdf.worker.min.mjs,pdf.min.mjs}",
|
||||
"input": "node_modules/pdfjs-dist/legacy/build/",
|
||||
"output": "/assets/js/"
|
||||
}
|
||||
],
|
||||
@@ -77,7 +81,6 @@
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"ng2-pdf-viewer",
|
||||
"filesize",
|
||||
"file-saver"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
@@ -125,7 +128,7 @@
|
||||
"defaultConfiguration": ""
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||
"options": {
|
||||
"buildTarget": "paperless-ui:build:en-US"
|
||||
},
|
||||
@@ -136,7 +139,7 @@
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"builder": "@angular-builders/custom-webpack:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "paperless-ui:build"
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ test('dashboard inbox link', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await page.getByRole('link', { name: 'Documents in inbox' }).click()
|
||||
await expect(page).toHaveURL(/tags__id__all=9/)
|
||||
await expect(page).toHaveURL(/tags__id__in=9/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||
})
|
||||
|
||||
|
@@ -236,7 +236,7 @@
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tag\":9,\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tags\":[9],\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
@@ -250,7 +250,7 @@
|
||||
"time": 0.609,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=10&ordering=-created&truncate_content=true&tags__id__all=9",
|
||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=10&ordering=-created&truncate_content=true&tags__id__in=9",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
@@ -284,7 +284,7 @@
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "tags__id__all",
|
||||
"name": "tags__id__in",
|
||||
"value": "9"
|
||||
}
|
||||
],
|
||||
@@ -468,7 +468,7 @@
|
||||
"time": 0.951,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__all=9",
|
||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=9",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
@@ -502,7 +502,7 @@
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "tags__id__all",
|
||||
"name": "tags__id__in",
|
||||
"value": "9"
|
||||
}
|
||||
],
|
||||
|
@@ -236,7 +236,7 @@
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tag\":9,\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tags\":[9],\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
@@ -250,7 +250,7 @@
|
||||
"time": 0.622,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=10&ordering=-created&truncate_content=true&tags__id__all=9",
|
||||
"url": "http://localhost:8000/api/documents/?page=1&page_size=10&ordering=-created&truncate_content=true&tags__id__in=9",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
@@ -284,7 +284,7 @@
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "tags__id__all",
|
||||
"name": "tags__id__in",
|
||||
"value": "9"
|
||||
}
|
||||
],
|
||||
|
@@ -236,7 +236,7 @@
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tag\":9,\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tags\":[9],\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
|
@@ -236,7 +236,7 @@
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tag\":9,\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tags\":[9],\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
|
@@ -84,13 +84,8 @@ test('date filtering', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Dates' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Last 3 months' }).first().click()
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
|
||||
await page.getByRole('button', { name: 'Dates Clear selected' }).click()
|
||||
await page.getByRole('button', { name: 'Dates' }).click()
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'After mm/dd/yyyy' })
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click()
|
||||
await page.getByRole('menuitem', { name: 'Last 3 months' }).first().click()
|
||||
await page.getByLabel('Datesselected').getByRole('button').first().click()
|
||||
await page.getByRole('combobox', { name: 'Select month' }).selectOption('12')
|
||||
await page.getByRole('combobox', { name: 'Select year' }).selectOption('2022')
|
||||
await page.getByText('11', { exact: true }).click()
|
||||
|
24
src-ui/extra-webpack.config.ts
Normal file
24
src-ui/extra-webpack.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as webpack from 'webpack'
|
||||
import {
|
||||
CustomWebpackBrowserSchema,
|
||||
TargetOptions,
|
||||
} from '@angular-builders/custom-webpack'
|
||||
const { codecovWebpackPlugin } = require('@codecov/webpack-plugin')
|
||||
|
||||
export default (
|
||||
config: webpack.Configuration,
|
||||
options: CustomWebpackBrowserSchema,
|
||||
targetOptions: TargetOptions
|
||||
) => {
|
||||
if (config.plugins) {
|
||||
config.plugins.push(
|
||||
codecovWebpackPlugin({
|
||||
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
|
||||
bundleName: 'paperless-ngx',
|
||||
uploadToken: process.env.CODECOV_TOKEN,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
1997
src-ui/messages.xlf
1997
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
8077
src-ui/package-lock.json
generated
8077
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,58 +11,60 @@
|
||||
},
|
||||
"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.2.6",
|
||||
"@angular/common": "~18.2.6",
|
||||
"@angular/compiler": "~18.2.6",
|
||||
"@angular/core": "~18.2.6",
|
||||
"@angular/forms": "~18.2.6",
|
||||
"@angular/localize": "~18.2.6",
|
||||
"@angular/platform-browser": "~18.2.6",
|
||||
"@angular/platform-browser-dynamic": "~18.2.6",
|
||||
"@angular/router": "~18.2.6",
|
||||
"@ng-bootstrap/ng-bootstrap": "^17.0.1",
|
||||
"@ng-select/ng-select": "^13.9.0",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"mime-names": "^1.0.0",
|
||||
"ng2-pdf-viewer": "^10.2.2",
|
||||
"ng2-pdf-viewer": "^10.3.1",
|
||||
"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"
|
||||
"tslib": "^2.7.0",
|
||||
"uuid": "^10.0.0",
|
||||
"zone.js": "^0.14.8"
|
||||
},
|
||||
"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",
|
||||
"@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",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.57.0",
|
||||
"@angular-builders/custom-webpack": "^18.0.0",
|
||||
"@angular-builders/jest": "^18.0.0",
|
||||
"@angular-devkit/build-angular": "^18.2.2",
|
||||
"@angular-devkit/core": "^18.2.6",
|
||||
"@angular-devkit/schematics": "^18.2.6",
|
||||
"@angular-eslint/builder": "18.3.1",
|
||||
"@angular-eslint/eslint-plugin": "18.3.1",
|
||||
"@angular-eslint/eslint-plugin-template": "18.3.1",
|
||||
"@angular-eslint/schematics": "18.3.1",
|
||||
"@angular-eslint/template-parser": "18.3.1",
|
||||
"@angular/cli": "~18.2.6",
|
||||
"@angular/compiler-cli": "~18.2.2",
|
||||
"@codecov/webpack-plugin": "^1.2.0",
|
||||
"@playwright/test": "^1.47.2",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/node": "^22.7.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.0",
|
||||
"@typescript-eslint/parser": "^8.8.0",
|
||||
"@typescript-eslint/utils": "^8.0.0",
|
||||
"eslint": "^9.11.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^14.1.0",
|
||||
"jest-preset-angular": "^14.2.4",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"wait-on": "^7.2.0"
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ import localeFr from '@angular/common/locales/fr'
|
||||
import localeHu from '@angular/common/locales/hu'
|
||||
import localeIt from '@angular/common/locales/it'
|
||||
import localeJa from '@angular/common/locales/ja'
|
||||
import localeKo from '@angular/common/locales/ko'
|
||||
import localeLb from '@angular/common/locales/lb'
|
||||
import localeNl from '@angular/common/locales/nl'
|
||||
import localeNo from '@angular/common/locales/no'
|
||||
@@ -55,6 +56,7 @@ registerLocaleData(localeFr)
|
||||
registerLocaleData(localeHu)
|
||||
registerLocaleData(localeIt)
|
||||
registerLocaleData(localeJa)
|
||||
registerLocaleData(localeKo)
|
||||
registerLocaleData(localeLb)
|
||||
registerLocaleData(localeNl)
|
||||
registerLocaleData(localeNo)
|
||||
|
@@ -26,6 +26,7 @@ import { MailComponent } from './components/manage/mail/mail.component'
|
||||
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
|
||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
||||
import { ConfigComponent } from './components/admin/config/config.component'
|
||||
import { TrashComponent } from './components/admin/trash/trash.component'
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
@@ -144,6 +145,17 @@ export const routes: Routes = [
|
||||
requireAdmin: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'trash',
|
||||
component: TrashComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.Delete,
|
||||
type: PermissionType.Document,
|
||||
},
|
||||
},
|
||||
},
|
||||
// redirect old paths
|
||||
{
|
||||
path: 'settings/mail',
|
||||
|
@@ -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)
|
||||
|
@@ -35,6 +35,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private permissionsService: PermissionsService,
|
||||
private hotKeyService: HotKeyService
|
||||
) {
|
||||
let anyWindow = window as any
|
||||
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.mjs'
|
||||
this.settings.updateAppearanceSettings()
|
||||
}
|
||||
|
||||
|
@@ -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'
|
||||
@@ -37,6 +41,7 @@ import { DocumentCardSmallComponent } from './components/document-list/document-
|
||||
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component'
|
||||
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||
import { TextComponent } from './components/common/input/text/text.component'
|
||||
import { TextAreaComponent } from './components/common/input/textarea/textarea.component'
|
||||
import { SelectComponent } from './components/common/input/select/select.component'
|
||||
import { CheckComponent } from './components/common/input/check/check.component'
|
||||
import { UrlComponent } from './components/common/input/url/url.component'
|
||||
@@ -104,6 +109,7 @@ import { FileDropComponent } from './components/file-drop/file-drop.component'
|
||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
||||
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||
import { CustomFieldsDropdownComponent } from './components/common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||
import { CustomFieldsQueryDropdownComponent } from './components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component'
|
||||
import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
import { PdfViewerModule } from 'ng2-pdf-viewer'
|
||||
import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
|
||||
@@ -115,7 +121,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'
|
||||
@@ -125,6 +130,7 @@ import { CustomFieldDisplayComponent } from './components/common/custom-field-di
|
||||
import { GlobalSearchComponent } from './components/app-frame/global-search/global-search.component'
|
||||
import { HotkeyDialogComponent } from './components/common/hotkey-dialog/hotkey-dialog.component'
|
||||
import { DeletePagesConfirmDialogComponent } from './components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
|
||||
import { TrashComponent } from './components/admin/trash/trash.component'
|
||||
import {
|
||||
airplane,
|
||||
archive,
|
||||
@@ -137,6 +143,7 @@ import {
|
||||
arrowRightShort,
|
||||
arrowUpRight,
|
||||
asterisk,
|
||||
braces,
|
||||
bodyText,
|
||||
boxArrowUp,
|
||||
boxArrowUpRight,
|
||||
@@ -168,6 +175,7 @@ import {
|
||||
download,
|
||||
envelope,
|
||||
envelopeAt,
|
||||
envelopeAtFill,
|
||||
exclamationCircleFill,
|
||||
exclamationTriangle,
|
||||
exclamationTriangleFill,
|
||||
@@ -184,6 +192,7 @@ import {
|
||||
folderFill,
|
||||
funnel,
|
||||
gear,
|
||||
google,
|
||||
grid,
|
||||
gripVertical,
|
||||
hash,
|
||||
@@ -194,6 +203,8 @@ import {
|
||||
link,
|
||||
listTask,
|
||||
listUl,
|
||||
microsoft,
|
||||
nodePlus,
|
||||
pencil,
|
||||
people,
|
||||
peopleFill,
|
||||
@@ -223,6 +234,7 @@ import {
|
||||
uiRadios,
|
||||
upcScan,
|
||||
x,
|
||||
xCircle,
|
||||
xLg,
|
||||
} from 'ngx-bootstrap-icons'
|
||||
|
||||
@@ -238,6 +250,7 @@ const icons = {
|
||||
arrowRightShort,
|
||||
arrowUpRight,
|
||||
asterisk,
|
||||
braces,
|
||||
bodyText,
|
||||
boxArrowUp,
|
||||
boxArrowUpRight,
|
||||
@@ -269,6 +282,7 @@ const icons = {
|
||||
download,
|
||||
envelope,
|
||||
envelopeAt,
|
||||
envelopeAtFill,
|
||||
exclamationCircleFill,
|
||||
exclamationTriangle,
|
||||
exclamationTriangleFill,
|
||||
@@ -285,6 +299,7 @@ const icons = {
|
||||
folderFill,
|
||||
funnel,
|
||||
gear,
|
||||
google,
|
||||
grid,
|
||||
gripVertical,
|
||||
hash,
|
||||
@@ -295,6 +310,8 @@ const icons = {
|
||||
link,
|
||||
listTask,
|
||||
listUl,
|
||||
microsoft,
|
||||
nodePlus,
|
||||
pencil,
|
||||
people,
|
||||
peopleFill,
|
||||
@@ -324,6 +341,7 @@ const icons = {
|
||||
uiRadios,
|
||||
upcScan,
|
||||
x,
|
||||
xCircle,
|
||||
xLg,
|
||||
}
|
||||
|
||||
@@ -343,6 +361,7 @@ import localeFr from '@angular/common/locales/fr'
|
||||
import localeHu from '@angular/common/locales/hu'
|
||||
import localeIt from '@angular/common/locales/it'
|
||||
import localeJa from '@angular/common/locales/ja'
|
||||
import localeKo from '@angular/common/locales/ko'
|
||||
import localeLb from '@angular/common/locales/lb'
|
||||
import localeNl from '@angular/common/locales/nl'
|
||||
import localeNo from '@angular/common/locales/no'
|
||||
@@ -374,6 +393,7 @@ registerLocaleData(localeFr)
|
||||
registerLocaleData(localeHu)
|
||||
registerLocaleData(localeIt)
|
||||
registerLocaleData(localeJa)
|
||||
registerLocaleData(localeKo)
|
||||
registerLocaleData(localeLb)
|
||||
registerLocaleData(localeNl)
|
||||
registerLocaleData(localeNo)
|
||||
@@ -427,6 +447,7 @@ function initializeApp(settings: SettingsService) {
|
||||
DocumentCardSmallComponent,
|
||||
BulkEditorComponent,
|
||||
TextComponent,
|
||||
TextAreaComponent,
|
||||
SelectComponent,
|
||||
CheckComponent,
|
||||
UrlComponent,
|
||||
@@ -479,6 +500,7 @@ function initializeApp(settings: SettingsService) {
|
||||
CustomFieldsComponent,
|
||||
CustomFieldEditDialogComponent,
|
||||
CustomFieldsDropdownComponent,
|
||||
CustomFieldsQueryDropdownComponent,
|
||||
ProfileEditDialogComponent,
|
||||
DocumentLinkComponent,
|
||||
PreviewPopupComponent,
|
||||
@@ -497,12 +519,13 @@ function initializeApp(settings: SettingsService) {
|
||||
GlobalSearchComponent,
|
||||
HotkeyDialogComponent,
|
||||
DeletePagesConfirmDialogComponent,
|
||||
TrashComponent,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
NgbModule,
|
||||
HttpClientModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
PdfViewerModule,
|
||||
@@ -512,7 +535,6 @@ function initializeApp(settings: SettingsService) {
|
||||
TourNgBootstrapModule,
|
||||
DragDropModule,
|
||||
NgxBootstrapIconsModule.pick(icons),
|
||||
NgxFilesizeModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -541,7 +563,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)
|
||||
|
@@ -192,7 +192,7 @@
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs"></pngx-input-check>
|
||||
<pngx-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,7 +332,7 @@
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="SettingsNavIDs.SavedViews">
|
||||
<li [ngbNavItem]="SettingsNavIDs.SavedViews" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.SavedView }">
|
||||
<a ngbNavLink i18n>Saved views</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
@@ -70,11 +70,11 @@ export class TasksComponent
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.close()
|
||||
this.tasksService.dismissTasks(tasks)
|
||||
this.selectedTasks.clear()
|
||||
this.clearSelection()
|
||||
})
|
||||
} else {
|
||||
this.tasksService.dismissTasks(tasks)
|
||||
this.selectedTasks.clear()
|
||||
this.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
|
98
src-ui/src/app/components/admin/trash/trash.component.html
Normal file
98
src-ui/src/app/components/admin/trash/trash.component.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<pngx-page-header
|
||||
title="Trash"
|
||||
i18n-title
|
||||
info="Manage trashed documents that are pending deletion."
|
||||
i18n-info
|
||||
infoLink="usage/#document-trash">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments.size === 0">
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments)" [disabled]="selectedDocuments.size === 0">
|
||||
<i-bs name="arrow-counterclockwise"></i-bs> <ng-container i18n>Restore selected</ng-container>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments)" [disabled]="selectedDocuments.size === 0">
|
||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete selected</ng-container>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash.length === 0">
|
||||
<i-bs name="trash"></i-bs> <ng-container i18n>Empty trash</ng-container>
|
||||
</button>
|
||||
</pngx-page-header>
|
||||
|
||||
<div class="row mb-3">
|
||||
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="totalDocuments" [(page)]="page" [maxSize]="5" (pageChange)="reload()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
</div>
|
||||
|
||||
<div class="card border table-responsive mb-3">
|
||||
<table class="table table-striped align-middle shadow-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="allToggled" [disabled]="documentsInTrash.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-objects"></label>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="fw-normal" i18n>Name</th>
|
||||
<th scope="col" class="fw-normal d-none d-sm-table-cell" i18n>Remaining</th>
|
||||
<th scope="col" class="fw-normal" i18n>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (isLoading) {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@for (document of documentsInTrash; track document.id) {
|
||||
<tr (click)="toggleSelected(document); $event.stopPropagation();">
|
||||
<td>
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="{{document.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td scope="row">{{ document.title }}</td>
|
||||
<td scope="row" i18n>{{ getDaysRemaining(document) }} days</td>
|
||||
<td scope="row">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="restore(document)" ngbDropdownItem i18n>Restore</button>
|
||||
<button (click)="delete(document)" ngbDropdownItem i18n>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group d-none d-sm-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="restore(document); $event.stopPropagation();">
|
||||
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs> <ng-container i18n>Restore</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="delete(document); $event.stopPropagation();">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (!isLoading) {
|
||||
<div class="d-flex mb-2">
|
||||
<div>
|
||||
<ng-container i18n>{totalDocuments, plural, =1 {One document in trash} other {{{totalDocuments || 0}} total documents in trash}}</ng-container>
|
||||
@if (selectedDocuments.size > 0) {
|
||||
({{selectedDocuments.size}} selected)
|
||||
}
|
||||
</div>
|
||||
@if (documentsInTrash.length > 20) {
|
||||
<ngb-pagination class="ms-auto" [pageSize]="25" [collectionSize]="totalDocuments" [(page)]="page" [maxSize]="5" (pageChange)="reload()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
}
|
||||
</div>
|
||||
}
|
200
src-ui/src/app/components/admin/trash/trash.component.spec.ts
Normal file
200
src-ui/src/app/components/admin/trash/trash.component.spec.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { TrashComponent } from './trash.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbPaginationModule,
|
||||
NgbPopoverModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TrashService } from 'src/app/services/trash.service'
|
||||
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 = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
created: new Date('2023-03-01T10:26:03.093116Z'),
|
||||
deleted_at: new Date('2023-03-01T10:26:03.093116Z'),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'test2',
|
||||
created: new Date('2023-03-01T10:26:03.093116Z'),
|
||||
deleted_at: new Date('2023-03-01T10:26:03.093116Z'),
|
||||
},
|
||||
]
|
||||
|
||||
describe('TrashComponent', () => {
|
||||
let component: TrashComponent
|
||||
let fixture: ComponentFixture<TrashComponent>
|
||||
let trashService: TrashService
|
||||
let modalService: NgbModal
|
||||
let toastService: ToastService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TrashComponent,
|
||||
PageHeaderComponent,
|
||||
ConfirmDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbPopoverModule,
|
||||
NgbPaginationModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TrashComponent)
|
||||
trashService = TestBed.inject(TrashService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should call correct service method on reload', () => {
|
||||
const trashSpy = jest.spyOn(trashService, 'getTrash')
|
||||
trashSpy.mockReturnValue(
|
||||
of({
|
||||
count: 2,
|
||||
all: documentsInTrash.map((d) => d.id),
|
||||
results: documentsInTrash,
|
||||
})
|
||||
)
|
||||
component.reload()
|
||||
expect(trashSpy).toHaveBeenCalled()
|
||||
expect(component.documentsInTrash).toEqual(documentsInTrash)
|
||||
})
|
||||
|
||||
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()
|
||||
modal.componentInstance.confirmClicked.next()
|
||||
expect(trashSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
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()
|
||||
modal.componentInstance.confirmClicked.next()
|
||||
expect(trashSpy).toHaveBeenCalled()
|
||||
modal.close()
|
||||
component.emptyTrash(new Set([1, 2]))
|
||||
modal.componentInstance.confirmClicked.next()
|
||||
expect(trashSpy).toHaveBeenCalledWith([1, 2])
|
||||
})
|
||||
|
||||
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, 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()
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
component.restoreAll(new Set([1, 2]))
|
||||
expect(restoreSpy).toHaveBeenCalledWith([1, 2])
|
||||
})
|
||||
|
||||
it('should support toggle all items in view', () => {
|
||||
component.documentsInTrash = documentsInTrash
|
||||
expect(component.selectedDocuments.size).toEqual(0)
|
||||
const toggleAllSpy = jest.spyOn(component, 'toggleAll')
|
||||
const checkButton = fixture.debugElement.queryAll(
|
||||
By.css('input.form-check-input')
|
||||
)[0]
|
||||
checkButton.nativeElement.dispatchEvent(new Event('click'))
|
||||
checkButton.nativeElement.checked = true
|
||||
checkButton.nativeElement.dispatchEvent(new Event('click'))
|
||||
expect(toggleAllSpy).toHaveBeenCalled()
|
||||
expect(component.selectedDocuments.size).toEqual(documentsInTrash.length)
|
||||
})
|
||||
|
||||
it('should support toggle item', () => {
|
||||
component.selectedDocuments = new Set([1])
|
||||
component.toggleSelected(documentsInTrash[0])
|
||||
expect(component.selectedDocuments.size).toEqual(0)
|
||||
component.toggleSelected(documentsInTrash[0])
|
||||
expect(component.selectedDocuments.size).toEqual(1)
|
||||
})
|
||||
|
||||
it('should support clear selection', () => {
|
||||
component.selectedDocuments = new Set([1])
|
||||
component.clearSelection()
|
||||
expect(component.selectedDocuments.size).toEqual(0)
|
||||
})
|
||||
|
||||
it('should correctly display days remaining', () => {
|
||||
expect(component.getDaysRemaining(documentsInTrash[0])).toBeLessThan(0)
|
||||
const tenDaysAgo = new Date()
|
||||
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10)
|
||||
expect(
|
||||
component.getDaysRemaining({ deleted_at: tenDaysAgo })
|
||||
).toBeGreaterThan(0) // 10 days ago but depends on month
|
||||
})
|
||||
})
|
165
src-ui/src/app/components/admin/trash/trash.component.ts
Normal file
165
src-ui/src/app/components/admin/trash/trash.component.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Component, OnDestroy } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { TrashService } from 'src/app/services/trash.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-trash',
|
||||
templateUrl: './trash.component.html',
|
||||
styleUrl: './trash.component.scss',
|
||||
})
|
||||
export class TrashComponent implements OnDestroy {
|
||||
public documentsInTrash: Document[] = []
|
||||
public selectedDocuments: Set<number> = new Set()
|
||||
public allToggled: boolean = false
|
||||
public page: number = 1
|
||||
public totalDocuments: number
|
||||
public isLoading: boolean = false
|
||||
unsubscribeNotifier: Subject<void> = new Subject()
|
||||
|
||||
constructor(
|
||||
private trashService: TrashService,
|
||||
private toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
private settingsService: SettingsService
|
||||
) {
|
||||
this.reload()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeNotifier.next()
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.isLoading = true
|
||||
this.trashService.getTrash(this.page).subscribe((r) => {
|
||||
this.documentsInTrash = r.results
|
||||
this.totalDocuments = r.count
|
||||
this.isLoading = false
|
||||
this.selectedDocuments.clear()
|
||||
})
|
||||
}
|
||||
|
||||
delete(document: Document) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this document.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Delete`
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
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()
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
emptyTrash(documents?: Set<number>) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete`
|
||||
modal.componentInstance.messageBold = documents
|
||||
? $localize`This operation will permanently delete the selected documents.`
|
||||
: $localize`This operation will permanently delete all documents in the trash.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Delete`
|
||||
modal.componentInstance.confirmClicked
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.trashService
|
||||
.emptyTrash(documents ? Array.from(documents) : null)
|
||||
.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({
|
||||
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({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Document(s) restored`)
|
||||
this.allToggled = false
|
||||
this.reload()
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error restoring document(s)`,
|
||||
err
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
toggleAll(event: PointerEvent) {
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
this.selectedDocuments = new Set(this.documentsInTrash.map((t) => t.id))
|
||||
} else {
|
||||
this.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelected(object: Document) {
|
||||
this.selectedDocuments.has(object.id)
|
||||
? this.selectedDocuments.delete(object.id)
|
||||
: this.selectedDocuments.add(object.id)
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.allToggled = false
|
||||
this.selectedDocuments.clear()
|
||||
}
|
||||
|
||||
getDaysRemaining(document: Document): number {
|
||||
const delay = this.settingsService.get(SETTINGS_KEYS.EMPTY_TRASH_DELAY)
|
||||
const diff = new Date().getTime() - new Date(document.deleted_at).getTime()
|
||||
const days = Math.ceil(diff / (1000 * 3600 * 24))
|
||||
return delay - days
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
@@ -93,33 +93,35 @@
|
||||
</ul>
|
||||
|
||||
<div class="nav-group mt-3 mb-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
@if (savedViewService.loading || savedViewService.sidebarViews?.length > 0) {
|
||||
@if (savedViewService.loading) {
|
||||
<h6 class="sidebar-heading px-3 text-muted">
|
||||
<span i18n>Saved views</span>
|
||||
@if (savedViewService.loading) {
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
}
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
</h6>
|
||||
} @else if (savedViewService.sidebarViews?.length > 0) {
|
||||
<h6 class="sidebar-heading px-3 text-muted">
|
||||
<span i18n>Saved views</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||
@for (view of savedViewService.sidebarViews; track view.id) {
|
||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||
(cdkDragEnded)="onDragEnd($event)">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}"
|
||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||
popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="funnel"></i-bs><span> {{view.name}}</span>
|
||||
</a>
|
||||
@if (settingsService.organizingSidebarSavedViews) {
|
||||
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
||||
<i-bs name="grip-vertical"></i-bs>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||
@for (view of savedViewService.sidebarViews; track view.id) {
|
||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||
(cdkDragEnded)="onDragEnd($event)">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}"
|
||||
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
|
||||
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
|
||||
popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="funnel"></i-bs><span> {{view.name}}</span>
|
||||
</a>
|
||||
@if (settingsService.organizingSidebarSavedViews) {
|
||||
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
||||
<i-bs name="grip-vertical"></i-bs>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-group mt-3 mb-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
@@ -214,6 +216,13 @@
|
||||
<i-bs class="me-1" name="envelope"></i-bs><span> <ng-container i18n>Mail</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
|
||||
<a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash"
|
||||
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
|
||||
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<i-bs class="me-1" name="trash"></i-bs><span> <ng-container i18n>Trash</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@@ -12,6 +12,9 @@
|
||||
z-index: 995; /* Behind the navbar */
|
||||
padding: 50px 0 0; /* Height of navbar */
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
overflow-y: auto;
|
||||
--pngx-sidebar-width: 100%;
|
||||
max-width: var(--pngx-sidebar-width);
|
||||
|
||||
.sidebar-heading .spinner-border {
|
||||
width: 0.8em;
|
||||
@@ -24,15 +27,15 @@
|
||||
|
||||
// These come from the col-* classes for non-slim sidebar, needed for animation
|
||||
@media (min-width: 768px) {
|
||||
max-width: 25%;
|
||||
--pngx-sidebar-width: 25%;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
max-width: 16.66666667%;
|
||||
--pngx-sidebar-width: 16.66666667%;
|
||||
}
|
||||
|
||||
@media (min-width: 2400px) {
|
||||
max-width: 8.33333333%;
|
||||
--pngx-sidebar-width: 8.33333333%;
|
||||
}
|
||||
|
||||
transition: all .2s ease;
|
||||
@@ -109,12 +112,17 @@ main {
|
||||
|
||||
.sidebar-slim-toggler {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
position: fixed;
|
||||
left: calc(var(--pngx-sidebar-width) - 12px);
|
||||
top: 60px;
|
||||
z-index: 996;
|
||||
--bs-btn-padding-x: 0.35rem;
|
||||
--bs-btn-padding-y: 0.125rem;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.sidebar.slim .sidebar-slim-toggler {
|
||||
--pngx-sidebar-width: 50px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
@@ -115,7 +115,7 @@ describe('AppFrameComponent', () => {
|
||||
{
|
||||
provide: SavedViewService,
|
||||
useValue: {
|
||||
initialize: () => {},
|
||||
reload: () => {},
|
||||
listAll: () =>
|
||||
of({
|
||||
all: [saved_views.map((v) => v.id)],
|
||||
@@ -150,6 +150,8 @@ describe('AppFrameComponent', () => {
|
||||
},
|
||||
},
|
||||
PermissionsGuard,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -168,7 +170,7 @@ describe('AppFrameComponent', () => {
|
||||
.mockReturnValue('Hello World')
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
|
||||
savedViewSpy = jest.spyOn(savedViewService, 'initialize')
|
||||
savedViewSpy = jest.spyOn(savedViewService, 'reload')
|
||||
|
||||
fixture = TestBed.createComponent(AppFrameComponent)
|
||||
component = fixture.componentInstance
|
||||
|
@@ -35,7 +35,6 @@ import {
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-app-frame',
|
||||
@@ -74,7 +73,7 @@ export class AppFrameComponent
|
||||
PermissionType.SavedView
|
||||
)
|
||||
) {
|
||||
this.savedViewService.initialize()
|
||||
this.savedViewService.reload()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -65,10 +65,6 @@ form {
|
||||
--pngx-focus-alpha: 0;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mh-75 {
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -86,14 +86,4 @@ describe('ConfirmDialogComponent', () => {
|
||||
expect(closeModalSpy).toHaveBeenCalled()
|
||||
expect(confirmSubjectResult).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support delay confirm', fakeAsync(() => {
|
||||
component.confirmButtonEnabled = false
|
||||
component.delayConfirm(1)
|
||||
expect(component.confirmButtonEnabled).toBeFalsy()
|
||||
tick(1500)
|
||||
fixture.detectChanges()
|
||||
expect(component.confirmButtonEnabled).toBeTruthy()
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
})
|
||||
|
@@ -54,26 +54,6 @@ export class ConfirmDialogComponent {
|
||||
confirmSubject: Subject<boolean>
|
||||
alternativeSubject: Subject<boolean>
|
||||
|
||||
delayConfirm(seconds: number) {
|
||||
const refreshInterval = 0.15 // s
|
||||
|
||||
this.secondsTotal = seconds
|
||||
this.seconds = seconds
|
||||
|
||||
interval(refreshInterval * 1000)
|
||||
.pipe(
|
||||
take(this.secondsTotal / refreshInterval + 2) // need 2 more for animation to complete after 0
|
||||
)
|
||||
.subscribe((count) => {
|
||||
this.seconds = Math.max(
|
||||
0,
|
||||
this.secondsTotal - refreshInterval * (count + 1)
|
||||
)
|
||||
this.confirmButtonEnabled =
|
||||
this.secondsTotal - refreshInterval * count < 0
|
||||
})
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.confirmSubject?.next(false)
|
||||
this.confirmSubject?.complete()
|
||||
|
@@ -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
|
||||
|
@@ -27,6 +27,10 @@
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalsSwitch" [(ngModel)]="deleteOriginals" [disabled]="!userOwnsAllDocuments">
|
||||
<label class="form-check-label" for="deleteOriginalsSwitch" i18n>Delete original documents after successful merge</label>
|
||||
</div>
|
||||
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@@ -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,6 +2,7 @@ import { Component, OnInit } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { Document } from 'src/app/data/document'
|
||||
@@ -16,6 +17,7 @@ export class MergeConfirmDialogComponent
|
||||
implements OnInit
|
||||
{
|
||||
public documentIDs: number[] = []
|
||||
public deleteOriginals: boolean = false
|
||||
private _documents: Document[] = []
|
||||
get documents(): Document[] {
|
||||
return this._documents
|
||||
@@ -27,7 +29,8 @@ export class MergeConfirmDialogComponent
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService
|
||||
private documentService: DocumentService,
|
||||
private permissionService: PermissionsService
|
||||
) {
|
||||
super(activeModal)
|
||||
}
|
||||
@@ -48,4 +51,10 @@ export class MergeConfirmDialogComponent
|
||||
getDocument(documentID: number): Document {
|
||||
return this.documents.find((d) => d.id === documentID)
|
||||
}
|
||||
|
||||
get userOwnsAllDocuments(): boolean {
|
||||
return this.documents.every((d) =>
|
||||
this.permissionService.currentUserOwnsObject(d)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
@@ -44,6 +44,10 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-4">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalSwitch" [(ngModel)]="deleteOriginal" [disabled]="!userOwnsDocument">
|
||||
<label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||
|
@@ -1,12 +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
|
||||
@@ -16,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)
|
||||
@@ -32,6 +37,14 @@ describe('SplitConfirmDialogComponent', () => {
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should load document on init', () => {
|
||||
const getSpy = jest.spyOn(documentService, 'get')
|
||||
component.documentID = 1
|
||||
getSpy.mockReturnValue(of({ id: 1 } as any))
|
||||
component.ngOnInit()
|
||||
expect(documentService.get).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('should update pagesString when pages are added', () => {
|
||||
component.totalPages = 5
|
||||
component.page = 2
|
||||
@@ -79,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()
|
||||
})
|
||||
})
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer'
|
||||
|
||||
@Component({
|
||||
@@ -9,7 +11,10 @@ import { PDFDocumentProxy } from 'ng2-pdf-viewer'
|
||||
templateUrl: './split-confirm-dialog.component.html',
|
||||
styleUrl: './split-confirm-dialog.component.scss',
|
||||
})
|
||||
export class SplitConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
export class SplitConfirmDialogComponent
|
||||
extends ConfirmDialogComponent
|
||||
implements OnInit
|
||||
{
|
||||
public get pagesString(): string {
|
||||
let pagesStr = ''
|
||||
|
||||
@@ -32,8 +37,18 @@ export class SplitConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
private pages: Set<number> = new Set()
|
||||
|
||||
public documentID: number
|
||||
private document: Document
|
||||
public page: number = 1
|
||||
public totalPages: number
|
||||
public deleteOriginal: boolean = false
|
||||
|
||||
public get canSplit(): boolean {
|
||||
return (
|
||||
this.page < this.totalPages &&
|
||||
this.pages.size < this.totalPages - 1 &&
|
||||
!this.pages.has(this.page)
|
||||
)
|
||||
}
|
||||
|
||||
public get pdfSrc(): string {
|
||||
return this.documentService.getPreviewUrl(this.documentID)
|
||||
@@ -41,12 +56,19 @@ export class SplitConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
|
||||
constructor(
|
||||
activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService
|
||||
private documentService: DocumentService,
|
||||
private permissionService: PermissionsService
|
||||
) {
|
||||
super(activeModal)
|
||||
this.confirmButtonEnabled = this.pages.size > 0
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.documentService.get(this.documentID).subscribe((r) => {
|
||||
this.document = r
|
||||
})
|
||||
}
|
||||
|
||||
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
|
||||
this.totalPages = pdf.numPages
|
||||
}
|
||||
@@ -63,4 +85,8 @@ export class SplitConfirmDialogComponent extends ConfirmDialogComponent {
|
||||
this.pages.delete(page)
|
||||
this.confirmButtonEnabled = this.pages.size > 0
|
||||
}
|
||||
|
||||
get userOwnsDocument(): boolean {
|
||||
return this.permissionService.currentUserOwnsObject(this.document)
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,15 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@case (CustomFieldDataType.Boolean) {
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<span>{{field.name}}:</span>
|
||||
<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,28 @@ 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'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Field 5',
|
||||
data_type: CustomFieldDataType.Monetary,
|
||||
extra_data: { default_currency: 'JPY' },
|
||||
},
|
||||
]
|
||||
const document: Document = {
|
||||
id: 1,
|
||||
@@ -31,8 +46,12 @@ describe('CustomFieldDisplayComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CustomFieldDisplayComponent],
|
||||
providers: [DocumentService],
|
||||
imports: [HttpClientTestingModule],
|
||||
imports: [],
|
||||
providers: [
|
||||
DocumentService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents()
|
||||
})
|
||||
|
||||
@@ -98,4 +117,20 @@ describe('CustomFieldDisplayComponent', () => {
|
||||
expect(component.currency).toEqual('EUR')
|
||||
expect(component.value).toEqual(100)
|
||||
})
|
||||
|
||||
it('should respect explicit default currency', () => {
|
||||
component['defaultCurrencyCode'] = 'EUR' // mock default locale injection
|
||||
component.fieldId = 5
|
||||
component.document = {
|
||||
id: 1,
|
||||
title: 'Doc 1',
|
||||
custom_fields: [{ field: 5, document: 1, created: null, value: '100' }],
|
||||
}
|
||||
expect(component.currency).toEqual('JPY')
|
||||
expect(component.value).toEqual(100)
|
||||
})
|
||||
|
||||
it('should show select value', () => {
|
||||
expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
|
||||
})
|
||||
})
|
||||
|
@@ -90,7 +90,9 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
|
||||
)?.value
|
||||
if (this.value && this.field.data_type === CustomFieldDataType.Monetary) {
|
||||
this.currency =
|
||||
this.value.match(/([A-Z]{3})/)?.[0] ?? this.defaultCurrencyCode
|
||||
this.value.match(/([A-Z]{3})/)?.[0] ??
|
||||
this.field.extra_data?.default_currency ??
|
||||
this.defaultCurrencyCode
|
||||
this.value = parseFloat(this.value.replace(this.currency, ''))
|
||||
} else if (
|
||||
this.value?.length &&
|
||||
@@ -115,6 +117,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)
|
||||
|
@@ -0,0 +1,163 @@
|
||||
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
|
||||
<button class="btn btn-sm btn-outline-primary" id="dropdown_toggle" ngbDropdownToggle [disabled]="disabled">
|
||||
<i-bs name="{{icon}}"></i-bs>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
@if (isActive) {
|
||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
|
||||
}
|
||||
</button>
|
||||
<div class="px-3 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown_{{name}}">
|
||||
<div class="list-group list-group-flush">
|
||||
@for (element of selectionModel.queries; track element.id; let i = $index) {
|
||||
<div class="list-group-item px-0 d-flex flex-nowrap">
|
||||
@switch (element.type) {
|
||||
@case (CustomFieldQueryComponentType.Atom) {
|
||||
<ng-container *ngTemplateOutlet="queryAtom; context: { atom: element }"></ng-container>
|
||||
}
|
||||
@case (CustomFieldQueryComponentType.Expression) {
|
||||
<ng-container *ngTemplateOutlet="queryExpression; context: { expression: element }"></ng-container>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #comparisonValueTemplate let-atom="atom">
|
||||
@if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Date) {
|
||||
<input class="form-control" placeholder="yyyy-mm-dd"
|
||||
[(ngModel)]="atom.value"
|
||||
ngbDatepicker
|
||||
#d="ngbDatepicker" />
|
||||
<button class="btn btn-sm btn-outline-secondary rounded-end" (click)="d.toggle()" type="button">
|
||||
<i-bs name="calendar-event"></i-bs>
|
||||
</button>
|
||||
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Float || getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Integer) {
|
||||
<input class="w-25 form-control rounded-end" type="number" [(ngModel)]="atom.value" [disabled]="disabled">
|
||||
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Boolean) {
|
||||
<select class="w-25 form-select rounded-end" [(ngModel)]="atom.value" [disabled]="disabled">
|
||||
<option value="true" i18n>True</option>
|
||||
<option value="false" i18n>False</option>
|
||||
</select>
|
||||
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Select) {
|
||||
<ng-select
|
||||
class="paperless-input-select rounded-end"
|
||||
[items]="getSelectOptionsForField(atom.field)"
|
||||
[(ngModel)]="atom.value"
|
||||
[disabled]="disabled"
|
||||
(mousedown)="$event.stopImmediatePropagation()"
|
||||
></ng-select>
|
||||
} @else {
|
||||
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="atom.value" [disabled]="disabled">
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #queryAtom let-atom="atom">
|
||||
<div class="input-group input-group-sm">
|
||||
<ng-select
|
||||
class="paperless-input-select"
|
||||
[items]="customFields"
|
||||
[(ngModel)]="atom.field"
|
||||
[disabled]="disabled"
|
||||
bindLabel="name"
|
||||
bindValue="id"
|
||||
(mousedown)="$event.stopImmediatePropagation()"
|
||||
></ng-select>
|
||||
<select class="w-25 form-select" [(ngModel)]="atom.operator" [disabled]="disabled">
|
||||
<option *ngFor="let operator of getOperatorsForField(atom.field)" [ngValue]="operator.value">{{operator.label}}</option>
|
||||
</select>
|
||||
@switch (atom.operator) {
|
||||
@case (CustomFieldQueryOperator.Exists) {
|
||||
<select class="w-25 form-select rounded-end" [(ngModel)]="atom.value" [disabled]="disabled">
|
||||
<option value="true" i18n>True</option>
|
||||
<option value="false" i18n>False</option>
|
||||
</select>
|
||||
}
|
||||
@case (CustomFieldQueryOperator.IsNull) {
|
||||
<select class="w-25 form-select rounded-end" [(ngModel)]="atom.value" [disabled]="disabled">
|
||||
<option value="true" i18n>True</option>
|
||||
<option value="false" i18n>False</option>
|
||||
</select>
|
||||
}
|
||||
@case (CustomFieldQueryOperator.GreaterThanOrEqual) {
|
||||
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
|
||||
}
|
||||
@case (CustomFieldQueryOperator.LessThanOrEqual) {
|
||||
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
|
||||
}
|
||||
@case (CustomFieldQueryOperator.GreaterThan) {
|
||||
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
|
||||
}
|
||||
@case (CustomFieldQueryOperator.LessThan) {
|
||||
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
|
||||
}
|
||||
@case (CustomFieldQueryOperator.Contains) {
|
||||
<pngx-input-document-link [(ngModel)]="atom.value" class="w-25 form-select doc-link-select p-0" placeholder="Search docs..." i18n-placeholder [minimal]="true"></pngx-input-document-link>
|
||||
}
|
||||
@case (CustomFieldQueryOperator.In) {
|
||||
<ng-select
|
||||
class="paperless-input-select rounded-end"
|
||||
[items]="getSelectOptionsForField(atom.field)"
|
||||
[(ngModel)]="atom.value"
|
||||
[disabled]="disabled"
|
||||
[multiple]="true"
|
||||
(mousedown)="$event.stopImmediatePropagation()"
|
||||
></ng-select>
|
||||
}
|
||||
@case (CustomFieldQueryOperator.Exact) {
|
||||
<ng-container *ngTemplateOutlet="comparisonValueTemplate; context: { atom: atom }"></ng-container>
|
||||
}
|
||||
@default {
|
||||
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="atom.value" [disabled]="disabled">
|
||||
}
|
||||
}
|
||||
<button class="btn btn-link btn-sm text-danger pe-0" type="button" (click)="removeElement(atom)" [disabled]="disabled">
|
||||
<i-bs name="x-circle"></i-bs>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #queryExpression let-expression="expression">
|
||||
<div class="d-flex w-100">
|
||||
<div class="d-flex flex-grow-1 flex-column">
|
||||
<div class="btn-group btn-group-xs" role="group">
|
||||
<input [(ngModel)]="expression.operator" type="radio" class="btn-check" id="logicalOperatorOr_{{expression.id}}" name="logicalOperatorOr_{{expression.id}}" value="OR" [disabled]="expression.depth > 0 && expression.value.length < 2">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorOr_{{expression.id}}" i18n>Any</label>
|
||||
<input [(ngModel)]="expression.operator" type="radio" class="btn-check" id="logicalOperatorAnd_{{expression.id}}" name="logicalOperatorAnd_{{expression.id}}" value="AND" [disabled]="expression.depth > 0 && expression.value.length < 2">
|
||||
<label class="btn btn-outline-primary" for="logicalOperatorAnd_{{expression.id}}" i18n>All</label>
|
||||
@if (expression.negatable) {
|
||||
<input [(ngModel)]="expression.operator" type="radio" class="btn-check" id="logicalOperatorNot_{{expression.id}}" name="logicalOperatorNot_{{expression.id}}" value="NOT">
|
||||
<label class="btn btn-outline-secondary" for="logicalOperatorNot_{{expression.id}}" i18n>Not</label>
|
||||
}
|
||||
</div>
|
||||
<div class="list-group list-group-flush mb-n2">
|
||||
@for (element of expression.value; track element.id; let i = $index) {
|
||||
<div class="list-group-item px-0 d-flex flex-nowrap">
|
||||
@switch (element.type) {
|
||||
@case (CustomFieldQueryComponentType.Atom) {
|
||||
<ng-container *ngTemplateOutlet="queryAtom; context: { atom: element }"></ng-container>
|
||||
}
|
||||
@case (CustomFieldQueryComponentType.Expression) {
|
||||
<ng-container *ngTemplateOutlet="queryExpression; context: { expression: element }"></ng-container>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group-vertical ms-2 ps-2 border-start" role="group" aria-label="Vertical button group">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary text-primary" title="Add query" i18n-title (click)="addAtom(expression)" [disabled]="disabled || expression.value.length === CUSTOM_FIELD_QUERY_MAX_ATOMS">
|
||||
<i-bs name="node-plus"></i-bs>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary text-primary" title="Add expression" i18n-title (click)="addExpression(expression)" [disabled]="disabled || expression.depth === CUSTOM_FIELD_QUERY_MAX_DEPTH">
|
||||
<i-bs name="braces"></i-bs>
|
||||
</button>
|
||||
@if (expression.depth > 0) {
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary text-danger" (click)="removeElement(expression)" [disabled]="disabled">
|
||||
<i-bs name="x-circle"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
@@ -0,0 +1,43 @@
|
||||
.dropdown-menu {
|
||||
width: 370px;
|
||||
@media(min-width: 768px) {
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .ng-select-container {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
::ng-deep .rounded-end .ng-select-container {
|
||||
border-top-right-radius: var(--bs-border-radius) !important;
|
||||
border-bottom-right-radius: var(--bs-border-radius) !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .ng-select {
|
||||
max-width: 100px;
|
||||
min-width: 35%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
::ng-deep .doc-link-select {
|
||||
padding-top: 0 !important;
|
||||
border-top-right-radius: var(--bs-border-radius) !important;
|
||||
border-bottom-right-radius: var(--bs-border-radius) !important;
|
||||
background-image: none !important;
|
||||
|
||||
.ng-select-container,
|
||||
.ng-select.ng-select-opened > .ng-select-container {
|
||||
border: none !important;
|
||||
min-height: 34px !important;
|
||||
background: none !important;
|
||||
}
|
||||
.ng-select {
|
||||
max-width: 200px;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
@@ -0,0 +1,320 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
CustomFieldQueriesModel,
|
||||
CustomFieldsQueryDropdownComponent,
|
||||
} from './custom-fields-query-dropdown.component'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { of } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import {
|
||||
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
|
||||
CustomFieldQueryLogicalOperator,
|
||||
CustomFieldQueryOperatorGroups,
|
||||
} from 'src/app/data/custom-field-query'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import {
|
||||
CustomFieldQueryExpression,
|
||||
CustomFieldQueryAtom,
|
||||
CustomFieldQueryElement,
|
||||
} from 'src/app/utils/custom-field-query-element'
|
||||
|
||||
const customFields = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test Field',
|
||||
data_type: CustomFieldDataType.String,
|
||||
extra_data: {},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Test Select Field',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: { select_options: ['Option 1', 'Option 2'] },
|
||||
},
|
||||
]
|
||||
|
||||
describe('CustomFieldsQueryDropdownComponent', () => {
|
||||
let component: CustomFieldsQueryDropdownComponent
|
||||
let fixture: ComponentFixture<CustomFieldsQueryDropdownComponent>
|
||||
let customFieldsService: CustomFieldsService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CustomFieldsQueryDropdownComponent],
|
||||
imports: [NgbDropdownModule, NgxBootstrapIconsModule.pick(allIcons)],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
count: customFields.length,
|
||||
all: customFields.map((f) => f.id),
|
||||
results: customFields,
|
||||
})
|
||||
)
|
||||
fixture = TestBed.createComponent(CustomFieldsQueryDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
component.icon = 'ui-radios'
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should initialize custom fields on creation', () => {
|
||||
expect(component.customFields).toEqual(customFields)
|
||||
})
|
||||
|
||||
it('should add an expression when opened if queries are empty', () => {
|
||||
component.selectionModel.clear()
|
||||
component.onOpenChange(true)
|
||||
expect(component.selectionModel.queries.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should support reset the selection model', () => {
|
||||
component.selectionModel.addExpression()
|
||||
component.reset()
|
||||
expect(component.selectionModel.isEmpty()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should get operators for a field', () => {
|
||||
const field: CustomField = {
|
||||
id: 1,
|
||||
name: 'Test Field',
|
||||
data_type: CustomFieldDataType.String,
|
||||
extra_data: {},
|
||||
}
|
||||
component.customFields = [field]
|
||||
const operators = component.getOperatorsForField(1)
|
||||
expect(operators.length).toEqual(
|
||||
[
|
||||
...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
|
||||
CustomFieldQueryOperatorGroups.Basic
|
||||
],
|
||||
...CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
|
||||
CustomFieldQueryOperatorGroups.String
|
||||
],
|
||||
].length
|
||||
)
|
||||
|
||||
// Fallback to basic operators if field is not found
|
||||
const operators2 = component.getOperatorsForField(2)
|
||||
expect(operators2.length).toEqual(
|
||||
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[
|
||||
CustomFieldQueryOperatorGroups.Basic
|
||||
].length
|
||||
)
|
||||
})
|
||||
|
||||
it('should get select options for a field', () => {
|
||||
const field: CustomField = {
|
||||
id: 1,
|
||||
name: 'Test Field',
|
||||
data_type: CustomFieldDataType.Select,
|
||||
extra_data: { select_options: ['Option 1', 'Option 2'] },
|
||||
}
|
||||
component.customFields = [field]
|
||||
const options = component.getSelectOptionsForField(1)
|
||||
expect(options).toEqual(['Option 1', 'Option 2'])
|
||||
|
||||
// Fallback to empty array if field is not found
|
||||
const options2 = component.getSelectOptionsForField(2)
|
||||
expect(options2).toEqual([])
|
||||
})
|
||||
|
||||
it('should remove an element from the selection model', () => {
|
||||
const expression = new CustomFieldQueryExpression()
|
||||
const atom = new CustomFieldQueryAtom()
|
||||
;(expression.value as CustomFieldQueryElement[]).push(atom)
|
||||
component.selectionModel.addExpression(expression)
|
||||
component.removeElement(atom)
|
||||
expect(component.selectionModel.isEmpty()).toBeTruthy()
|
||||
const expression2 = new CustomFieldQueryExpression([
|
||||
CustomFieldQueryLogicalOperator.And,
|
||||
[
|
||||
[1, 'icontains', 'test'],
|
||||
[2, 'icontains', 'test'],
|
||||
],
|
||||
])
|
||||
component.selectionModel.addExpression(expression2)
|
||||
component.removeElement(expression2)
|
||||
expect(component.selectionModel.isEmpty()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should emit selectionModelChange when model changes', () => {
|
||||
const nextSpy = jest.spyOn(component.selectionModelChange, 'next')
|
||||
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
|
||||
component.selectionModel.addAtom(atom)
|
||||
atom.changed.next(atom)
|
||||
expect(nextSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should complete selection model subscription when new selection model is set', () => {
|
||||
const completeSpy = jest.spyOn(component.selectionModel.changed, 'complete')
|
||||
const selectionModel = new CustomFieldQueriesModel()
|
||||
component.selectionModel = selectionModel
|
||||
expect(completeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support adding an atom', () => {
|
||||
const expression = new CustomFieldQueryExpression()
|
||||
component.addAtom(expression)
|
||||
expect(expression.value.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should support adding an expression', () => {
|
||||
const expression = new CustomFieldQueryExpression()
|
||||
component.addExpression(expression)
|
||||
expect(expression.value.length).toBe(1)
|
||||
})
|
||||
|
||||
it('should support getting a custom field by ID', () => {
|
||||
expect(component.getCustomFieldByID(1)).toEqual(customFields[0])
|
||||
})
|
||||
|
||||
it('should sanitize name from title', () => {
|
||||
component.title = 'Test Title'
|
||||
expect(component.name).toBe('test_title')
|
||||
})
|
||||
|
||||
describe('CustomFieldQueriesModel', () => {
|
||||
let model: CustomFieldQueriesModel
|
||||
|
||||
beforeEach(() => {
|
||||
model = new CustomFieldQueriesModel()
|
||||
})
|
||||
|
||||
it('should initialize with empty queries', () => {
|
||||
expect(model.queries).toEqual([])
|
||||
})
|
||||
|
||||
it('should clear queries and fire event', () => {
|
||||
const nextSpy = jest.spyOn(model.changed, 'next')
|
||||
model.addExpression()
|
||||
model.clear()
|
||||
expect(model.queries).toEqual([])
|
||||
expect(nextSpy).toHaveBeenCalledWith(model)
|
||||
})
|
||||
|
||||
it('should clear queries without firing event', () => {
|
||||
const nextSpy = jest.spyOn(model.changed, 'next')
|
||||
model.addExpression()
|
||||
model.clear(false)
|
||||
expect(model.queries).toEqual([])
|
||||
expect(nextSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should validate an empty model as invalid', () => {
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should validate a model with valid expression as valid', () => {
|
||||
const expression = new CustomFieldQueryExpression()
|
||||
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
|
||||
const atom2 = new CustomFieldQueryAtom([2, 'icontains', 'test'])
|
||||
const expression2 = new CustomFieldQueryExpression()
|
||||
expression2.addAtom(atom)
|
||||
expression2.addAtom(atom2)
|
||||
expression.addExpression(expression2)
|
||||
model.addExpression(expression)
|
||||
expect(model.isValid()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should validate a model with invalid expression as invalid', () => {
|
||||
const expression = new CustomFieldQueryExpression()
|
||||
model.addExpression(expression)
|
||||
expect(model.isValid()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should validate an atom with in or contains operator', () => {
|
||||
const atom = new CustomFieldQueryAtom([1, 'in', '[1,2,3]'])
|
||||
expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
|
||||
atom.operator = 'contains'
|
||||
atom.value = [1, 2, 3]
|
||||
expect(model['validateAtom'].apply(null, [atom])).toBeTruthy()
|
||||
atom.value = null
|
||||
expect(model['validateAtom'].apply(null, [atom])).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should check if model is empty', () => {
|
||||
expect(model.isEmpty()).toBeTruthy()
|
||||
model.addExpression()
|
||||
expect(model.isEmpty()).toBeTruthy()
|
||||
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
|
||||
model.addAtom(atom)
|
||||
expect(model.isEmpty()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should add an atom to the model', () => {
|
||||
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
|
||||
model.addAtom(atom)
|
||||
expect(model.queries.length).toBe(1)
|
||||
expect(
|
||||
(model.queries[0] as CustomFieldQueryExpression).value.length
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('should add an expression to the model, propagate changes', () => {
|
||||
const expression = new CustomFieldQueryExpression()
|
||||
model.addExpression(expression)
|
||||
expect(model.queries.length).toBe(1)
|
||||
const expression2 = new CustomFieldQueryExpression([
|
||||
CustomFieldQueryLogicalOperator.And,
|
||||
[
|
||||
[1, 'icontains', 'test'],
|
||||
[2, 'icontains', 'test'],
|
||||
],
|
||||
])
|
||||
model.addExpression(expression2)
|
||||
const nextSpy = jest.spyOn(model.changed, 'next')
|
||||
expression2.changed.next(expression2)
|
||||
expect(nextSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove an element from the model', () => {
|
||||
const expression = new CustomFieldQueryExpression([
|
||||
CustomFieldQueryLogicalOperator.And,
|
||||
[
|
||||
[1, 'icontains', 'test'],
|
||||
[2, 'icontains', 'test'],
|
||||
],
|
||||
])
|
||||
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
|
||||
const expression2 = new CustomFieldQueryExpression([
|
||||
CustomFieldQueryLogicalOperator.And,
|
||||
[
|
||||
[3, 'icontains', 'test'],
|
||||
[4, 'icontains', 'test'],
|
||||
],
|
||||
])
|
||||
expression.addAtom(atom)
|
||||
expression2.addExpression(expression)
|
||||
model.addExpression(expression2)
|
||||
model.removeElement(atom)
|
||||
expect(model.queries.length).toBe(1)
|
||||
model.removeElement(expression2)
|
||||
})
|
||||
|
||||
it('should fire changed event when an atom changes', () => {
|
||||
const nextSpy = jest.spyOn(model.changed, 'next')
|
||||
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
|
||||
model.addAtom(atom)
|
||||
atom.changed.next(atom)
|
||||
expect(nextSpy).toHaveBeenCalledWith(model)
|
||||
})
|
||||
|
||||
it('should complete changed subject when element is removed', () => {
|
||||
const expression = new CustomFieldQueryExpression()
|
||||
const atom = new CustomFieldQueryAtom([1, 'icontains', 'test'])
|
||||
;(expression.value as CustomFieldQueryElement[]).push(atom)
|
||||
model.addExpression(expression)
|
||||
const completeSpy = jest.spyOn(atom.changed, 'complete')
|
||||
model.removeElement(atom)
|
||||
expect(completeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,295 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, first, takeUntil } from 'rxjs'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import {
|
||||
CustomFieldQueryElementType,
|
||||
CustomFieldQueryOperator,
|
||||
CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE,
|
||||
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
|
||||
CustomFieldQueryOperatorGroups,
|
||||
CUSTOM_FIELD_QUERY_OPERATOR_LABELS,
|
||||
CUSTOM_FIELD_QUERY_MAX_DEPTH,
|
||||
CUSTOM_FIELD_QUERY_MAX_ATOMS,
|
||||
} from 'src/app/data/custom-field-query'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import {
|
||||
CustomFieldQueryElement,
|
||||
CustomFieldQueryExpression,
|
||||
CustomFieldQueryAtom,
|
||||
} from 'src/app/utils/custom-field-query-element'
|
||||
import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
|
||||
|
||||
export class CustomFieldQueriesModel {
|
||||
public queries: CustomFieldQueryElement[] = []
|
||||
|
||||
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
||||
|
||||
public clear(fireEvent = true) {
|
||||
this.queries = []
|
||||
if (fireEvent) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
}
|
||||
|
||||
public isValid(): boolean {
|
||||
return (
|
||||
this.queries.length > 0 &&
|
||||
this.validateExpression(this.queries[0] as CustomFieldQueryExpression)
|
||||
)
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return (
|
||||
this.queries.length === 0 ||
|
||||
(this.queries.length === 1 && this.queries[0].value.length === 0)
|
||||
)
|
||||
}
|
||||
|
||||
private validateAtom(atom: CustomFieldQueryAtom) {
|
||||
let valid = !!(atom.field && atom.operator && atom.value !== null)
|
||||
if (
|
||||
[
|
||||
CustomFieldQueryOperator.In.valueOf(),
|
||||
CustomFieldQueryOperator.Contains.valueOf(),
|
||||
].includes(atom.operator) &&
|
||||
atom.value
|
||||
) {
|
||||
valid = valid && atom.value.length > 0
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
private validateExpression(expression: CustomFieldQueryExpression) {
|
||||
return (
|
||||
expression.operator &&
|
||||
expression.value.length > 0 &&
|
||||
(expression.value as CustomFieldQueryElement[]).every((e) =>
|
||||
e.type === CustomFieldQueryElementType.Atom
|
||||
? this.validateAtom(e as CustomFieldQueryAtom)
|
||||
: this.validateExpression(e as CustomFieldQueryExpression)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public addAtom(atom: CustomFieldQueryAtom) {
|
||||
if (this.queries.length === 0) {
|
||||
this.addExpression()
|
||||
}
|
||||
;(this.queries[0].value as CustomFieldQueryElement[]).push(atom)
|
||||
atom.changed.subscribe(() => {
|
||||
if (atom.field && atom.operator && atom.value) {
|
||||
this.changed.next(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public addExpression(
|
||||
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
|
||||
) {
|
||||
if (this.queries.length > 0) {
|
||||
;(
|
||||
(this.queries[0] as CustomFieldQueryExpression)
|
||||
.value as CustomFieldQueryElement[]
|
||||
).push(expression)
|
||||
} else {
|
||||
this.queries.push(expression)
|
||||
}
|
||||
expression.changed.subscribe(() => {
|
||||
this.changed.next(this)
|
||||
})
|
||||
}
|
||||
|
||||
private findElement(
|
||||
queryElement: CustomFieldQueryElement,
|
||||
elements: any[]
|
||||
): CustomFieldQueryElement {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
if (elements[i] === queryElement) {
|
||||
return elements.splice(i, 1)[0]
|
||||
} else if (elements[i].type === CustomFieldQueryElementType.Expression) {
|
||||
return this.findElement(
|
||||
queryElement,
|
||||
elements[i].value as CustomFieldQueryElement[]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removeElement(queryElement: CustomFieldQueryElement) {
|
||||
let foundComponent
|
||||
for (let i = 0; i < this.queries.length; i++) {
|
||||
let query = this.queries[i]
|
||||
if (query === queryElement) {
|
||||
foundComponent = this.queries.splice(i, 1)[0]
|
||||
break
|
||||
} else if (query.type === CustomFieldQueryElementType.Expression) {
|
||||
foundComponent = this.findElement(queryElement, query.value as any[])
|
||||
}
|
||||
}
|
||||
if (foundComponent) {
|
||||
foundComponent.changed.complete()
|
||||
if (this.isEmpty()) {
|
||||
this.clear()
|
||||
}
|
||||
this.changed.next(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-custom-fields-query-dropdown',
|
||||
templateUrl: './custom-fields-query-dropdown.component.html',
|
||||
styleUrls: ['./custom-fields-query-dropdown.component.scss'],
|
||||
})
|
||||
export class CustomFieldsQueryDropdownComponent implements OnDestroy {
|
||||
public CustomFieldQueryComponentType = CustomFieldQueryElementType
|
||||
public CustomFieldQueryOperator = CustomFieldQueryOperator
|
||||
public CustomFieldDataType = CustomFieldDataType
|
||||
public CUSTOM_FIELD_QUERY_MAX_DEPTH = CUSTOM_FIELD_QUERY_MAX_DEPTH
|
||||
public CUSTOM_FIELD_QUERY_MAX_ATOMS = CUSTOM_FIELD_QUERY_MAX_ATOMS
|
||||
public popperOptions = popperOptionsReenablePreventOverflow
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Input()
|
||||
filterPlaceholder: string = ''
|
||||
|
||||
@Input()
|
||||
icon: string
|
||||
|
||||
@Input()
|
||||
allowSelectNone: boolean = false
|
||||
|
||||
@Input()
|
||||
editing = false
|
||||
|
||||
@Input()
|
||||
applyOnClose = false
|
||||
|
||||
get name(): string {
|
||||
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
||||
}
|
||||
|
||||
@Input()
|
||||
disabled: boolean = false
|
||||
|
||||
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||
|
||||
private _selectionModel: CustomFieldQueriesModel
|
||||
|
||||
@Input()
|
||||
set selectionModel(model: CustomFieldQueriesModel) {
|
||||
if (this._selectionModel) {
|
||||
this._selectionModel.changed.complete()
|
||||
}
|
||||
model.changed.subscribe(() => {
|
||||
this.onModelChange()
|
||||
})
|
||||
this._selectionModel = model
|
||||
}
|
||||
|
||||
get selectionModel(): CustomFieldQueriesModel {
|
||||
return this._selectionModel
|
||||
}
|
||||
|
||||
private onModelChange() {
|
||||
if (this.selectionModel.isEmpty() || this.selectionModel.isValid()) {
|
||||
this.selectionModelChange.next(this.selectionModel)
|
||||
this.selectionModel.isEmpty() && this.dropdown?.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Output()
|
||||
selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
|
||||
|
||||
customFields: CustomField[] = []
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(protected customFieldsService: CustomFieldsService) {
|
||||
this.selectionModel = new CustomFieldQueriesModel()
|
||||
this.getFields()
|
||||
this.reset()
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(this)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
public onOpenChange(open: boolean) {
|
||||
if (open && this.selectionModel.queries.length === 0) {
|
||||
this.selectionModel.addExpression()
|
||||
}
|
||||
}
|
||||
|
||||
public get isActive(): boolean {
|
||||
return (
|
||||
(this.selectionModel.queries[0] as CustomFieldQueryExpression)?.value
|
||||
?.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
private getFields() {
|
||||
this.customFieldsService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => {
|
||||
this.customFields = result.results
|
||||
})
|
||||
}
|
||||
|
||||
public getCustomFieldByID(id: number): CustomField {
|
||||
return this.customFields.find((field) => field.id === id)
|
||||
}
|
||||
|
||||
public addAtom(expression: CustomFieldQueryExpression) {
|
||||
expression.addAtom()
|
||||
}
|
||||
|
||||
public addExpression(expression: CustomFieldQueryExpression) {
|
||||
expression.addExpression()
|
||||
}
|
||||
|
||||
public removeElement(element: CustomFieldQueryElement) {
|
||||
this.selectionModel.removeElement(element)
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.selectionModel.clear(false)
|
||||
this.selectionModel.changed.next(this.selectionModel)
|
||||
}
|
||||
|
||||
getOperatorsForField(
|
||||
fieldID: number
|
||||
): Array<{ value: string; label: string }> {
|
||||
const field = this.customFields.find((field) => field.id === fieldID)
|
||||
const groups: CustomFieldQueryOperatorGroups[] = field
|
||||
? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type]
|
||||
: [CustomFieldQueryOperatorGroups.Basic]
|
||||
const operators = groups.flatMap(
|
||||
(group) => CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP[group]
|
||||
)
|
||||
return operators.map((operator) => ({
|
||||
value: operator,
|
||||
label: CUSTOM_FIELD_QUERY_OPERATOR_LABELS[operator],
|
||||
}))
|
||||
}
|
||||
|
||||
getSelectOptionsForField(fieldID: number): string[] {
|
||||
const field = this.customFields.find((field) => field.id === fieldID)
|
||||
if (field) {
|
||||
return field.extra_data['select_options']
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
<div class="btn-group w-100" ngbDropdown role="group">
|
||||
<div class="btn-group w-100" ngbDropdown role="group" [popperOptions]="popperOptions">
|
||||
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="createdDateBefore || createdDateAfter ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
|
||||
<i-bs width="1em" height="1em" name="calendar-event-fill"></i-bs>
|
||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||
<div class="pe-2 pe-lg-4">
|
||||
<div class="pe-4">
|
||||
{{rd.name}}
|
||||
</div>
|
||||
<div class="text-muted small pe-2">
|
||||
@@ -28,20 +28,19 @@
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>After</div>
|
||||
<div class="selected-icon">
|
||||
@if (createdDateAfter) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearCreatedAfter()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearCreatedAfter()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>After</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="createdDateAfter" ngbDatepicker #createdDateAfterPicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="createdDateAfterPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
@@ -49,20 +48,19 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>Before</div>
|
||||
<div class="selected-icon">
|
||||
@if (createdDateBefore) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearCreatedBefore()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearCreatedBefore()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>Before</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="createdDateBefore" ngbDatepicker #createdDateBeforePicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="createdDateBeforePicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
@@ -83,7 +81,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-100 align-items-center ps-2">
|
||||
<div class="pe-2 pe-lg-4">
|
||||
<div class="pe-4">
|
||||
{{rd.name}}
|
||||
</div>
|
||||
<div class="text-muted small pe-2">
|
||||
@@ -94,20 +92,19 @@
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>After</div>
|
||||
<div class="selected-icon">
|
||||
@if (addedDateAfter) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearAddedAfter()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedAfter()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>After</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="addedDateAfter" ngbDatepicker #addedDateAfterPicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="addedDateAfterPicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
@@ -115,20 +112,19 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
|
||||
<div class="list-group-item d-flex p-2" role="menuitem">
|
||||
|
||||
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
|
||||
<div i18n>Before</div>
|
||||
<div class="selected-icon">
|
||||
@if (addedDateBefore) {
|
||||
<a class="btn btn-link p-0 m-0" (click)="clearAddedBefore()">
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
<small i18n>Clear</small>
|
||||
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedBefore()">
|
||||
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
|
||||
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input class="form-control" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
<div class="input-group input-group-sm small ps-1 pe-2">
|
||||
<span class="input-group-text w-25 small text-muted" i18n>Before</span>
|
||||
<input class="form-control small" [placeholder]="datePlaceHolder" (dateSelect)="onChangeDebounce()" (change)="onChangeDebounce()" (keypress)="onKeyPress($event)"
|
||||
maxlength="10" [(ngModel)]="addedDateBefore" ngbDatepicker #addedDateBeforePicker="ngbDatepicker">
|
||||
<button class="btn btn-outline-secondary" (click)="addedDateBeforePicker.toggle()" type="button">
|
||||
<i-bs width="1em" height="1em" name="calendar"></i-bs>
|
||||
|
@@ -5,6 +5,12 @@
|
||||
--bs-dropdown-min-width: 40rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.border-end {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -14,3 +20,24 @@
|
||||
min-width: 1em;
|
||||
min-height: 1em;
|
||||
}
|
||||
|
||||
.input-group-sm {
|
||||
.form-control {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.focus-variants {
|
||||
.variant-focused {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
.variant-unfocused {
|
||||
display: none;
|
||||
}
|
||||
.variant-focused {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -11,6 +11,7 @@ import { Subject, Subscription } from 'rxjs'
|
||||
import { debounceTime } from 'rxjs/operators'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
import { popperOptionsReenablePreventOverflow } from 'src/app/utils/popper-options'
|
||||
|
||||
export interface DateSelection {
|
||||
createdBefore?: string
|
||||
@@ -35,6 +36,8 @@ export enum RelativeDate {
|
||||
providers: [{ provide: NgbDateAdapter, useClass: ISODateAdapter }],
|
||||
})
|
||||
export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||
public popperOptions = popperOptionsReenablePreventOverflow
|
||||
|
||||
constructor(settings: SettingsService) {
|
||||
this.datePlaceHolder = settings.getLocalizedDateInputFormat()
|
||||
}
|
||||
|
@@ -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,28 @@
|
||||
@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>
|
||||
}
|
||||
@case (CustomFieldDataType.Monetary) {
|
||||
<div class="my-3">
|
||||
<pngx-input-text i18n-title title="Default Currency" hint="3-character currency code" i18n-hint formControlName="default_currency" placeholder="Use locale" i18n-placeholder autocomplete="off"></pngx-input-text>
|
||||
</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,10 @@ 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)]),
|
||||
default_currency: new FormControl(null),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,4 +102,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)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user