mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-18 17:36:25 +00:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f045f4494 | ||
|
|
4fdb28c8d6 | ||
|
|
f1049cf889 | ||
|
|
8d664fad56 | ||
|
|
f6ddcfa839 | ||
|
|
ce59f2ad5e | ||
|
|
0de00a4ac1 | ||
|
|
bec72dffeb | ||
|
|
463e95367c | ||
|
|
1739de2694 | ||
|
|
fd8db27a88 | ||
|
|
9ddf14bebe | ||
|
|
1cf8ea3aba | ||
|
|
134993fce6 | ||
|
|
ff1955e014 | ||
|
|
d83bbdc50b | ||
|
|
907b6d1294 | ||
|
|
4e7bb1c8da | ||
|
|
09ab694d05 | ||
|
|
03ced65d5f | ||
|
|
01d919cf31 | ||
|
|
8d5f331e63 | ||
|
|
ed556ead6f | ||
|
|
e10a904f33 | ||
|
|
f2a05b61da | ||
|
|
21f96f0679 | ||
|
|
2ffabd54e5 | ||
|
|
1197437750 | ||
|
|
d1339374d0 | ||
|
|
5ba4b9d6b2 | ||
|
|
b386ea9426 | ||
|
|
197174f400 | ||
|
|
97dceba783 | ||
|
|
2ed4400827 | ||
|
|
58cbcbd6ef | ||
|
|
4aeb2e1a74 | ||
|
|
45c5f81b34 | ||
|
|
13201dbfff | ||
|
|
0b1523f4e5 | ||
|
|
cd3b1a221e | ||
|
|
4855f4b8b1 | ||
|
|
6587470033 | ||
|
|
6487dab132 | ||
|
|
b643a68fa3 | ||
|
|
b60e16fe33 | ||
|
|
c508be6ecd | ||
|
|
3b2d4fe876 | ||
|
|
b47f301831 | ||
|
|
a79b9de1a2 | ||
|
|
e98da2e72c | ||
|
|
718171a125 | ||
|
|
aaa130e20d | ||
|
|
4606caeaa8 | ||
|
|
4813a7bc70 | ||
|
|
fb82aa0ee1 | ||
|
|
c7e0c32226 | ||
|
|
607adf44f3 | ||
|
|
625780899d | ||
|
|
25542c56b9 | ||
|
|
45e2b7f814 | ||
|
|
6b34f592df | ||
|
|
6cf732e6ec | ||
|
|
dfd959839f | ||
|
|
d165e89ac3 | ||
|
|
421a87c94b | ||
|
|
b55529b913 | ||
|
|
c62d892969 | ||
|
|
9e6aa55230 | ||
|
|
6090305b77 | ||
|
|
d1b516a089 | ||
|
|
89aff63e52 | ||
|
|
11e9c4d8cc | ||
|
|
9d84e95771 | ||
|
|
2aced1c305 | ||
|
|
454098630b | ||
|
|
8f3ab2791b | ||
|
|
61209b1057 | ||
|
|
38a817e887 | ||
|
|
5e3d1b26e7 | ||
|
|
4996b7e5f7 | ||
|
|
2c8fddb554 | ||
|
|
ae05011062 | ||
|
|
50a6b7e154 | ||
|
|
d0ce4113e0 | ||
|
|
d55900b877 | ||
|
|
2a73ab4693 | ||
|
|
2aea220c6d | ||
|
|
b0c305e852 | ||
|
|
73a77d2a45 | ||
|
|
3a011e7c04 | ||
|
|
d48b75d862 | ||
|
|
f6e26d5953 | ||
|
|
7863780883 | ||
|
|
c2ac9a26a2 | ||
|
|
58e8f796d1 | ||
|
|
ba0f4718e5 | ||
|
|
6fb4daf03e | ||
|
|
f6c34494a7 | ||
|
|
1e4d284b30 | ||
|
|
1f3406fd77 | ||
|
|
398faf36fc | ||
|
|
ce841d4196 | ||
|
|
c77f8acf41 | ||
|
|
212674f9df | ||
|
|
283ced56d1 | ||
|
|
530e57151d | ||
|
|
1141c3f361 | ||
|
|
49416d3372 | ||
|
|
88ae60a4a0 | ||
|
|
6651c80fb9 | ||
|
|
6d6650d5f6 | ||
|
|
6df252c99b | ||
|
|
5881f05dbc | ||
|
|
00eba3b223 | ||
|
|
f7ab8d23a7 | ||
|
|
85b596d20d | ||
|
|
4d43f6b63d | ||
|
|
ea1eb551a7 | ||
|
|
5842944d1e | ||
|
|
5781a0d51f | ||
|
|
e5f48739a0 | ||
|
|
d378c861f6 | ||
|
|
9466bfdb00 |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -9,7 +9,7 @@ Please include a summary of the change and which issue is fixed (if any) and any
|
||||
-->
|
||||
|
||||
<!--
|
||||
⚠️ Important: Pull requests that implement a new feature *should almost always target an existing feature request*. This is in order to balance the work of implementing and maintaining new features vs. community-interest. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers.
|
||||
⚠️ Important: Pull requests that implement a new feature or enhancement *should almost always target an existing feature request* with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers.
|
||||
-->
|
||||
|
||||
Closes #(issue or discussion)
|
||||
@@ -22,7 +22,7 @@ NOTE: Please check only one box!
|
||||
-->
|
||||
|
||||
- [ ] Bug fix: non-breaking change which fixes an issue.
|
||||
- [ ] New feature: non-breaking change which adds functionality. _Please read the important note above._
|
||||
- [ ] New feature / Enhancement: non-breaking change which adds functionality. _Please read the important note above._
|
||||
- [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected.
|
||||
- [ ] Documentation only.
|
||||
- [ ] Other. Please explain:
|
||||
|
||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ on:
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.11.15"
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.12.1"
|
||||
# This is the default version of Python to use in most steps which aren't specific
|
||||
DEFAULT_PYTHON_VERSION: "3.10"
|
||||
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm
|
||||
@@ -283,7 +283,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
-
|
||||
name: Upload frontend coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -299,7 +299,7 @@ jobs:
|
||||
path: src/
|
||||
-
|
||||
name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -310,7 +310,7 @@ jobs:
|
||||
build-docker-image:
|
||||
name: Build Docker image for ${{ github.ref_name }}
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
@@ -645,7 +645,7 @@ jobs:
|
||||
script: |
|
||||
const { repo, owner } = context.repo;
|
||||
const result = await github.rest.pulls.create({
|
||||
title: '[Documentation] Add ${{ needs.publish-release.outputs.version }} changelog',
|
||||
title: 'Documentation: Add ${{ needs.publish-release.outputs.version }} changelog',
|
||||
owner,
|
||||
repo,
|
||||
head: '${{ needs.publish-release.outputs.version }}-changelog',
|
||||
|
||||
2
.github/workflows/cleanup-tags.yml
vendored
2
.github/workflows/cleanup-tags.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
package_name: "${{ matrix.primary-name }}"
|
||||
scheme: "branch"
|
||||
repo_name: "paperless-ngx"
|
||||
match_regex: "feature-"
|
||||
match_regex: "(feature|fix)"
|
||||
do_delete: "true"
|
||||
|
||||
cleanup-untagged-images:
|
||||
|
||||
@@ -47,11 +47,11 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.1.11'
|
||||
rev: 'v0.2.1'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.12.1
|
||||
rev: 24.1.1
|
||||
hooks:
|
||||
- id: black
|
||||
# Dockerfile hooks
|
||||
|
||||
42
.ruff.toml
42
.ruff.toml
@@ -1,8 +1,3 @@
|
||||
# https://beta.ruff.rs/docs/settings/
|
||||
# https://beta.ruff.rs/docs/rules/
|
||||
extend-select = ["I", "W", "UP", "COM", "DJ", "EXE", "ISC", "ICN", "G201", "INP", "PIE", "RSE", "SIM", "TID", "PLC", "PLE", "RUF"]
|
||||
# TODO PTH
|
||||
ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
fix = true
|
||||
line-length = 88
|
||||
respect-gitignore = true
|
||||
@@ -11,13 +6,42 @@ target-version = "py39"
|
||||
output-format = "grouped"
|
||||
show-fixes = true
|
||||
|
||||
[per-file-ignores]
|
||||
# https://docs.astral.sh/ruff/settings/
|
||||
# https://docs.astral.sh/ruff/rules/
|
||||
[lint]
|
||||
extend-select = [
|
||||
"W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
|
||||
"I", # https://docs.astral.sh/ruff/rules/#isort-i
|
||||
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
|
||||
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
||||
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
||||
"EXE", # https://docs.astral.sh/ruff/rules/#flake8-executable-exe
|
||||
"ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc
|
||||
"ICN", # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn
|
||||
"G201", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g
|
||||
"INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp
|
||||
"PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie
|
||||
"Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q
|
||||
"RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse
|
||||
"T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20
|
||||
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
|
||||
"TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid
|
||||
"TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch
|
||||
"PLC", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||
"PLE", # https://docs.astral.sh/ruff/rules/#pylint-pl
|
||||
"RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
|
||||
"FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly
|
||||
]
|
||||
# TODO PTH https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
|
||||
ignore = ["DJ001", "SIM105", "RUF012"]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
".github/scripts/*.py" = ["E501", "INP001", "SIM117"]
|
||||
"docker/wait-for-redis.py" = ["INP001"]
|
||||
"docker/wait-for-redis.py" = ["INP001", "T201"]
|
||||
"*/tests/*.py" = ["E501", "SIM117"]
|
||||
"*/migrations/*.py" = ["E501", "SIM"]
|
||||
"*/migrations/*.py" = ["E501", "SIM", "T201"]
|
||||
"src/paperless_tesseract/tests/test_parser.py" = ["RUF001"]
|
||||
"src/documents/models.py" = ["SIM115"]
|
||||
|
||||
[isort]
|
||||
[lint.isort]
|
||||
force-single-line = true
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -29,7 +29,7 @@ COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.11.15 \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.12.1 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
@@ -39,8 +39,6 @@ RUN set -eux \
|
||||
# - Don't leave anything extra in here
|
||||
FROM docker.io/python:3.11-slim-bookworm as main-app
|
||||
|
||||
ENV PYTHONWARNINGS="ignore:::django.http.response:517"
|
||||
|
||||
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/"
|
||||
LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx"
|
||||
@@ -57,6 +55,12 @@ ARG JBIG2ENC_VERSION=0.29
|
||||
ARG QPDF_VERSION=11.6.4
|
||||
ARG GS_VERSION=10.02.1
|
||||
|
||||
# Set Python environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# Ignore warning from Whitenoise
|
||||
PYTHONWARNINGS="ignore:::django.http.response:517"
|
||||
|
||||
#
|
||||
# Begin installation and configuration
|
||||
# Order the steps below from least often changed to most
|
||||
|
||||
9
Pipfile
9
Pipfile
@@ -7,7 +7,8 @@ name = "pypi"
|
||||
dateparser = "~=1.2"
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
django = "~=4.2.9"
|
||||
django = "~=4.2.10"
|
||||
django-allauth = "*"
|
||||
django-auditlog = "*"
|
||||
django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
@@ -45,12 +46,12 @@ python-magic = "*"
|
||||
pyzbar = "*"
|
||||
rapidfuzz = "*"
|
||||
redis = {extras = ["hiredis"], version = "*"}
|
||||
scikit-learn = "~=1.3"
|
||||
scikit-learn = "~=1.4"
|
||||
setproctitle = "*"
|
||||
tika-client = "*"
|
||||
tqdm = "*"
|
||||
uvicorn = {extras = ["standard"], version = "*"}
|
||||
watchdog = "~=3.0"
|
||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
||||
watchdog = "~=4.0"
|
||||
whitenoise = "~=6.6"
|
||||
whoosh="~=2.7"
|
||||
zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
|
||||
1253
Pipfile.lock
generated
1253
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
12
README.md
12
README.md
@@ -21,7 +21,7 @@ Paperless-ngx is a document management system that transforms your physical docu
|
||||
|
||||
Paperless-ngx is the official successor to the original [Paperless](https://github.com/the-paperless-project/paperless) & [Paperless-ng](https://github.com/jonaswinkler/paperless-ng) projects and is designed to distribute the responsibility of advancing and supporting the project among a team of people. [Consider joining us!](#community-support)
|
||||
|
||||
A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. _Note: demo content is reset frequently and confidential information should not be uploaded._
|
||||
Thanks to the generous folks at [DigitalOcean](https://m.do.co/c/8d70b916d462), a demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. _Note: demo content is reset frequently and confidential information should not be uploaded._
|
||||
|
||||
- [Features](#features)
|
||||
- [Getting started](#getting-started)
|
||||
@@ -33,6 +33,16 @@ A demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com)
|
||||
- [Affiliated Projects](#affiliated-projects)
|
||||
- [Important Note](#important-note)
|
||||
|
||||
<p align="right">This project is supported by:<br/>
|
||||
<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">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_black_.svg" width="140px">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Features
|
||||
|
||||
<picture>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# To install and update paperless with this file, do the following:
|
||||
#
|
||||
# - Open portainer Stacks list and click 'Add stack'
|
||||
# - Paste the contents of this file and assign a name, e.g. 'Paperless'
|
||||
# - Paste the contents of this file and assign a name, e.g. 'paperless'
|
||||
# - Click 'Deploy the stack' and wait for it to be deployed
|
||||
# - Open the list of containers, select paperless_webserver_1
|
||||
# - Click 'Console' and then 'Connect' to open the command line inside the container
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SUPERVISORD_WORKING_DIR="${PAPERLESS_SUPERVISORD_WORKING_DIR:-$PWD}"
|
||||
rootless_args=()
|
||||
if [ "$(id -u)" == "$(id -u paperless)" ]; then
|
||||
rootless_args=(
|
||||
--user
|
||||
paperless
|
||||
--logfile
|
||||
supervisord.log
|
||||
"${SUPERVISORD_WORKING_DIR}/supervisord.log"
|
||||
--pidfile
|
||||
supervisord.pid
|
||||
"${SUPERVISORD_WORKING_DIR}/supervisord.pid"
|
||||
)
|
||||
fi
|
||||
|
||||
|
||||
@@ -67,15 +67,15 @@ you installed paperless-ngx in the first place. The releases are
|
||||
available at the [release
|
||||
page](https://github.com/paperless-ngx/paperless-ngx/releases).
|
||||
|
||||
First of all, ensure that paperless is stopped.
|
||||
First of all, make sure no active processes (like consumption) are running, then [make a backup](#backup).
|
||||
|
||||
After that, ensure that paperless is stopped:
|
||||
|
||||
```shell-session
|
||||
$ cd /path/to/paperless
|
||||
$ docker compose down
|
||||
```
|
||||
|
||||
After that, [make a backup](#backup).
|
||||
|
||||
1. If you pull the image from the docker hub, all you need to do is:
|
||||
|
||||
```shell-session
|
||||
|
||||
@@ -434,8 +434,10 @@ to view more detailed information about the health of the celery workers
|
||||
used for asynchronous tasks. This includes details on currently running,
|
||||
queued and completed tasks, timing and more. Flower can also be used
|
||||
with Prometheus, as it exports metrics. For details on its capabilities,
|
||||
refer to the Flower documentation.
|
||||
refer to the [Flower](https://flower.readthedocs.io/en/latest/index.html)
|
||||
documentation.
|
||||
|
||||
Flower can be enabled with the setting [PAPERLESS_ENABLE_FLOWER](configuration/#PAPERLESS_ENABLE_FLOWER).
|
||||
To configure Flower further, create a `flowerconfig.py` and
|
||||
place it into the `src/paperless` directory. For a Docker
|
||||
installation, you can use volumes to accomplish this:
|
||||
@@ -444,6 +446,8 @@ installation, you can use volumes to accomplish this:
|
||||
services:
|
||||
# ...
|
||||
webserver:
|
||||
environment:
|
||||
- PAPERLESS_ENABLE_FLOWER
|
||||
ports:
|
||||
- 5555:5555 # (2)!
|
||||
# ...
|
||||
@@ -452,7 +456,7 @@ services:
|
||||
```
|
||||
|
||||
1. Note the `:ro` tag means the file will be mounted as read only.
|
||||
2. `flower` runs by default on port 5555, but this can be configured
|
||||
2. By default, Flower runs on port 5555, but this can be configured.
|
||||
|
||||
## Custom Container Initialization
|
||||
|
||||
@@ -513,6 +517,18 @@ existing tables) with:
|
||||
an older system may fix issues that can arise while setting up Paperless-ngx but
|
||||
`utf8mb3` can cause issues with consumption (where `utf8mb4` does not).
|
||||
|
||||
### Missing timezones
|
||||
|
||||
MySQL as well as MariaDB do not have any timezone information by default (though some
|
||||
docker images such as the official MariaDB image take care of this for you) which will
|
||||
cause unexpected behavior with date-based queries.
|
||||
|
||||
To fix this, execute one of the following commands:
|
||||
|
||||
MySQL: `mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql -p`
|
||||
|
||||
MariaDB: `mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql -p`
|
||||
|
||||
## Barcodes {#barcodes}
|
||||
|
||||
Paperless is able to utilize barcodes for automatically performing some tasks.
|
||||
@@ -553,6 +569,14 @@ barcode is located. However, differing from the splitting, the page with the
|
||||
barcode _will_ be retained. This allows application of a barcode to any page, including
|
||||
one which holds data to keep in the document.
|
||||
|
||||
### Tag Assignment
|
||||
|
||||
When enabled, Paperless will parse barcodes and attempt to interpret and assign tags.
|
||||
|
||||
See the relevant settings [`PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE`](configuration.md#PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE)
|
||||
and [`PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING`](configuration.md#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING)
|
||||
for more information.
|
||||
|
||||
## Automatic collation of double-sided documents {#collate}
|
||||
|
||||
!!! note
|
||||
@@ -624,3 +648,47 @@ single-sided split marker page, the split document(s) will have an empty page at
|
||||
whatever else was on the backside of the split marker page.) You can work around that by having
|
||||
a split marker page that has the split barcode on _both_ sides. This way, the extra page will
|
||||
get automatically removed.
|
||||
|
||||
## SSO and third party authentication with Paperless-ngx
|
||||
|
||||
Paperless-ngx has a built-in authentication system from Django but you can easily integrate an
|
||||
external authentication solution using one of the following methods:
|
||||
|
||||
### Remote User authentication
|
||||
|
||||
This is a simple option that uses remote user authentication made available by certain SSO
|
||||
applications. See the relevant configuration options for more information:
|
||||
[PAPERLESS_ENABLE_HTTP_REMOTE_USER](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER) and
|
||||
[PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME](configuration.md#PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME)
|
||||
|
||||
### OpenID Connect and social authentication
|
||||
|
||||
Version 2.5.0 of Paperless-ngx added support for integrating other authentication systems via
|
||||
the [django-allauth](https://github.com/pennersr/django-allauth) package. Once set up, users
|
||||
can either log in or (optionally) sign up using any third party systems you integrate. See the
|
||||
relevant [configuration settings](configuration.md#PAPERLESS_SOCIALACCOUNT_PROVIDERS) and
|
||||
[django-allauth docs](https://docs.allauth.org/en/latest/socialaccount/configuration.html)
|
||||
for more information.
|
||||
|
||||
To associate an existing Paperless-ngx account with a social account, first login with your
|
||||
regular credentials and then choose "My Profile" from the user dropdown in the app and you
|
||||
will see options to connect social account(s). If enabled, signup options will be available
|
||||
on the login page.
|
||||
|
||||
As an example, to set up login via Github, the following environment variables would need to be
|
||||
set:
|
||||
|
||||
```conf
|
||||
PAPERLESS_APPS="allauth.socialaccount.providers.github"
|
||||
PAPERLESS_SOCIALACCOUNT_PROVIDERS='{"github": {"APPS": [{"provider_id": "github","name": "Github","client_id": "<CLIENT_ID>","secret": "<CLIENT_SECRET>"}]}}'
|
||||
```
|
||||
|
||||
Or, to use OpenID Connect ("OIDC"), via Keycloak in this example:
|
||||
|
||||
```conf
|
||||
PAPERLESS_APPS="allauth.socialaccount.providers.openid_connect"
|
||||
PAPERLESS_SOCIALACCOUNT_PROVIDERS='
|
||||
{"openid_connect": {"APPS": [{"provider_id": "keycloak","name": "Keycloak","client_id": "paperless","secret": "<CLIENT_SECRET>","settings": { "server_url": "https://<KEYCLOAK_SERVER>/realms/<REALM>/.well-known/openid-configuration"}}]}}'
|
||||
```
|
||||
|
||||
More details about configuration option for various providers can be found in the [allauth documentation](https://docs.allauth.org/en/latest/socialaccount/providers/index.html#provider-specifics).
|
||||
|
||||
75
docs/api.md
75
docs/api.md
@@ -179,8 +179,8 @@ The REST api provides four different forms of authentication.
|
||||
|
||||
4. Remote User authentication
|
||||
|
||||
If already setup (see
|
||||
[configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER)),
|
||||
If enabled (see
|
||||
[configuration](configuration.md#PAPERLESS_ENABLE_HTTP_REMOTE_USER_API)),
|
||||
you can authenticate against the API using Remote User auth.
|
||||
|
||||
## Searching for documents
|
||||
@@ -191,7 +191,7 @@ results:
|
||||
|
||||
- `/api/documents/?query=your%20search%20query`: Search for a document
|
||||
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage.md#basic-usage_searching).
|
||||
- `/api/documents/?more_like=1234`: Search for documents similar to
|
||||
- `/api/documents/?more_like_id=1234`: Search for documents similar to
|
||||
the document with id 1234.
|
||||
|
||||
Pagination works exactly the same as it does for normal requests on this
|
||||
@@ -330,6 +330,65 @@ granted). You can pass the parameter `full_perms=true` to API calls to view the
|
||||
full permissions of objects in a format that mirrors the `set_permissions`
|
||||
parameter above.
|
||||
|
||||
## Bulk Editing
|
||||
|
||||
The API supports various bulk-editing operations which are executed asynchronously.
|
||||
|
||||
### Documents
|
||||
|
||||
For bulk operations on documents, use the endpoint `/api/bulk_edit/` which accepts
|
||||
a json payload of the format:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [LIST_OF_DOCUMENT_IDS],
|
||||
"method": METHOD, // see below
|
||||
"parameters": args // see below
|
||||
}
|
||||
```
|
||||
|
||||
The following methods are supported:
|
||||
|
||||
- `set_correspondent`
|
||||
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
|
||||
- `set_document_type`
|
||||
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
|
||||
- `set_storage_path`
|
||||
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
|
||||
- `add_tag`
|
||||
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||
- `remove_tag`
|
||||
- Requires `parameters`: `{ "tag": TAG_ID }`
|
||||
- `modify_tags`
|
||||
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and / or `{ "remove_tags": [LIST_OF_TAG_IDS] }`
|
||||
- `delete`
|
||||
- No `parameters` required
|
||||
- `redo_ocr`
|
||||
- No `parameters` required
|
||||
- `set_permissions`
|
||||
- Requires `parameters`:
|
||||
- `"permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
|
||||
- `"owner": OWNER_ID or null`
|
||||
- `"merge": true or false` (defaults to false)
|
||||
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
|
||||
removing them) or be merged with existing permissions.
|
||||
|
||||
### Objects
|
||||
|
||||
Bulk editing for objects (tags, document types etc.) currently supports set permissions or delete
|
||||
operations, using the endpoint: `/api/bulk_edit_objects/`, which requires a json payload of the format:
|
||||
|
||||
```json
|
||||
{
|
||||
"objects": [LIST_OF_OBJECT_IDS],
|
||||
"object_type": "tags", "correspondents", "document_types" or "storage_paths",
|
||||
"operation": "set_permissions" or "delete",
|
||||
"owner": OWNER_ID, // optional
|
||||
"permissions": { "view": { "users": [] ... }, "change": { ... } }, // (see 'set_permissions' format above)
|
||||
"merge": true / false // defaults to false, see above
|
||||
}
|
||||
```
|
||||
|
||||
## API Versioning
|
||||
|
||||
The REST API is versioned since Paperless-ngx 1.3.0.
|
||||
@@ -386,3 +445,13 @@ Initial API version.
|
||||
color to use for a specific tag, which is either black or white
|
||||
depending on the brightness of `Tag.color`.
|
||||
- Removed field `Tag.colour`.
|
||||
|
||||
#### Version 3
|
||||
|
||||
- Permissions endpoints have been added.
|
||||
- The format of the `/api/ui_settings/` has changed.
|
||||
|
||||
#### Version 4
|
||||
|
||||
- Consumption templates were refactored to workflows and API endpoints
|
||||
changed as such.
|
||||
|
||||
@@ -1,5 +1,261 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.5.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Generated secret key may include single or double quotes [@schmidtnz](https://github.com/schmidtnz) ([#5767](https://github.com/paperless-ngx/paperless-ngx/pull/5767))
|
||||
- Fix: consumer status alerts container blocks elements [@shamoon](https://github.com/shamoon) ([#5762](https://github.com/paperless-ngx/paperless-ngx/pull/5762))
|
||||
- Fix: handle document notes user format api change [@shamoon](https://github.com/shamoon) ([#5751](https://github.com/paperless-ngx/paperless-ngx/pull/5751))
|
||||
- Fix: Assign ASN from barcode only after any splitting [@stumpylog](https://github.com/stumpylog) ([#5745](https://github.com/paperless-ngx/paperless-ngx/pull/5745))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps): Bump the major-versions group with 1 update [@dependabot](https://github.com/dependabot) ([#5741](https://github.com/paperless-ngx/paperless-ngx/pull/5741))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Fix: consumer status alerts container blocks elements [@shamoon](https://github.com/shamoon) ([#5762](https://github.com/paperless-ngx/paperless-ngx/pull/5762))
|
||||
- Fix: handle document notes user format api change [@shamoon](https://github.com/shamoon) ([#5751](https://github.com/paperless-ngx/paperless-ngx/pull/5751))
|
||||
- Fix: Assign ASN from barcode only after any splitting [@stumpylog](https://github.com/stumpylog) ([#5745](https://github.com/paperless-ngx/paperless-ngx/pull/5745))
|
||||
- Chore(deps): Bump the major-versions group with 1 update [@dependabot](https://github.com/dependabot) ([#5741](https://github.com/paperless-ngx/paperless-ngx/pull/5741))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.5.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Splitting on ASN barcodes even if not enabled [@stumpylog](https://github.com/stumpylog) ([#5740](https://github.com/paperless-ngx/paperless-ngx/pull/5740))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5737](https://github.com/paperless-ngx/paperless-ngx/pull/5737))
|
||||
- Chore(deps): Bump the django group with 1 update [@dependabot](https://github.com/dependabot) ([#5739](https://github.com/paperless-ngx/paperless-ngx/pull/5739))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>3 changes</summary>
|
||||
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5737](https://github.com/paperless-ngx/paperless-ngx/pull/5737))
|
||||
- Chore(deps): Bump the django group with 1 update [@dependabot](https://github.com/dependabot) ([#5739](https://github.com/paperless-ngx/paperless-ngx/pull/5739))
|
||||
- Fix: Splitting on ASN barcodes even if not enabled [@stumpylog](https://github.com/stumpylog) ([#5740](https://github.com/paperless-ngx/paperless-ngx/pull/5740))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.5.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Enhancement: bulk delete objects [@shamoon](https://github.com/shamoon) ([#5688](https://github.com/paperless-ngx/paperless-ngx/pull/5688))
|
||||
|
||||
### Notable Changes
|
||||
|
||||
- Feature: OIDC \& social authentication [@mpflanzer](https://github.com/mpflanzer) ([#5190](https://github.com/paperless-ngx/paperless-ngx/pull/5190))
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: confirm buttons [@shamoon](https://github.com/shamoon) ([#5680](https://github.com/paperless-ngx/paperless-ngx/pull/5680))
|
||||
- Enhancement: bulk delete objects [@shamoon](https://github.com/shamoon) ([#5688](https://github.com/paperless-ngx/paperless-ngx/pull/5688))
|
||||
- Feature: allow create objects from bulk edit [@shamoon](https://github.com/shamoon) ([#5667](https://github.com/paperless-ngx/paperless-ngx/pull/5667))
|
||||
- Feature: Allow tagging by putting barcodes on documents [@pkrahmer](https://github.com/pkrahmer) ([#5580](https://github.com/paperless-ngx/paperless-ngx/pull/5580))
|
||||
- Feature: Cache metadata and suggestions in Redis [@stumpylog](https://github.com/stumpylog) ([#5638](https://github.com/paperless-ngx/paperless-ngx/pull/5638))
|
||||
- Feature: Japanese translation [@shamoon](https://github.com/shamoon) ([#5641](https://github.com/paperless-ngx/paperless-ngx/pull/5641))
|
||||
- Feature: option for auto-remove inbox tags on save [@shamoon](https://github.com/shamoon) ([#5562](https://github.com/paperless-ngx/paperless-ngx/pull/5562))
|
||||
- Enhancement: allow paperless to run in read-only filesystem [@hegerdes](https://github.com/hegerdes) ([#5596](https://github.com/paperless-ngx/paperless-ngx/pull/5596))
|
||||
- Enhancement: mergeable bulk edit permissions [@shamoon](https://github.com/shamoon) ([#5508](https://github.com/paperless-ngx/paperless-ngx/pull/5508))
|
||||
- Enhancement: re-implement remote user auth for unsafe API requests as opt-in [@shamoon](https://github.com/shamoon) ([#5561](https://github.com/paperless-ngx/paperless-ngx/pull/5561))
|
||||
- Enhancement: Respect PDF cropbox for thumbnail generation [@henningBunk](https://github.com/henningBunk) ([#5531](https://github.com/paperless-ngx/paperless-ngx/pull/5531))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Test metadata items for Unicode issues [@stumpylog](https://github.com/stumpylog) ([#5707](https://github.com/paperless-ngx/paperless-ngx/pull/5707))
|
||||
- Change: try to show preview even if metadata fails [@shamoon](https://github.com/shamoon) ([#5706](https://github.com/paperless-ngx/paperless-ngx/pull/5706))
|
||||
- Fix: only check workflow trigger source if not empty [@shamoon](https://github.com/shamoon) ([#5701](https://github.com/paperless-ngx/paperless-ngx/pull/5701))
|
||||
- Fix: frontend validation of number fields fails upon save [@shamoon](https://github.com/shamoon) ([#5646](https://github.com/paperless-ngx/paperless-ngx/pull/5646))
|
||||
- Fix: Explicit validation of custom field name unique constraint [@shamoon](https://github.com/shamoon) ([#5647](https://github.com/paperless-ngx/paperless-ngx/pull/5647))
|
||||
- Fix: Don't attempt to retrieve object types user doesn't have permissions to [@shamoon](https://github.com/shamoon) ([#5612](https://github.com/paperless-ngx/paperless-ngx/pull/5612))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Documentation: add detail about consumer polling behavior [@silmaril42](https://github.com/silmaril42) ([#5674](https://github.com/paperless-ngx/paperless-ngx/pull/5674))
|
||||
- Paperless-ngx Demo: new and improved [@shamoon](https://github.com/shamoon) ([#5639](https://github.com/paperless-ngx/paperless-ngx/pull/5639))
|
||||
- Documentation: Add docs about missing timezones in MySQL/MariaDB [@Programie](https://github.com/Programie) ([#5583](https://github.com/paperless-ngx/paperless-ngx/pull/5583))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore(deps): Bump the actions group with 1 update [@dependabot](https://github.com/dependabot) ([#5629](https://github.com/paperless-ngx/paperless-ngx/pull/5629))
|
||||
- Chore(deps): Bump the actions group with 1 update [@dependabot](https://github.com/dependabot) ([#5597](https://github.com/paperless-ngx/paperless-ngx/pull/5597))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>9 changes</summary>
|
||||
|
||||
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#5676](https://github.com/paperless-ngx/paperless-ngx/pull/5676))
|
||||
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.40.1 to 1.41.2 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.40.1 to 1.41.2 in /src-ui @dependabot) ([#5634](https://github.com/paperless-ngx/paperless-ngx/pull/5634))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 19 updates [@dependabot](https://github.com/dependabot) ([#5630](https://github.com/paperless-ngx/paperless-ngx/pull/5630))
|
||||
- Chore(deps-dev): Bump the frontend-jest-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#5631](https://github.com/paperless-ngx/paperless-ngx/pull/5631))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#5632](https://github.com/paperless-ngx/paperless-ngx/pull/5632))
|
||||
- Chore(deps): Bump zone.js from 0.14.2 to 0.14.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#5633](https://github.com/paperless-ngx/paperless-ngx/pull/5633))
|
||||
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.6 to 20.11.16 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.6 to 20.11.16 in /src-ui @dependabot) ([#5635](https://github.com/paperless-ngx/paperless-ngx/pull/5635))
|
||||
- Chore(deps): Bump the actions group with 1 update [@dependabot](https://github.com/dependabot) ([#5629](https://github.com/paperless-ngx/paperless-ngx/pull/5629))
|
||||
- Chore(deps): Bump the actions group with 1 update [@dependabot](https://github.com/dependabot) ([#5597](https://github.com/paperless-ngx/paperless-ngx/pull/5597))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>28 changes</summary>
|
||||
|
||||
- Chore: Ensure all creations of directories create the parents too [@stumpylog](https://github.com/stumpylog) ([#5711](https://github.com/paperless-ngx/paperless-ngx/pull/5711))
|
||||
- Fix: Test metadata items for Unicode issues [@stumpylog](https://github.com/stumpylog) ([#5707](https://github.com/paperless-ngx/paperless-ngx/pull/5707))
|
||||
- Change: try to show preview even if metadata fails [@shamoon](https://github.com/shamoon) ([#5706](https://github.com/paperless-ngx/paperless-ngx/pull/5706))
|
||||
- Fix: only check workflow trigger source if not empty [@shamoon](https://github.com/shamoon) ([#5701](https://github.com/paperless-ngx/paperless-ngx/pull/5701))
|
||||
- Enhancement: confirm buttons [@shamoon](https://github.com/shamoon) ([#5680](https://github.com/paperless-ngx/paperless-ngx/pull/5680))
|
||||
- Enhancement: bulk delete objects [@shamoon](https://github.com/shamoon) ([#5688](https://github.com/paperless-ngx/paperless-ngx/pull/5688))
|
||||
- Chore: Backend dependencies update [@stumpylog](https://github.com/stumpylog) ([#5676](https://github.com/paperless-ngx/paperless-ngx/pull/5676))
|
||||
- Feature: OIDC \& social authentication [@mpflanzer](https://github.com/mpflanzer) ([#5190](https://github.com/paperless-ngx/paperless-ngx/pull/5190))
|
||||
- Chore: Don't write Python bytecode in the Docker image [@stumpylog](https://github.com/stumpylog) ([#5677](https://github.com/paperless-ngx/paperless-ngx/pull/5677))
|
||||
- Feature: allow create objects from bulk edit [@shamoon](https://github.com/shamoon) ([#5667](https://github.com/paperless-ngx/paperless-ngx/pull/5667))
|
||||
- Chore: Use memory cache backend in debug mode [@shamoon](https://github.com/shamoon) ([#5666](https://github.com/paperless-ngx/paperless-ngx/pull/5666))
|
||||
- Chore: Adds additional rules for Ruff linter [@stumpylog](https://github.com/stumpylog) ([#5660](https://github.com/paperless-ngx/paperless-ngx/pull/5660))
|
||||
- Feature: Allow tagging by putting barcodes on documents [@pkrahmer](https://github.com/pkrahmer) ([#5580](https://github.com/paperless-ngx/paperless-ngx/pull/5580))
|
||||
- Feature: Cache metadata and suggestions in Redis [@stumpylog](https://github.com/stumpylog) ([#5638](https://github.com/paperless-ngx/paperless-ngx/pull/5638))
|
||||
- Fix: frontend validation of number fields fails upon save [@shamoon](https://github.com/shamoon) ([#5646](https://github.com/paperless-ngx/paperless-ngx/pull/5646))
|
||||
- Fix: Explicit validation of custom field name unique constraint [@shamoon](https://github.com/shamoon) ([#5647](https://github.com/paperless-ngx/paperless-ngx/pull/5647))
|
||||
- Feature: Japanese translation [@shamoon](https://github.com/shamoon) ([#5641](https://github.com/paperless-ngx/paperless-ngx/pull/5641))
|
||||
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.40.1 to 1.41.2 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.40.1 to 1.41.2 in /src-ui @dependabot) ([#5634](https://github.com/paperless-ngx/paperless-ngx/pull/5634))
|
||||
- Feature: option for auto-remove inbox tags on save [@shamoon](https://github.com/shamoon) ([#5562](https://github.com/paperless-ngx/paperless-ngx/pull/5562))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 19 updates [@dependabot](https://github.com/dependabot) ([#5630](https://github.com/paperless-ngx/paperless-ngx/pull/5630))
|
||||
- Chore(deps-dev): Bump the frontend-jest-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#5631](https://github.com/paperless-ngx/paperless-ngx/pull/5631))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 2 updates [@dependabot](https://github.com/dependabot) ([#5632](https://github.com/paperless-ngx/paperless-ngx/pull/5632))
|
||||
- Chore(deps): Bump zone.js from 0.14.2 to 0.14.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#5633](https://github.com/paperless-ngx/paperless-ngx/pull/5633))
|
||||
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.6 to 20.11.16 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.6 to 20.11.16 in /src-ui @dependabot) ([#5635](https://github.com/paperless-ngx/paperless-ngx/pull/5635))
|
||||
- Enhancement: mergeable bulk edit permissions [@shamoon](https://github.com/shamoon) ([#5508](https://github.com/paperless-ngx/paperless-ngx/pull/5508))
|
||||
- Enhancement: re-implement remote user auth for unsafe API requests as opt-in [@shamoon](https://github.com/shamoon) ([#5561](https://github.com/paperless-ngx/paperless-ngx/pull/5561))
|
||||
- Enhancement: Respect PDF cropbox for thumbnail generation [@henningBunk](https://github.com/henningBunk) ([#5531](https://github.com/paperless-ngx/paperless-ngx/pull/5531))
|
||||
- Fix: Don't attempt to retrieve object types user doesn't have permissions to [@shamoon](https://github.com/shamoon) ([#5612](https://github.com/paperless-ngx/paperless-ngx/pull/5612))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.4.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Ensure the scratch directory exists before consuming via the folder [@stumpylog](https://github.com/stumpylog) ([#5579](https://github.com/paperless-ngx/paperless-ngx/pull/5579))
|
||||
|
||||
### All App Changes
|
||||
|
||||
- Fix: Ensure the scratch directory exists before consuming via the folder [@stumpylog](https://github.com/stumpylog) ([#5579](https://github.com/paperless-ngx/paperless-ngx/pull/5579))
|
||||
|
||||
## paperless-ngx 2.4.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: improve one of the date matching regexes [@shamoon](https://github.com/shamoon) ([#5540](https://github.com/paperless-ngx/paperless-ngx/pull/5540))
|
||||
- Fix: tweak doc detail component behavior while awaiting metadata [@shamoon](https://github.com/shamoon) ([#5546](https://github.com/paperless-ngx/paperless-ngx/pull/5546))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>2 changes</summary>
|
||||
|
||||
- Fix: improve one of the date matching regexes [@shamoon](https://github.com/shamoon) ([#5540](https://github.com/paperless-ngx/paperless-ngx/pull/5540))
|
||||
- Fix: tweak doc detail component behavior while awaiting metadata [@shamoon](https://github.com/shamoon) ([#5546](https://github.com/paperless-ngx/paperless-ngx/pull/5546))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.4.1
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Change: merge workflow permissions assignments instead of overwrite [@shamoon](https://github.com/shamoon) ([#5496](https://github.com/paperless-ngx/paperless-ngx/pull/5496))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Minor frontend things in 2.4.0 [@shamoon](https://github.com/shamoon) ([#5514](https://github.com/paperless-ngx/paperless-ngx/pull/5514))
|
||||
- Fix: install script fails on alpine linux [@shamoon](https://github.com/shamoon) ([#5520](https://github.com/paperless-ngx/paperless-ngx/pull/5520))
|
||||
- Fix: enforce permissions for app config [@shamoon](https://github.com/shamoon) ([#5516](https://github.com/paperless-ngx/paperless-ngx/pull/5516))
|
||||
- Fix: render images not converted to pdf, refactor doc detail rendering [@shamoon](https://github.com/shamoon) ([#5475](https://github.com/paperless-ngx/paperless-ngx/pull/5475))
|
||||
- Fix: Dont parse numbers with exponent as integer [@shamoon](https://github.com/shamoon) ([#5457](https://github.com/paperless-ngx/paperless-ngx/pull/5457))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Build fix- branches [@shamoon](https://github.com/shamoon) ([#5501](https://github.com/paperless-ngx/paperless-ngx/pull/5501))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps-dev): Bump the development group with 1 update [@dependabot](https://github.com/dependabot) ([#5503](https://github.com/paperless-ngx/paperless-ngx/pull/5503))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>7 changes</summary>
|
||||
|
||||
- Revert "Enhancement: support remote user auth directly against API (DRF)" @shamoon ([#5534](https://github.com/paperless-ngx/paperless-ngx/pull/5534))
|
||||
- Fix: Minor frontend things in 2.4.0 [@shamoon](https://github.com/shamoon) ([#5514](https://github.com/paperless-ngx/paperless-ngx/pull/5514))
|
||||
- Fix: enforce permissions for app config [@shamoon](https://github.com/shamoon) ([#5516](https://github.com/paperless-ngx/paperless-ngx/pull/5516))
|
||||
- Change: merge workflow permissions assignments instead of overwrite [@shamoon](https://github.com/shamoon) ([#5496](https://github.com/paperless-ngx/paperless-ngx/pull/5496))
|
||||
- Chore(deps-dev): Bump the development group with 1 update [@dependabot](https://github.com/dependabot) ([#5503](https://github.com/paperless-ngx/paperless-ngx/pull/5503))
|
||||
- Fix: render images not converted to pdf, refactor doc detail rendering [@shamoon](https://github.com/shamoon) ([#5475](https://github.com/paperless-ngx/paperless-ngx/pull/5475))
|
||||
- Fix: Dont parse numbers with exponent as integer [@shamoon](https://github.com/shamoon) ([#5457](https://github.com/paperless-ngx/paperless-ngx/pull/5457))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.4.0
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: support remote user auth directly against API (DRF) [@shamoon](https://github.com/shamoon) ([#5386](https://github.com/paperless-ngx/paperless-ngx/pull/5386))
|
||||
- Feature: Add additional caching support to suggestions and metadata [@stumpylog](https://github.com/stumpylog) ([#5414](https://github.com/paperless-ngx/paperless-ngx/pull/5414))
|
||||
- Feature: help tooltips [@shamoon](https://github.com/shamoon) ([#5383](https://github.com/paperless-ngx/paperless-ngx/pull/5383))
|
||||
- Enhancement: warn when outdated doc detected [@shamoon](https://github.com/shamoon) ([#5372](https://github.com/paperless-ngx/paperless-ngx/pull/5372))
|
||||
- Feature: app branding [@shamoon](https://github.com/shamoon) ([#5357](https://github.com/paperless-ngx/paperless-ngx/pull/5357))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: doc link removal when has never been assigned [@shamoon](https://github.com/shamoon) ([#5451](https://github.com/paperless-ngx/paperless-ngx/pull/5451))
|
||||
- Fix: dont lose permissions ui if owner changed from [@shamoon](https://github.com/shamoon) ([#5433](https://github.com/paperless-ngx/paperless-ngx/pull/5433))
|
||||
- Fix: Getting next ASN when no documents have an ASN [@stumpylog](https://github.com/stumpylog) ([#5431](https://github.com/paperless-ngx/paperless-ngx/pull/5431))
|
||||
- Fix: signin username floating label [@shamoon](https://github.com/shamoon) ([#5424](https://github.com/paperless-ngx/paperless-ngx/pull/5424))
|
||||
- Fix: shared by me filter with multiple users / groups in postgres [@shamoon](https://github.com/shamoon) ([#5396](https://github.com/paperless-ngx/paperless-ngx/pull/5396))
|
||||
- Fix: Catch new warning when loading the classifier [@stumpylog](https://github.com/stumpylog) ([#5395](https://github.com/paperless-ngx/paperless-ngx/pull/5395))
|
||||
- Fix: doc detail component fixes [@shamoon](https://github.com/shamoon) ([#5373](https://github.com/paperless-ngx/paperless-ngx/pull/5373))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: better bootstrap icons [@shamoon](https://github.com/shamoon) ([#5403](https://github.com/paperless-ngx/paperless-ngx/pull/5403))
|
||||
- Chore: Close outdated support / general discussions [@shamoon](https://github.com/shamoon) ([#5443](https://github.com/paperless-ngx/paperless-ngx/pull/5443))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#5413](https://github.com/paperless-ngx/paperless-ngx/pull/5413))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5412](https://github.com/paperless-ngx/paperless-ngx/pull/5412))
|
||||
- Chore(deps-dev): Bump jinja2 from 3.1.2 to 3.1.3 [@dependabot](https://github.com/dependabot) ([#5352](https://github.com/paperless-ngx/paperless-ngx/pull/5352))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>16 changes</summary>
|
||||
|
||||
- Fix: doc link removal when has never been assigned [@shamoon](https://github.com/shamoon) ([#5451](https://github.com/paperless-ngx/paperless-ngx/pull/5451))
|
||||
- Chore: better bootstrap icons [@shamoon](https://github.com/shamoon) ([#5403](https://github.com/paperless-ngx/paperless-ngx/pull/5403))
|
||||
- Fix: dont lose permissions ui if owner changed from [@shamoon](https://github.com/shamoon) ([#5433](https://github.com/paperless-ngx/paperless-ngx/pull/5433))
|
||||
- Enhancement: support remote user auth directly against API (DRF) [@shamoon](https://github.com/shamoon) ([#5386](https://github.com/paperless-ngx/paperless-ngx/pull/5386))
|
||||
- Fix: Getting next ASN when no documents have an ASN [@stumpylog](https://github.com/stumpylog) ([#5431](https://github.com/paperless-ngx/paperless-ngx/pull/5431))
|
||||
- Feature: Add additional caching support to suggestions and metadata [@stumpylog](https://github.com/stumpylog) ([#5414](https://github.com/paperless-ngx/paperless-ngx/pull/5414))
|
||||
- Chore(deps): Bump the small-changes group with 2 updates [@dependabot](https://github.com/dependabot) ([#5413](https://github.com/paperless-ngx/paperless-ngx/pull/5413))
|
||||
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5412](https://github.com/paperless-ngx/paperless-ngx/pull/5412))
|
||||
- Fix: signin username floating label [@shamoon](https://github.com/shamoon) ([#5424](https://github.com/paperless-ngx/paperless-ngx/pull/5424))
|
||||
- Feature: help tooltips [@shamoon](https://github.com/shamoon) ([#5383](https://github.com/paperless-ngx/paperless-ngx/pull/5383))
|
||||
- Enhancement / QoL: show selected tasks count [@shamoon](https://github.com/shamoon) ([#5379](https://github.com/paperless-ngx/paperless-ngx/pull/5379))
|
||||
- Fix: shared by me filter with multiple users / groups in postgres [@shamoon](https://github.com/shamoon) ([#5396](https://github.com/paperless-ngx/paperless-ngx/pull/5396))
|
||||
- Fix: doc detail component fixes [@shamoon](https://github.com/shamoon) ([#5373](https://github.com/paperless-ngx/paperless-ngx/pull/5373))
|
||||
- Enhancement: warn when outdated doc detected [@shamoon](https://github.com/shamoon) ([#5372](https://github.com/paperless-ngx/paperless-ngx/pull/5372))
|
||||
- Feature: app branding [@shamoon](https://github.com/shamoon) ([#5357](https://github.com/paperless-ngx/paperless-ngx/pull/5357))
|
||||
- Chore: Initial refactor of consume task [@stumpylog](https://github.com/stumpylog) ([#5367](https://github.com/paperless-ngx/paperless-ngx/pull/5367))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.3.3
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -34,6 +34,8 @@ matcher.
|
||||
`redis://<username>:<password>@<host>:<port>`
|
||||
- With the requirepass option PAPERLESS_REDIS =
|
||||
`redis://:<password>@<host>:<port>`
|
||||
- To include the redis database index PAPERLESS_REDIS =
|
||||
`redis://<username>:<password>@<host>:<port>/<DBIndex>`
|
||||
|
||||
[More information on securing your Redis
|
||||
Instance](https://redis.io/docs/getting-started/#securing-redis).
|
||||
@@ -452,19 +454,32 @@ applications.
|
||||
|
||||
This will allow authentication by simply adding a
|
||||
`Remote-User: <username>` header to a request. Use with care! You
|
||||
especially *must: ensure that any such header is not passed from
|
||||
your proxy server to paperless.
|
||||
especially *must* ensure that any such header is not passed from
|
||||
external requests to your reverse-proxy to paperless (that would
|
||||
effectively bypass all authentication).
|
||||
|
||||
If you're exposing paperless to the internet directly, do not use
|
||||
this.
|
||||
If you're exposing paperless to the internet directly (i.e.
|
||||
without a reverse proxy), do not use this.
|
||||
|
||||
Also see the warning [in the official documentation](https://docs.djangoproject.com/en/4.1/howto/auth-remote-user/#configuration).
|
||||
|
||||
Defaults to "false" which disables this feature.
|
||||
|
||||
#### [`PAPERLESS_ENABLE_HTTP_REMOTE_USER_API=<bool>`](#PAPERLESS_ENABLE_HTTP_REMOTE_USER_API) {#PAPERLESS_ENABLE_HTTP_REMOTE_USER_API}
|
||||
|
||||
: Allows authentication via HTTP_REMOTE_USER directly against the API
|
||||
|
||||
!!! warning
|
||||
|
||||
See the warning above about securing your installation when using remote user header authentication. This setting is separate from
|
||||
`PAPERLESS_ENABLE_HTTP_REMOTE_USER` to avoid introducing a security vulnerability to existing reverse proxy setups. As above,
|
||||
ensure that your reverse proxy does not simply pass the `Remote-User` header from the internet to paperless.
|
||||
|
||||
Defaults to "false" which disables this feature.
|
||||
|
||||
#### [`PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=<str>`](#PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME) {#PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME}
|
||||
|
||||
: If "PAPERLESS_ENABLE_HTTP_REMOTE_USER" is enabled, this
|
||||
: If "PAPERLESS_ENABLE_HTTP_REMOTE_USER" or `PAPERLESS_ENABLE_HTTP_REMOTE_USER_API` are enabled, this
|
||||
property allows to customize the name of the HTTP header from which
|
||||
the authenticated username is extracted. Values are in terms of
|
||||
[HttpRequest.META](https://docs.djangoproject.com/en/4.1/ref/request-response/#django.http.HttpRequest.META).
|
||||
@@ -521,6 +536,42 @@ This is for use with self-signed certificates against local IMAP servers.
|
||||
Settings this value has security implications for the security of your email.
|
||||
Understand what it does and be sure you need to before setting.
|
||||
|
||||
#### [`PAPERLESS_SOCIALACCOUNT_PROVIDERS=<json>`](#PAPERLESS_SOCIALACCOUNT_PROVIDERS) {#PAPERLESS_SOCIALACCOUNT_PROVIDERS}
|
||||
|
||||
: This variable is used to setup login and signup via social account providers which are compatible with django-allauth.
|
||||
See the corresponding [django-allauth documentation](https://docs.allauth.org/en/0.60.0/socialaccount/providers/index.html)
|
||||
for a list of provider configurations. You will also need to include the relevant Django 'application' inside the
|
||||
[PAPERLESS_APPS](#PAPERLESS_APPS) setting to activate that specific authentication provider (e.g. `allauth.socialaccount.providers.openid_connect` for the [OIDC Connect provider](https://docs.allauth.org/en/latest/socialaccount/providers/openid_connect.html)).
|
||||
|
||||
Defaults to None, which does not enable any third party authentication systems.
|
||||
|
||||
#### [`PAPERLESS_SOCIAL_AUTO_SIGNUP=<bool>`](#PAPERLESS_SOCIAL_AUTO_SIGNUP) {#PAPERLESS_SOCIAL_AUTO_SIGNUP}
|
||||
|
||||
: Attempt to signup the user using retrieved email, username etc from the third party authentication
|
||||
system. See the corresponding
|
||||
[django-allauth documentation](https://docs.allauth.org/en/0.60.0/socialaccount/configuration.html)
|
||||
|
||||
Defaults to False
|
||||
|
||||
#### [`PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS=<bool>`](#PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS) {#PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS}
|
||||
|
||||
: Allow users to signup for a new Paperless-ngx account using any setup third party authentication systems.
|
||||
|
||||
Defaults to True
|
||||
|
||||
#### [`PAPERLESS_ACCOUNT_ALLOW_SIGNUPS=<bool>`](#PAPERLESS_ACCOUNT_ALLOW_SIGNUPS) {#PAPERLESS_ACCOUNT_ALLOW_SIGNUPS}
|
||||
|
||||
: Allow users to signup for a new Paperless-ngx account.
|
||||
|
||||
Defaults to False
|
||||
|
||||
#### [`PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL=<string>`](#PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL) {#PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL}
|
||||
|
||||
: The protocol used when generating URLs, e.g. login callback URLs. See the corresponding
|
||||
[django-allauth documentation](https://docs.allauth.org/en/latest/account/configuration.html)
|
||||
|
||||
Defaults to 'https'
|
||||
|
||||
## OCR settings {#ocr}
|
||||
|
||||
Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
|
||||
@@ -891,6 +942,14 @@ documents.
|
||||
|
||||
Default is none, which disables the temporary directory.
|
||||
|
||||
#### [`PAPERLESS_APPS=<string>`](#PAPERLESS_APPS) {#PAPERLESS_APPS}
|
||||
|
||||
: A comma-separated list of Django apps to be included in Django's
|
||||
[`INSTALLED_APPS`](https://docs.djangoproject.com/en/5.0/ref/applications/). This setting should
|
||||
be used with caution!
|
||||
|
||||
Defaults to None, which does not add any additional apps.
|
||||
|
||||
## Document Consumption {#consume_config}
|
||||
|
||||
#### [`PAPERLESS_CONSUMER_DELETE_DUPLICATES=<bool>`](#PAPERLESS_CONSUMER_DELETE_DUPLICATES) {#PAPERLESS_CONSUMER_DELETE_DUPLICATES}
|
||||
@@ -1049,8 +1108,10 @@ system changes with `inotify`.
|
||||
|
||||
#### [`PAPERLESS_CONSUMER_POLLING_RETRY_COUNT=<num>`](#PAPERLESS_CONSUMER_POLLING_RETRY_COUNT) {#PAPERLESS_CONSUMER_POLLING_RETRY_COUNT}
|
||||
|
||||
: If consumer polling is enabled, sets the number of times paperless
|
||||
will check for a file to remain unmodified.
|
||||
: If consumer polling is enabled, sets the maximum number of times
|
||||
paperless will check for a file to remain unmodified. If a file's
|
||||
modification time and size are identical for two consecutive checks, it
|
||||
will be consumed.
|
||||
|
||||
Defaults to 5.
|
||||
|
||||
@@ -1159,6 +1220,55 @@ combination with PAPERLESS_CONSUMER_BARCODE_UPSCALE bigger than 1.0.
|
||||
|
||||
Defaults to "300"
|
||||
|
||||
#### [`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
|
||||
assigns or creates tags if a properly formatted barcode is detected.
|
||||
|
||||
The barcode must match one of the (configurable) regular expressions.
|
||||
If the barcode text contains ',' (comma), it is split into multiple
|
||||
barcodes which are individually processed for tagging.
|
||||
|
||||
Matching is case insensitive.
|
||||
|
||||
Defaults to false.
|
||||
|
||||
#### [`PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING=<json dict>`](#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING) {#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING}
|
||||
|
||||
: Defines a dictionary of filter regex and substitute expressions.
|
||||
|
||||
Syntax: {"<regex>": "<substitute>" [,...]]}
|
||||
|
||||
A barcode is considered for tagging if the barcode text matches
|
||||
at least one of the provided <regex> pattern.
|
||||
|
||||
If a match is found, the <substitute> rule is applied. This allows very
|
||||
versatile reformatting and mapping of barcode pattern to tag values.
|
||||
|
||||
If a tag is not found it will be created.
|
||||
|
||||
Defaults to:
|
||||
|
||||
{"TAG:(.*)": "\\g<1>"} which defines
|
||||
- a regex TAG:(.*) which includes barcodes beginning with TAG:
|
||||
followed by any text that gets stored into match group #1 and
|
||||
- a substitute \\g<1> that replaces the original barcode text
|
||||
by the content in match group #1.
|
||||
Consequently, the tag is the barcode text without its TAG: prefix.
|
||||
|
||||
More examples:
|
||||
|
||||
{"ASN12.*": "JOHN", "ASN13.*": "SMITH"} for example maps
|
||||
- ASN12nnnn barcodes to the tag JOHN and
|
||||
- ASN13nnnn barcodes to the tag SMITH.
|
||||
|
||||
{"T-J": "JOHN", "T-S": "SMITH", "T-D": "DOE"} directly maps
|
||||
- T-J barcodes to the tag JOHN,
|
||||
- T-S barcodes to the tag SMITH and
|
||||
- T-D barcodes to the tag DOE.
|
||||
|
||||
Please refer to the Python regex documentation for more information.
|
||||
|
||||
## Audit Trail
|
||||
|
||||
#### [`PAPERLESS_AUDIT_LOG_ENABLED=<bool>`](#PAPERLESS_AUDIT_LOG_ENABLED) {#PAPERLESS_AUDIT_LOG_ENABLED}
|
||||
@@ -1329,6 +1439,12 @@ started by the container.
|
||||
|
||||
You can read more about this in the [advanced documentation](advanced_usage.md#celery-monitoring).
|
||||
|
||||
#### [`PAPERLESS_SUPERVISORD_WORKING_DIR=<defined>`](#PAPERLESS_SUPERVISORD_WORKING_DIR) {#PAPERLESS_SUPERVISORD_WORKING_DIR}
|
||||
|
||||
: If this environment variable is defined, the `supervisord.log` and `supervisord.pid` file will be created under the specified path in `PAPERLESS_SUPERVISORD_WORKING_DIR`. Setting `PAPERLESS_SUPERVISORD_WORKING_DIR=/tmp` and `PYTHONPYCACHEPREFIX=/tmp/pycache` would allow paperless to work on a read-only filesystem.
|
||||
|
||||
Please take note that the `PAPERLESS_DATA_DIR` and `PAPERLESS_MEDIA_ROOT` paths still have to be writable, just like the `PAPERLESS_SUPERVISORD_WORKING_DIR`. The can be archived by using bind or volume mounts. Only works in the container is run as user *paperless*
|
||||
|
||||
## Frontend Settings
|
||||
|
||||
#### [`PAPERLESS_APP_TITLE=<bool>`](#PAPERLESS_APP_TITLE) {#PAPERLESS_APP_TITLE}
|
||||
|
||||
@@ -8,6 +8,13 @@ physical documents into a searchable online archive so you can keep, well, _less
|
||||
[Get started](setup.md){ .md-button .md-button--primary .index-callout }
|
||||
[Demo](https://demo.paperless-ngx.com){ .md-button .md-button--secondary target=\_blank }
|
||||
|
||||
<div style="display: flex; justify-content: end; margin-top: -1.5rem;">
|
||||
<a href="https://m.do.co/c/8d70b916d462" target="_blank">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_white.svg#only-dark" class="no-lightbox" width="150px">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_black.svg#only-light" class="no-lightbox" width="150px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="grid-right" markdown>
|
||||
{.index-screenshot}
|
||||
|
||||
@@ -331,7 +331,7 @@ Workflows allow you to filter by:
|
||||
There is currently one type of workflow action, "Assignment", which can assign:
|
||||
|
||||
- Title, see [title placeholders](usage.md#title-placeholders) below
|
||||
- Tags, correspondent, document types
|
||||
- Tags, correspondent, document type and storage path
|
||||
- Document owner
|
||||
- View and / or edit permissions to users or groups
|
||||
- Custom fields. Note that no value for the field will be set
|
||||
|
||||
@@ -315,7 +315,7 @@ fi
|
||||
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.$DOCKER_COMPOSE_VERSION.yml" -O docker-compose.yml
|
||||
wget "https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env" -O .env
|
||||
|
||||
SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' < /dev/urandom | head --bytes 64)
|
||||
SECRET_KEY=$(LC_ALL=C tr -dc 'a-zA-Z0-9!#$%&()*+,-./:;<=>?@[\]^_`{|}~' < /dev/urandom | dd bs=1 count=64 2>/dev/null)
|
||||
|
||||
|
||||
DEFAULT_LANGUAGES=("deu eng fra ita spa")
|
||||
|
||||
@@ -73,4 +73,6 @@ extra:
|
||||
link: https://matrix.to/#/#paperless:matrix.org
|
||||
plugins:
|
||||
- search
|
||||
- glightbox
|
||||
- glightbox:
|
||||
skip_classes:
|
||||
- no-lightbox
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
#PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT
|
||||
#PAPERLESS_CONSUMER_BARCODE_UPSCALE=0.0
|
||||
#PAPERLESS_CONSUMER_BARCODE_DPI=300
|
||||
#PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE=false
|
||||
#PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING={"TAG:(.*)": "\\g<1>"}
|
||||
#PAPERLESS_CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED=false
|
||||
#PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME=double-sided
|
||||
#PAPERLESS_CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT=false
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"fr-FR": "src/locale/messages.fr_FR.xlf",
|
||||
"hu-HU": "src/locale/messages.hu_HU.xlf",
|
||||
"it-IT": "src/locale/messages.it_IT.xlf",
|
||||
"ja-JP": "src/locale/messages.ja_JP.xlf",
|
||||
"lb-LU": "src/locale/messages.lb_LU.xlf",
|
||||
"nl-NL": "src/locale/messages.nl_NL.xlf",
|
||||
"no-NO": "src/locale/messages.no_NO.xlf",
|
||||
|
||||
@@ -2700,7 +2700,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2734,7 +2734,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2768,7 +2768,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2802,7 +2802,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2836,7 +2836,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2870,7 +2870,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2904,7 +2904,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2938,7 +2938,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2972,7 +2972,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3006,7 +3006,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3040,7 +3040,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3074,7 +3074,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3108,7 +3108,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3142,7 +3142,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3176,7 +3176,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3210,7 +3210,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3244,7 +3244,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3278,7 +3278,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -3312,7 +3312,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
|
||||
@@ -425,7 +425,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -470,7 +470,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -645,7 +645,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -685,7 +685,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -729,7 +729,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
|
||||
@@ -843,7 +843,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -994,7 +994,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
|
||||
@@ -996,7 +996,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1301,7 +1301,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1484,7 +1484,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1518,7 +1518,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1552,7 +1552,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1586,7 +1586,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1620,7 +1620,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1654,7 +1654,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1688,7 +1688,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1722,7 +1722,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1756,7 +1756,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1790,7 +1790,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1824,7 +1824,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1858,7 +1858,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1892,7 +1892,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1926,7 +1926,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1960,7 +1960,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -1994,7 +1994,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2028,7 +2028,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2062,7 +2062,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2096,7 +2096,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2130,7 +2130,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2164,7 +2164,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2198,7 +2198,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2232,7 +2232,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2266,7 +2266,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2300,7 +2300,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2334,7 +2334,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2368,7 +2368,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2402,7 +2402,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2436,7 +2436,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
@@ -2470,7 +2470,7 @@
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": -1,
|
||||
"status": 200,
|
||||
"statusText": "",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
4036
src-ui/package-lock.json
generated
4036
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,17 +11,17 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^17.0.4",
|
||||
"@angular/common": "~17.0.8",
|
||||
"@angular/compiler": "~17.0.8",
|
||||
"@angular/core": "~17.0.8",
|
||||
"@angular/forms": "~17.0.8",
|
||||
"@angular/localize": "~17.0.8",
|
||||
"@angular/platform-browser": "~17.0.8",
|
||||
"@angular/platform-browser-dynamic": "~17.0.8",
|
||||
"@angular/router": "~17.0.8",
|
||||
"@angular/cdk": "^17.1.2",
|
||||
"@angular/common": "~17.1.2",
|
||||
"@angular/compiler": "~17.1.2",
|
||||
"@angular/core": "~17.1.2",
|
||||
"@angular/forms": "~17.1.2",
|
||||
"@angular/localize": "~17.1.2",
|
||||
"@angular/platform-browser": "~17.1.2",
|
||||
"@angular/platform-browser-dynamic": "~17.1.2",
|
||||
"@angular/router": "~17.1.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
|
||||
"@ng-select/ng-select": "^12.0.4",
|
||||
"@ng-select/ng-select": "^12.0.6",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.2",
|
||||
@@ -31,33 +31,33 @@
|
||||
"ngx-color": "^9.0.0",
|
||||
"ngx-cookie-service": "^17.0.1",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
"ngx-ui-tour-ng-bootstrap": "^14.0.1",
|
||||
"ngx-ui-tour-ng-bootstrap": "^14.0.2",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^9.0.1",
|
||||
"zone.js": "^0.14.2"
|
||||
"zone.js": "^0.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "17.0.0",
|
||||
"@angular-devkit/build-angular": "~17.0.8",
|
||||
"@angular-eslint/builder": "17.1.1",
|
||||
"@angular-eslint/eslint-plugin": "17.1.1",
|
||||
"@angular-eslint/eslint-plugin-template": "17.1.1",
|
||||
"@angular-eslint/schematics": "17.1.1",
|
||||
"@angular-eslint/template-parser": "17.1.1",
|
||||
"@angular/cli": "~17.0.8",
|
||||
"@angular/compiler-cli": "~17.0.7",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/node": "^20.10.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"@angular-devkit/build-angular": "~17.1.2",
|
||||
"@angular-eslint/builder": "17.2.1",
|
||||
"@angular-eslint/eslint-plugin": "17.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "17.2.1",
|
||||
"@angular-eslint/schematics": "17.2.1",
|
||||
"@angular-eslint/template-parser": "17.2.1",
|
||||
"@angular/cli": "~17.1.2",
|
||||
"@angular/compiler-cli": "~17.1.2",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.16",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^13.1.4",
|
||||
"jest-preset-angular": "^14.0.0",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"ts-node": "~10.9.1",
|
||||
|
||||
@@ -23,6 +23,7 @@ import localeFi from '@angular/common/locales/fi'
|
||||
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 localeLb from '@angular/common/locales/lb'
|
||||
import localeNl from '@angular/common/locales/nl'
|
||||
import localeNo from '@angular/common/locales/no'
|
||||
@@ -53,6 +54,7 @@ registerLocaleData(localeFi)
|
||||
registerLocaleData(localeFr)
|
||||
registerLocaleData(localeHu)
|
||||
registerLocaleData(localeIt)
|
||||
registerLocaleData(localeJa)
|
||||
registerLocaleData(localeLb)
|
||||
registerLocaleData(localeNl)
|
||||
registerLocaleData(localeNo)
|
||||
|
||||
@@ -186,8 +186,8 @@ export const routes: Routes = [
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Admin,
|
||||
action: PermissionAction.Change,
|
||||
type: PermissionType.AppConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -112,6 +112,7 @@ import { SwitchComponent } from './components/common/input/switch/switch.compone
|
||||
import { ConfigComponent } from './components/admin/config/config.component'
|
||||
import { FileComponent } from './components/common/input/file/file.component'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
|
||||
import {
|
||||
archive,
|
||||
arrowCounterclockwise,
|
||||
@@ -137,6 +138,7 @@ import {
|
||||
chevronDoubleLeft,
|
||||
chevronDoubleRight,
|
||||
clipboard,
|
||||
clipboardCheck,
|
||||
clipboardCheckFill,
|
||||
clipboardFill,
|
||||
dash,
|
||||
@@ -221,6 +223,7 @@ const icons = {
|
||||
chevronDoubleLeft,
|
||||
chevronDoubleRight,
|
||||
clipboard,
|
||||
clipboardCheck,
|
||||
clipboardCheckFill,
|
||||
clipboardFill,
|
||||
dash,
|
||||
@@ -295,6 +298,7 @@ import localeFi from '@angular/common/locales/fi'
|
||||
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 localeLb from '@angular/common/locales/lb'
|
||||
import localeNl from '@angular/common/locales/nl'
|
||||
import localeNo from '@angular/common/locales/no'
|
||||
@@ -325,6 +329,7 @@ registerLocaleData(localeFi)
|
||||
registerLocaleData(localeFr)
|
||||
registerLocaleData(localeHu)
|
||||
registerLocaleData(localeIt)
|
||||
registerLocaleData(localeJa)
|
||||
registerLocaleData(localeLb)
|
||||
registerLocaleData(localeNl)
|
||||
registerLocaleData(localeNo)
|
||||
@@ -437,6 +442,7 @@ function initializeApp(settings: SettingsService) {
|
||||
SwitchComponent,
|
||||
ConfigComponent,
|
||||
FileComponent,
|
||||
ConfirmButtonComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PageHeaderComponent } from '../../common/page-header/page-header.compon
|
||||
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'
|
||||
|
||||
describe('ConfigComponent', () => {
|
||||
let component: ConfigComponent
|
||||
@@ -43,6 +44,7 @@ describe('ConfigComponent', () => {
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { of, throwError } from 'rxjs'
|
||||
import { HttpClientTestingModule } 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'
|
||||
|
||||
const paperless_logs = [
|
||||
'[2023-05-29 03:05:01,224] [DEBUG] [paperless.tasks] Training data unchanged.',
|
||||
@@ -37,7 +38,12 @@ describe('LogsComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LogsComponent, PageHeaderComponent],
|
||||
providers: [],
|
||||
imports: [HttpClientTestingModule, BrowserModule, NgbModule],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
BrowserModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
logService = TestBed.inject(LogService)
|
||||
|
||||
@@ -158,6 +158,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Document editing</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="offset-md-3 col">
|
||||
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4" i18n>Bulk editing</h4>
|
||||
|
||||
<div class="row mb-3">
|
||||
@@ -311,7 +319,15 @@
|
||||
</div>
|
||||
<div class="mb-2 col-auto">
|
||||
<label class="form-label" for="name_{{view.id}}" i18n>Actions</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }" i18n>Delete</button>
|
||||
|
||||
<pngx-confirm-button
|
||||
label="Delete"
|
||||
i18n-label
|
||||
(confirm)="deleteSavedView(view)"
|
||||
*pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.SavedView }"
|
||||
buttonClasses="btn-sm btn-outline-danger form-control"
|
||||
iconName="trash">
|
||||
</pngx-confirm-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import { TextComponent } from '../../common/input/text/text.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { SettingsComponent } from './settings.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
||||
|
||||
const savedViews = [
|
||||
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
||||
@@ -82,6 +84,7 @@ describe('SettingsComponent', () => {
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
IfOwnerDirective,
|
||||
ConfirmButtonComponent,
|
||||
],
|
||||
providers: [CustomDatePipe, DatePipe, PermissionsGuard],
|
||||
imports: [
|
||||
@@ -92,6 +95,7 @@ describe('SettingsComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
NgbAlertModule,
|
||||
NgSelectModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -287,7 +291,7 @@ describe('SettingsComponent', () => {
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
expect(storeSpy).toHaveBeenCalled()
|
||||
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
|
||||
expect(setSpy).toHaveBeenCalledTimes(24)
|
||||
expect(setSpy).toHaveBeenCalledTimes(25)
|
||||
|
||||
// succeed
|
||||
storeSpy.mockReturnValueOnce(of(true))
|
||||
|
||||
@@ -88,6 +88,7 @@ export class SettingsComponent
|
||||
defaultPermsViewGroups: new FormControl(null),
|
||||
defaultPermsEditUsers: new FormControl(null),
|
||||
defaultPermsEditGroups: new FormControl(null),
|
||||
documentEditingRemoveInboxTags: new FormControl(null),
|
||||
|
||||
notificationsConsumerNewDocument: new FormControl(null),
|
||||
notificationsConsumerSuccess: new FormControl(null),
|
||||
@@ -271,6 +272,9 @@ export class SettingsComponent
|
||||
defaultPermsEditGroups: this.settings.get(
|
||||
SETTINGS_KEYS.DEFAULT_PERMS_EDIT_GROUPS
|
||||
),
|
||||
documentEditingRemoveInboxTags: this.settings.get(
|
||||
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
|
||||
),
|
||||
savedViews: {},
|
||||
}
|
||||
}
|
||||
@@ -484,6 +488,10 @@ export class SettingsComponent
|
||||
SETTINGS_KEYS.DEFAULT_PERMS_EDIT_GROUPS,
|
||||
this.settingsForm.value.defaultPermsEditGroups
|
||||
)
|
||||
this.settings.set(
|
||||
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS,
|
||||
this.settingsForm.value.documentEditingRemoveInboxTags
|
||||
)
|
||||
this.settings.setLanguage(this.settingsForm.value.displayLanguage)
|
||||
this.settings
|
||||
.storeSettings()
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { TasksComponent } from './tasks.component'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const tasks: PaperlessTask[] = [
|
||||
{
|
||||
@@ -138,6 +139,7 @@ describe('TasksComponent', () => {
|
||||
NgbModule,
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -33,64 +33,64 @@
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (groups) {
|
||||
<h4 class="mt-4 d-flex">
|
||||
<ng-container i18n>Groups</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Group</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
@if (groups.length > 0) {
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
@for (group of groups; track group) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@if (groups) {
|
||||
<h4 class="mt-4 d-flex">
|
||||
<ng-container i18n>Groups</ng-container>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
|
||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Group</ng-container>
|
||||
</button>
|
||||
</h4>
|
||||
@if (groups.length > 0) {
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
@for (group of groups; track group) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
|
||||
<div class="col"></div>
|
||||
<div class="col"></div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (groups.length === 0) {
|
||||
<li class="list-group-item" i18n>No groups defined</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!users || !groups) {
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
@if (groups.length === 0) {
|
||||
<li class="list-group-item" i18n>No groups defined</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
|
||||
@if (!users || !groups) {
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import { SettingsComponent } from '../settings/settings.component'
|
||||
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'
|
||||
|
||||
const users = [
|
||||
{ id: 1, username: 'user1', is_superuser: false },
|
||||
@@ -92,6 +93,7 @@ describe('UsersAndGroupsComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
NgbAlertModule,
|
||||
NgSelectModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
fixture = TestBed.createComponent(UsersAndGroupsComponent)
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
<i-bs class="me-1" name="gear"></i-bs><span> <ng-container i18n>Settings</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
|
||||
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
|
||||
@@ -21,6 +21,10 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import {
|
||||
DjangoMessageLevel,
|
||||
DjangoMessagesService,
|
||||
} from 'src/app/services/django-messages.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
@@ -33,6 +37,7 @@ import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||
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'
|
||||
|
||||
const saved_views = [
|
||||
{
|
||||
@@ -82,6 +87,7 @@ describe('AppFrameComponent', () => {
|
||||
let permissionsService: PermissionsService
|
||||
let remoteVersionService: RemoteVersionService
|
||||
let toastService: ToastService
|
||||
let messagesService: DjangoMessagesService
|
||||
let openDocumentsService: OpenDocumentsService
|
||||
let searchService: SearchService
|
||||
let documentListViewService: DocumentListViewService
|
||||
@@ -101,6 +107,7 @@ describe('AppFrameComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
DragDropModule,
|
||||
NgbModalModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
@@ -121,6 +128,7 @@ describe('AppFrameComponent', () => {
|
||||
RemoteVersionService,
|
||||
IfPermissionsDirective,
|
||||
ToastService,
|
||||
DjangoMessagesService,
|
||||
OpenDocumentsService,
|
||||
SearchService,
|
||||
NgbModal,
|
||||
@@ -149,6 +157,7 @@ describe('AppFrameComponent', () => {
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
remoteVersionService = TestBed.inject(RemoteVersionService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
messagesService = TestBed.inject(DjangoMessagesService)
|
||||
openDocumentsService = TestBed.inject(OpenDocumentsService)
|
||||
searchService = TestBed.inject(SearchService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
@@ -391,4 +400,19 @@ describe('AppFrameComponent', () => {
|
||||
backdrop: 'static',
|
||||
})
|
||||
})
|
||||
|
||||
it('should show toasts for django messages', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
jest.spyOn(messagesService, 'get').mockReturnValue([
|
||||
{ level: DjangoMessageLevel.WARNING, message: 'Test warning' },
|
||||
{ level: DjangoMessageLevel.ERROR, message: 'Test error' },
|
||||
{ level: DjangoMessageLevel.SUCCESS, message: 'Test success' },
|
||||
{ level: DjangoMessageLevel.INFO, message: 'Test info' },
|
||||
{ level: DjangoMessageLevel.DEBUG, message: 'Test debug' },
|
||||
])
|
||||
component.ngOnInit()
|
||||
expect(toastErrorSpy).toHaveBeenCalledTimes(2)
|
||||
expect(toastInfoSpy).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
} from 'rxjs/operators'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import {
|
||||
DjangoMessageLevel,
|
||||
DjangoMessagesService,
|
||||
} from 'src/app/services/django-messages.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { SearchService } from 'src/app/services/rest/search.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
@@ -73,7 +77,8 @@ export class AppFrameComponent
|
||||
public tasksService: TasksService,
|
||||
private readonly toastService: ToastService,
|
||||
private modalService: NgbModal,
|
||||
permissionsService: PermissionsService
|
||||
public permissionsService: PermissionsService,
|
||||
private djangoMessagesService: DjangoMessagesService
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -92,6 +97,20 @@ export class AppFrameComponent
|
||||
this.checkForUpdates()
|
||||
}
|
||||
this.tasksService.reload()
|
||||
|
||||
this.djangoMessagesService.get().forEach((message) => {
|
||||
switch (message.level) {
|
||||
case DjangoMessageLevel.ERROR:
|
||||
case DjangoMessageLevel.WARNING:
|
||||
this.toastService.showError(message.message)
|
||||
break
|
||||
case DjangoMessageLevel.SUCCESS:
|
||||
case DjangoMessageLevel.INFO:
|
||||
case DjangoMessageLevel.DEBUG:
|
||||
this.toastService.showInfo(message.message)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleSlimSidebar(): void {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { ClearableBadgeComponent } from './clearable-badge.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('ClearableBadgeComponent', () => {
|
||||
let component: ClearableBadgeComponent
|
||||
@@ -8,6 +9,7 @@ describe('ClearableBadgeComponent', () => {
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ClearableBadgeComponent],
|
||||
imports: [NgxBootstrapIconsModule.pick(allIcons)],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn {{buttonClasses}}"
|
||||
(click)="onClick($event)"
|
||||
[disabled]="disabled"
|
||||
[ngbPopover]="popoverContent"
|
||||
[autoClose]="true"
|
||||
(hidden)="confirming = false"
|
||||
#popover="ngbPopover"
|
||||
popoverClass="popover-slim"
|
||||
>
|
||||
@if (iconName) {
|
||||
<i-bs [class.me-1]="label" name="{{iconName}}"></i-bs>
|
||||
}
|
||||
<ng-container>{{label}}</ng-container>
|
||||
</button>
|
||||
|
||||
<ng-template #popoverContent>
|
||||
<div>
|
||||
{{confirmMessage}} <button class="btn btn-link btn-sm text-danger p-0 m-0 lh-1" type="button" (click)="onConfirm($event)">Yes</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,12 @@
|
||||
// Taken from bootstrap rules, obv
|
||||
::ng-deep .input-group > pngx-confirm-button:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) > button,
|
||||
::ng-deep .btn-group > pngx-confirm-button:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) > button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
::ng-deep .input-group:not(.has-validation) > pngx-confirm-button:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) > button,
|
||||
::ng-deep .btn-group:not(.has-validation) > pngx-confirm-button:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) > button {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { ConfirmButtonComponent } from './confirm-button.component'
|
||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('ConfirmButtonComponent', () => {
|
||||
let component: ConfirmButtonComponent
|
||||
let fixture: ComponentFixture<ConfirmButtonComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ConfirmButtonComponent],
|
||||
imports: [NgbPopoverModule, NgxBootstrapIconsModule.pick(allIcons)],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ConfirmButtonComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should show confirm on click', () => {
|
||||
expect(component.popover.isOpen()).toBeFalsy()
|
||||
expect(component.confirming).toBeFalsy()
|
||||
component.onClick(new MouseEvent('click'))
|
||||
expect(component.popover.isOpen()).toBeTruthy()
|
||||
expect(component.confirming).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should emit confirm on confirm', () => {
|
||||
const confirmSpy = jest.spyOn(component.confirm, 'emit')
|
||||
component.onConfirm(new MouseEvent('click'))
|
||||
expect(confirmSpy).toHaveBeenCalled()
|
||||
expect(component.popover.isOpen()).toBeFalsy()
|
||||
expect(component.confirming).toBeFalsy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-confirm-button',
|
||||
templateUrl: './confirm-button.component.html',
|
||||
styleUrl: './confirm-button.component.scss',
|
||||
})
|
||||
export class ConfirmButtonComponent {
|
||||
@Input()
|
||||
label: string
|
||||
|
||||
@Input()
|
||||
confirmMessage: string = $localize`Are you sure?`
|
||||
|
||||
@Input()
|
||||
buttonClasses: string = 'btn-primary'
|
||||
|
||||
@Input()
|
||||
iconName: string
|
||||
|
||||
@Input()
|
||||
disabled: boolean = false
|
||||
|
||||
@Output()
|
||||
confirm: EventEmitter<void> = new EventEmitter<void>()
|
||||
|
||||
@ViewChild('popover') popover: NgbPopover
|
||||
|
||||
public confirming: boolean = false
|
||||
|
||||
public onClick(event: MouseEvent) {
|
||||
if (!this.confirming) {
|
||||
this.confirming = true
|
||||
this.popover.open()
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
public onConfirm(event: MouseEvent) {
|
||||
this.confirm.emit()
|
||||
this.confirming = false
|
||||
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
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'
|
||||
|
||||
const fields: CustomField[] = [
|
||||
{
|
||||
@@ -40,7 +41,6 @@ describe('CustomFieldsDropdownComponent', () => {
|
||||
let customFieldService: CustomFieldsService
|
||||
let toastService: ToastService
|
||||
let modalService: NgbModal
|
||||
let httpController: HttpTestingController
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -52,10 +52,10 @@ describe('CustomFieldsDropdownComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
NgbModalModule,
|
||||
NgbDropdownModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
})
|
||||
customFieldService = TestBed.inject(CustomFieldsService)
|
||||
httpController = TestBed.inject(HttpTestingController)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
jest.spyOn(customFieldService, 'listAll').mockReturnValue(
|
||||
|
||||
@@ -10,20 +10,17 @@ import {
|
||||
DateSelection,
|
||||
RelativeDate,
|
||||
} from './date-dropdown.component'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { HttpClientTestingModule } 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'
|
||||
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'
|
||||
|
||||
describe('DateDropdownComponent', () => {
|
||||
let component: DateDropdownComponent
|
||||
let httpTestingController: HttpTestingController
|
||||
let settingsService: SettingsService
|
||||
let settingsSpy
|
||||
|
||||
@@ -40,10 +37,10 @@ describe('DateDropdownComponent', () => {
|
||||
NgbModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
|
||||
@if (typeFieldDisabled) {
|
||||
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<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-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></pngx-permissions-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<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-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></pngx-input-select>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<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="Account" [items]="accounts" formControlName="account"></pngx-input-select>
|
||||
<pngx-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></pngx-input-text>
|
||||
<pngx-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></pngx-input-number>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<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-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { TagEditDialogComponent } from './tag-edit-dialog.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('TagEditDialogComponent', () => {
|
||||
let component: TagEditDialogComponent
|
||||
@@ -38,6 +39,7 @@ describe('TagEditDialogComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||
@@ -38,9 +38,13 @@
|
||||
@if(trigger.id > -1) {
|
||||
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{trigger.id}}</span>
|
||||
}
|
||||
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeTrigger(i)">
|
||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
<pngx-confirm-button
|
||||
label="Delete"
|
||||
i18n-label
|
||||
(confirm)="removeTrigger(i)"
|
||||
buttonClasses="btn-link text-danger ms-2"
|
||||
iconName="trash">
|
||||
</pngx-confirm-button>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordionCollapse>
|
||||
@@ -76,9 +80,13 @@
|
||||
@if(action.id > -1) {
|
||||
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
|
||||
}
|
||||
<button type="button" class="btn btn-link text-danger ms-2" (click)="removeAction(i)">
|
||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
<pngx-confirm-button
|
||||
label="Delete"
|
||||
i18n-label
|
||||
(confirm)="removeAction(i)"
|
||||
buttonClasses="btn-link text-danger ms-2"
|
||||
iconName="trash">
|
||||
</pngx-confirm-button>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordionCollapse>
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
WorkflowActionType,
|
||||
} from 'src/app/data/workflow-action'
|
||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
|
||||
import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component'
|
||||
|
||||
const workflow: Workflow = {
|
||||
name: 'Workflow 1',
|
||||
@@ -85,6 +86,7 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
SafeHtmlPipe,
|
||||
ConfirmButtonComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
|
||||
@@ -45,10 +45,18 @@
|
||||
</div>
|
||||
}
|
||||
@if (editing) {
|
||||
<button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
||||
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
||||
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
|
||||
</button>
|
||||
@if ((selectionModel.itemsSorted | filter: filterText).length === 0 && createRef !== undefined) {
|
||||
<button class="list-group-item list-group-item-action bg-light" (click)="createClicked()" [disabled]="disabled">
|
||||
<small class="ms-2"><ng-container i18n>Create</ng-container> "{{filterText}}"</small>
|
||||
<i-bs width="1.5em" height="1em" name="plus"></i-bs>
|
||||
</button>
|
||||
}
|
||||
@if ((selectionModel.itemsSorted | filter: filterText).length > 0) {
|
||||
<button class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
||||
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
||||
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (!editing && manyToOne) {
|
||||
<div class="list-group-item list-group-item-note pt-1 pb-2">
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { TagComponent } from '../tag/tag.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const items: Tag[] = [
|
||||
{
|
||||
@@ -63,7 +64,12 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
ClearableBadgeComponent,
|
||||
],
|
||||
providers: [FilterPipe],
|
||||
imports: [NgbModule, FormsModule, ReactiveFormsModule],
|
||||
imports: [
|
||||
NgbModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||
@@ -215,6 +221,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should apply changes and close when apply button clicked', () => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
@@ -236,6 +243,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should apply on close if enabled', () => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.applyOnClose = true
|
||||
component.selectionModel = selectionModel
|
||||
@@ -253,6 +261,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
@@ -279,6 +288,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
@@ -298,6 +308,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
let applyResult: ChangedItems
|
||||
component.apply.subscribe((result) => (applyResult = result))
|
||||
@@ -319,6 +330,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should support arrow keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
@@ -363,6 +375,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
@@ -398,6 +411,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
@@ -422,6 +436,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should toggle logical operator', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.manyToOne = true
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
@@ -450,6 +465,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
|
||||
it('should toggle intersection include / exclude', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
@@ -484,4 +500,46 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.itemsSorted).toEqual([nullItem, items[1], items[0]])
|
||||
})
|
||||
|
||||
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
|
||||
component.filterText = 'Test Filter Text'
|
||||
component.createRef = jest.fn()
|
||||
component.createClicked()
|
||||
expect(component.creating).toBeTruthy()
|
||||
expect(component.createRef).toHaveBeenCalledWith('Test Filter Text')
|
||||
const openSpy = jest.spyOn(component.dropdown, 'open')
|
||||
component.dropdownOpenChange(false)
|
||||
expect(openSpy).toHaveBeenCalled() // should keep open
|
||||
}))
|
||||
|
||||
it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.icon = 'tag-fill'
|
||||
component.editing = true
|
||||
component.createRef = jest.fn()
|
||||
const createSpy = jest.spyOn(component, 'createClicked')
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
component.filterText = 'FooBar'
|
||||
fixture.detectChanges()
|
||||
component.listFilterTextInput.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Enter' })
|
||||
)
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
tick(300)
|
||||
expect(createSpy).toHaveBeenCalled()
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -398,6 +398,11 @@ export class FilterableDropdownComponent {
|
||||
@Input()
|
||||
disabled = false
|
||||
|
||||
@Input()
|
||||
createRef: (name) => void
|
||||
|
||||
creating: boolean = false
|
||||
|
||||
@Output()
|
||||
apply = new EventEmitter<ChangedItems>()
|
||||
|
||||
@@ -437,6 +442,11 @@ export class FilterableDropdownComponent {
|
||||
}
|
||||
}
|
||||
|
||||
createClicked() {
|
||||
this.creating = true
|
||||
this.createRef(this.filterText)
|
||||
}
|
||||
|
||||
dropdownOpenChange(open: boolean): void {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
@@ -448,9 +458,14 @@ export class FilterableDropdownComponent {
|
||||
}
|
||||
this.opened.next(this)
|
||||
} else {
|
||||
this.filterText = ''
|
||||
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
||||
this.apply.emit(this.selectionModel.diff())
|
||||
if (this.creating) {
|
||||
this.dropdown.open()
|
||||
this.creating = false
|
||||
} else {
|
||||
this.filterText = ''
|
||||
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
||||
this.apply.emit(this.selectionModel.diff())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,6 +481,8 @@ export class FilterableDropdownComponent {
|
||||
this.dropdown.close()
|
||||
}
|
||||
}, 200)
|
||||
} else if (filtered.length == 0 && this.createRef) {
|
||||
this.createClicked()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core'
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
import { MatchingModel } from 'src/app/data/matching-model'
|
||||
|
||||
export enum ToggleableItemState {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { ColorComponent } from './color.component'
|
||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ColorSliderModule } from 'ngx-color/slider'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('ColorComponent', () => {
|
||||
let component: ColorComponent
|
||||
@@ -22,6 +23,7 @@ describe('ColorComponent', () => {
|
||||
ReactiveFormsModule,
|
||||
NgbPopoverModule,
|
||||
ColorSliderModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('DateComponent', () => {
|
||||
let component: DateComponent
|
||||
@@ -33,6 +34,7 @@ describe('DateComponent', () => {
|
||||
HttpClientTestingModule,
|
||||
NgbDatepickerModule,
|
||||
RouterTestingModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -47,17 +47,25 @@ describe('NumberComponent', () => {
|
||||
expect(component.value).toEqual(1002)
|
||||
})
|
||||
|
||||
it('should support float & monetary values', () => {
|
||||
component.writeValue(11.13)
|
||||
expect(component.value).toEqual(11)
|
||||
it('should support float, monetary values & scientific notation', () => {
|
||||
const mockFn = jest.fn()
|
||||
component.registerOnChange(mockFn)
|
||||
|
||||
component.step = 1
|
||||
component.onChange(11.13)
|
||||
expect(mockFn).toHaveBeenCalledWith(11)
|
||||
|
||||
component.onChange(1.23456789e8)
|
||||
expect(mockFn).toHaveBeenCalledWith(123456789)
|
||||
|
||||
component.step = 0.01
|
||||
component.onChange(11.1)
|
||||
expect(mockFn).toHaveBeenCalledWith('11.10')
|
||||
})
|
||||
|
||||
it('should display monetary values fixed to 2 decimals', () => {
|
||||
component.step = 0.01
|
||||
component.writeValue(11.1)
|
||||
expect(component.value).toEqual('11.10')
|
||||
component.step = 0.1
|
||||
component.writeValue(12.3456)
|
||||
expect(component.value).toEqual(12.3456)
|
||||
// float (step = .1) doesn't force 2 decimals
|
||||
component.writeValue(11.1)
|
||||
expect(component.value).toEqual(11.1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,8 +36,18 @@ export class NumberComponent extends AbstractInputComponent<number> {
|
||||
})
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = (newValue: any) => {
|
||||
// number validation
|
||||
if (this.step === 1 && newValue?.toString().indexOf('e') === -1)
|
||||
newValue = parseInt(newValue, 10)
|
||||
if (this.step === 0.01) newValue = parseFloat(newValue).toFixed(2)
|
||||
fn(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(newValue: any): void {
|
||||
if (this.step === 1) newValue = parseInt(newValue, 10)
|
||||
// Allow monetary values to be displayed with 2 decimals
|
||||
if (this.step === 0.01) newValue = parseFloat(newValue).toFixed(2)
|
||||
super.writeValue(newValue)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@angular/forms'
|
||||
import { PasswordComponent } from './password.component'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('PasswordComponent', () => {
|
||||
let component: PasswordComponent
|
||||
@@ -16,7 +17,11 @@ describe('PasswordComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PasswordComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PasswordComponent)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div [ngClass]="{'col-md-9': horizontal, 'align-items-center': horizontal, 'd-flex': horizontal}">
|
||||
<div [ngClass]="{'align-items-center': horizontal, 'd-flex': horizontal}">
|
||||
<div class="form-check form-switch">
|
||||
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||
@if (horizontal) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ColorComponent } from '../color/color.component'
|
||||
import { PermissionsFormComponent } from '../permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../select/select.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const tags: Tag[] = [
|
||||
{
|
||||
@@ -99,6 +100,7 @@ describe('TagsComponent', () => {
|
||||
NgbModalModule,
|
||||
NgbAccordionModule,
|
||||
NgbPopoverModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { UrlComponent } from './url.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('TextComponent', () => {
|
||||
let component: UrlComponent
|
||||
@@ -15,7 +16,11 @@ describe('TextComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [UrlComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(UrlComponent)
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
@if (!object && message) {
|
||||
<p class="mb-3" [innerHTML]="message | safeHtml"></p>
|
||||
}
|
||||
|
||||
<form [formGroup]="form">
|
||||
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
|
||||
<div class="form-group">
|
||||
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<div class="offset-lg-3 row">
|
||||
<pngx-input-switch i18n-title title="Merge with existing permissions" [horizontal]="true" [hint]="hint" formControlName="merge"></pngx-input-switch>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@@ -20,5 +23,5 @@
|
||||
<span class="visually-hidden" i18n>Loading...</span>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" [disabled]="!buttonsEnabled" i18n>Confirm</button>
|
||||
<button type="button" class="btn btn-primary" (click)="confirm()" [disabled]="!buttonsEnabled" i18n>Confirm</button>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
|
||||
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
|
||||
import { SwitchComponent } from '../input/switch/switch.component'
|
||||
|
||||
const set_permissions = {
|
||||
owner: 10,
|
||||
@@ -37,6 +38,7 @@ describe('PermissionsDialogComponent', () => {
|
||||
PermissionsDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
SelectComponent,
|
||||
SwitchComponent,
|
||||
PermissionsFormComponent,
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
@@ -112,4 +114,23 @@ describe('PermissionsDialogComponent', () => {
|
||||
expect(component.title).toEqual(`Edit permissions for ${obj.name}`)
|
||||
expect(component.permissions).toEqual(set_permissions)
|
||||
})
|
||||
|
||||
it('should toggle hint based on object existence (if editing) or merge flag', () => {
|
||||
component.form.get('merge').setValue(true)
|
||||
expect(component.hint.includes('Existing')).toBeTruthy()
|
||||
component.form.get('merge').setValue(false)
|
||||
expect(component.hint.includes('will be replaced')).toBeTruthy()
|
||||
component.object = {}
|
||||
expect(component.hint).toBeNull()
|
||||
})
|
||||
|
||||
it('should emit permissions and merge flag on confirm', () => {
|
||||
const confirmSpy = jest.spyOn(component.confirmClicked, 'emit')
|
||||
component.form.get('permissions_form').setValue(set_permissions)
|
||||
component.confirm()
|
||||
expect(confirmSpy).toHaveBeenCalledWith({
|
||||
permissions: set_permissions,
|
||||
merge: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ export class PermissionsDialogComponent {
|
||||
this.o = o
|
||||
this.title = $localize`Edit permissions for ` + o['name']
|
||||
this.form.patchValue({
|
||||
merge: true,
|
||||
permissions_form: {
|
||||
owner: o.owner,
|
||||
set_permissions: o.permissions,
|
||||
@@ -43,8 +44,9 @@ export class PermissionsDialogComponent {
|
||||
return this.o
|
||||
}
|
||||
|
||||
form = new FormGroup({
|
||||
public form = new FormGroup({
|
||||
permissions_form: new FormControl(),
|
||||
merge: new FormControl(true),
|
||||
})
|
||||
|
||||
buttonsEnabled: boolean = true
|
||||
@@ -66,11 +68,21 @@ export class PermissionsDialogComponent {
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
message =
|
||||
$localize`Note that permissions set here will override any existing permissions`
|
||||
get hint(): string {
|
||||
if (this.object) return null
|
||||
return this.form.get('merge').value
|
||||
? $localize`Existing owner, user and group permissions will be merged with these settings.`
|
||||
: $localize`Any and all existing owner, user and group permissions will be replaced.`
|
||||
}
|
||||
|
||||
cancelClicked() {
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
||||
confirm() {
|
||||
this.confirmClicked.emit({
|
||||
permissions: this.permissions,
|
||||
merge: this.form.get('merge').value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,22 +62,24 @@
|
||||
<i-bs width="1em" height="1em" name="check"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<div class="me-1 w-100">
|
||||
<ng-select
|
||||
name="user"
|
||||
class="user-select small"
|
||||
[(ngModel)]="selectionModel.includeUsers"
|
||||
[disabled]="disabled"
|
||||
[clearable]="false"
|
||||
[items]="users"
|
||||
bindLabel="username"
|
||||
multiple="true"
|
||||
bindValue="id"
|
||||
placeholder="Users"
|
||||
i18n-placeholder
|
||||
(change)="onUserSelect()">
|
||||
</ng-select>
|
||||
</div>
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.User)) {
|
||||
<div class="me-1 w-100">
|
||||
<ng-select
|
||||
name="user"
|
||||
class="user-select small"
|
||||
[(ngModel)]="selectionModel.includeUsers"
|
||||
[disabled]="disabled"
|
||||
[clearable]="false"
|
||||
[items]="users"
|
||||
bindLabel="username"
|
||||
multiple="true"
|
||||
bindValue="id"
|
||||
placeholder="Users"
|
||||
i18n-placeholder
|
||||
(change)="onUserSelect()">
|
||||
</ng-select>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
@if (selectionModel.ownerFilter === OwnerFilterType.NONE || selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
|
||||
<div class="list-group-item list-group-item-action d-flex align-items-center p-2 ps-3 border-bottom-0 border-start-0 border-end-0">
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const currentUserID = 13
|
||||
|
||||
@@ -69,6 +70,7 @@ describe('PermissionsFilterDropdownComponent', () => {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
||||
}
|
||||
|
||||
constructor(
|
||||
permissionsService: PermissionsService,
|
||||
public permissionsService: PermissionsService,
|
||||
userService: UserService,
|
||||
private settingsService: SettingsService
|
||||
) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const permissions = [
|
||||
'add_document',
|
||||
@@ -32,7 +33,12 @@ describe('PermissionsSelectComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PermissionsSelectComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgbModule],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PermissionsSelectComponent)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const doc = {
|
||||
id: 10,
|
||||
@@ -25,7 +26,10 @@ describe('PreviewPopupComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PreviewPopupComponent, PdfViewerComponent, SafeUrlPipe],
|
||||
imports: [HttpClientTestingModule],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
})
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
|
||||
@@ -41,14 +41,58 @@
|
||||
}
|
||||
<span class="visually-hidden" i18n>Copy</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="generateAuthToken()" i18n-title title="Regenerate auth token">
|
||||
<i-bs width="1.2em" height="1.2em" name="arrow-repeat"></i-bs>
|
||||
</button>
|
||||
<pngx-confirm-button
|
||||
title="Regenerate auth token"
|
||||
i18n-title
|
||||
buttonClasses=" btn-outline-secondary"
|
||||
iconName="arrow-repeat"
|
||||
[disabled]="!hasUsablePassword"
|
||||
(confirm)="generateAuthToken()">
|
||||
</pngx-confirm-button>
|
||||
</div>
|
||||
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
|
||||
</div>
|
||||
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
|
||||
</div>
|
||||
@if (socialAccounts?.length > 0) {
|
||||
<div class="mb-3">
|
||||
<p i18n>Connected social accounts</p>
|
||||
<ul class="list-group">
|
||||
@for (account of socialAccounts; track account.id) {
|
||||
<li class="list-group-item"
|
||||
ngbPopover="Set a password before disconnecting social account."
|
||||
i18n-ngbPopover
|
||||
[disablePopover]="hasUsablePassword"
|
||||
triggers="mouseenter:mouseleave">
|
||||
{{account.name}} ({{account.provider}})
|
||||
<pngx-confirm-button
|
||||
label="Disconnect"
|
||||
i18n-label
|
||||
title="Disconnect {{ account.name }} social account"
|
||||
i18n-title
|
||||
buttonClasses="btn-outline-danger btn-sm ms-2 align-baseline"
|
||||
iconName="trash"
|
||||
[disabled]="!hasUsablePassword"
|
||||
(confirm)="disconnectSocialAccount(account.id)">
|
||||
</pngx-confirm-button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="form-text text-muted text-end fst-italic" i18n>Warning: disconnecting social accounts cannot be undone</div>
|
||||
</div>
|
||||
}
|
||||
@if (socialAccountProviders?.length > 0) {
|
||||
<div class="mb-3">
|
||||
<p i18n>Connect new social account</p>
|
||||
<div class="list-group">
|
||||
@for (provider of socialAccountProviders; track provider.name) {
|
||||
<a class="list-group-item list-group-item-action text-primary d-flex align-items-center" href="{{ provider.login_url }}" rel="noopener noreferrer">
|
||||
{{provider.name}} <i-bs class="pb-1 ps-1" name="box-arrow-up-right"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
NgbAccordionModule,
|
||||
NgbActiveModal,
|
||||
NgbModalModule,
|
||||
NgbPopoverModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { TextComponent } from '../input/text/text.component'
|
||||
@@ -19,14 +20,25 @@ import { PasswordComponent } from '../input/password/password.component'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
|
||||
|
||||
const socialAccount = {
|
||||
id: 1,
|
||||
provider: 'test_provider',
|
||||
name: 'Test Provider',
|
||||
}
|
||||
const profile = {
|
||||
email: 'foo@bar.com',
|
||||
password: '*********',
|
||||
first_name: 'foo',
|
||||
last_name: 'bar',
|
||||
auth_token: '123456789abcdef',
|
||||
social_accounts: [socialAccount],
|
||||
}
|
||||
const socialAccountProviders = [
|
||||
{ name: 'Test Provider', login_url: 'https://example.com' },
|
||||
]
|
||||
|
||||
describe('ProfileEditDialogComponent', () => {
|
||||
let component: ProfileEditDialogComponent
|
||||
@@ -41,6 +53,7 @@ describe('ProfileEditDialogComponent', () => {
|
||||
ProfileEditDialogComponent,
|
||||
TextComponent,
|
||||
PasswordComponent,
|
||||
ConfirmButtonComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
@@ -49,6 +62,8 @@ describe('ProfileEditDialogComponent', () => {
|
||||
FormsModule,
|
||||
NgbModalModule,
|
||||
NgbAccordionModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgbPopoverModule,
|
||||
],
|
||||
})
|
||||
profileService = TestBed.inject(ProfileService)
|
||||
@@ -62,6 +77,11 @@ describe('ProfileEditDialogComponent', () => {
|
||||
it('should get profile on init, display in form', () => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
const getProvidersSpy = jest.spyOn(
|
||||
profileService,
|
||||
'getSocialAccountProviders'
|
||||
)
|
||||
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
||||
component.ngOnInit()
|
||||
expect(getSpy).toHaveBeenCalled()
|
||||
fixture.detectChanges()
|
||||
@@ -101,6 +121,11 @@ describe('ProfileEditDialogComponent', () => {
|
||||
expect(component.form.get('email_confirm').enabled).toBeFalsy()
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
const getProvidersSpy = jest.spyOn(
|
||||
profileService,
|
||||
'getSocialAccountProviders'
|
||||
)
|
||||
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
||||
component.ngOnInit()
|
||||
component.form.get('email').patchValue('foo@bar2.com')
|
||||
component.onEmailKeyUp({ target: { value: 'foo@bar2.com' } } as any)
|
||||
@@ -132,6 +157,12 @@ describe('ProfileEditDialogComponent', () => {
|
||||
expect(component.form.get('password_confirm').enabled).toBeFalsy()
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
const getProvidersSpy = jest.spyOn(
|
||||
profileService,
|
||||
'getSocialAccountProviders'
|
||||
)
|
||||
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
||||
component.hasUsablePassword = true
|
||||
component.ngOnInit()
|
||||
component.form.get('password').patchValue('new*pass')
|
||||
component.onPasswordKeyUp({
|
||||
@@ -165,6 +196,11 @@ describe('ProfileEditDialogComponent', () => {
|
||||
it('should logout on save if password changed', fakeAsync(() => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
const getProvidersSpy = jest.spyOn(
|
||||
profileService,
|
||||
'getSocialAccountProviders'
|
||||
)
|
||||
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
||||
component.ngOnInit()
|
||||
component['newPassword'] = 'new*pass'
|
||||
component.form.get('password').patchValue('new*pass')
|
||||
@@ -187,6 +223,11 @@ describe('ProfileEditDialogComponent', () => {
|
||||
it('should support auth token copy', fakeAsync(() => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
const getProvidersSpy = jest.spyOn(
|
||||
profileService,
|
||||
'getSocialAccountProviders'
|
||||
)
|
||||
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
||||
component.ngOnInit()
|
||||
const copySpy = jest.spyOn(clipboard, 'copy')
|
||||
component.copyAuthToken()
|
||||
@@ -218,4 +259,40 @@ describe('ProfileEditDialogComponent', () => {
|
||||
)
|
||||
expect(component.form.get('auth_token').value).toEqual(newToken)
|
||||
})
|
||||
|
||||
it('should get social account providers on init', () => {
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockReturnValue(of(profile))
|
||||
const getProvidersSpy = jest.spyOn(
|
||||
profileService,
|
||||
'getSocialAccountProviders'
|
||||
)
|
||||
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
||||
component.ngOnInit()
|
||||
expect(getProvidersSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should remove disconnected social account from component, show error if needed', () => {
|
||||
const disconnectSpy = jest.spyOn(profileService, 'disconnectSocialAccount')
|
||||
const getSpy = jest.spyOn(profileService, 'get')
|
||||
getSpy.mockImplementation(() => of(profile))
|
||||
component.ngOnInit()
|
||||
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
|
||||
expect(component.socialAccounts).toContainEqual(socialAccount)
|
||||
|
||||
// fail first
|
||||
disconnectSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('unable to disconnect'))
|
||||
)
|
||||
component.disconnectSocialAccount(socialAccount.id)
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
|
||||
// succeed
|
||||
disconnectSpy.mockReturnValue(of(socialAccount.id))
|
||||
component.disconnectSocialAccount(socialAccount.id)
|
||||
expect(disconnectSpy).toHaveBeenCalled()
|
||||
expect(component.socialAccounts).not.toContainEqual(socialAccount)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ProfileService } from 'src/app/services/profile.service'
|
||||
import { SocialAccount, SocialAccountProvider } from 'src/app/data/user-profile'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
@@ -30,6 +31,7 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
||||
private newPassword: string
|
||||
private passwordConfirm: string
|
||||
public showPasswordConfirm: boolean = false
|
||||
public hasUsablePassword: boolean = false
|
||||
|
||||
private currentEmail: string
|
||||
private newEmail: string
|
||||
@@ -38,6 +40,9 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
public copied: boolean = false
|
||||
|
||||
public socialAccounts: SocialAccount[] = []
|
||||
public socialAccountProviders: SocialAccountProvider[] = []
|
||||
|
||||
constructor(
|
||||
private profileService: ProfileService,
|
||||
public activeModal: NgbActiveModal,
|
||||
@@ -59,10 +64,19 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
||||
this.onEmailChange()
|
||||
})
|
||||
this.currentPassword = profile.password
|
||||
this.hasUsablePassword = profile.has_usable_password
|
||||
this.form.get('password').valueChanges.subscribe((newPassword) => {
|
||||
this.newPassword = newPassword
|
||||
this.onPasswordChange()
|
||||
})
|
||||
this.socialAccounts = profile.social_accounts
|
||||
})
|
||||
|
||||
this.profileService
|
||||
.getSocialAccountProviders()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((providers) => {
|
||||
this.socialAccountProviders = providers
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,4 +196,21 @@ export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
||||
this.copied = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
disconnectSocialAccount(id: number): void {
|
||||
this.profileService
|
||||
.disconnectSocialAccount(id)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (id: number) => {
|
||||
this.socialAccounts = this.socialAccounts.filter((a) => a.id != id)
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError(
|
||||
$localize`Error disconnecting social account`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { environment } from 'src/environments/environment'
|
||||
import { ShareLinksDropdownComponent } from './share-links-dropdown.component'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
describe('ShareLinksDropdownComponent', () => {
|
||||
let component: ShareLinksDropdownComponent
|
||||
@@ -29,7 +30,12 @@ describe('ShareLinksDropdownComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ShareLinksDropdownComponent],
|
||||
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
})
|
||||
|
||||
fixture = TestBed.createComponent(ShareLinksDropdownComponent)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { of } from 'rxjs'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Clipboard } from '@angular/cdk/clipboard'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const toasts = [
|
||||
{
|
||||
@@ -45,7 +46,11 @@ describe('ToastsComponent', () => {
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ToastsComponent],
|
||||
imports: [HttpClientTestingModule, NgbModule],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgbModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ToastService,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { SavedView } from 'src/app/data/saved-view'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const saved_views = [
|
||||
{
|
||||
@@ -107,6 +108,7 @@ describe('DashboardComponent', () => {
|
||||
RouterTestingModule,
|
||||
TourNgBootstrapModule,
|
||||
DragDropModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -15,8 +15,14 @@
|
||||
<tr>
|
||||
<th scope="col" i18n>Created</th>
|
||||
<th scope="col" i18n>Title</th>
|
||||
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
|
||||
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
||||
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
|
||||
} @else {
|
||||
<th scope="col" class="d-none d-md-table-cell"></th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -26,13 +32,15 @@
|
||||
<td class="py-2 py-md-3">
|
||||
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
|
||||
</td>
|
||||
<td class="py-2 py-md-3 d-none d-md-table-cell">
|
||||
@for (t of doc.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
|
||||
}
|
||||
</td>
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
||||
<td class="py-2 py-md-3 d-none d-md-table-cell">
|
||||
@for (t of doc.tags$ | async; track t) {
|
||||
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td class="position-relative py-2 py-md-3 d-none d-md-table-cell">
|
||||
@if (doc.correspondent !== null) {
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && doc.correspondent !== null) {
|
||||
<a class="btn-link text-dark text-decoration-none py-2 py-md-3" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a>
|
||||
}
|
||||
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
|
||||
|
||||
@@ -30,6 +30,7 @@ import { By } from '@angular/platform-browser'
|
||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const savedView: SavedView = {
|
||||
id: 1,
|
||||
@@ -94,6 +95,7 @@ describe('SavedViewWidgetComponent', () => {
|
||||
NgbModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
DragDropModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { DocumentListViewService } from 'src/app/services/document-list-view.ser
|
||||
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-saved-view-widget',
|
||||
@@ -40,7 +41,8 @@ export class SavedViewWidgetComponent
|
||||
private list: DocumentListViewService,
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
public documentListViewService: DocumentListViewService
|
||||
public documentListViewService: DocumentListViewService,
|
||||
public permissionsService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@@ -5,69 +5,77 @@
|
||||
<button class="btn btn-sm btn-outline-primary mt-3" (click)="fileUpload.click()" i18n>Browse files</button>
|
||||
<input type="file" class="visually-hidden" (change)="onFileSelected($event)" multiple #fileUpload>
|
||||
</form>
|
||||
<div class="fixed-bottom p-2 p-md-4" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'">
|
||||
<div class="row d-flex justify-content-end">
|
||||
<div class="col col-lg-4 col-xl-3 d-flex px-4 justify-content-between align-items-center">
|
||||
@if (getStatus().length > 0) {
|
||||
<p class="m-0 small text-muted">{{getStatusSummary()}}</p>
|
||||
}
|
||||
@if (getStatusCompleted().length > 0) {
|
||||
<a class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" >
|
||||
<span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span>
|
||||
<i-bs name="check2-all"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@for (status of getStatus(); track status) {
|
||||
<div>
|
||||
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (getStatusHidden().length) {
|
||||
<div class="alerts-hidden">
|
||||
@if (!alertsExpanded) {
|
||||
<p class="mt-3 mb-0 text-center">
|
||||
<span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span>
|
||||
•
|
||||
<a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
|
||||
</p>
|
||||
}
|
||||
<div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded">
|
||||
@for (status of getStatusHidden(); track status) {
|
||||
<div>
|
||||
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
|
||||
@if (getStatus().length > 0) {
|
||||
<div class="fixed-bottom p-2 p-md-4 d-flex justify-content-end pe-none" [ngClass]="slimSidebarEnabled ? 'col-slim' : 'offset-md-3 offset-lg-2'">
|
||||
<div class="col col-lg-4 col-xl-3 ps-0 pe-0 ps-lg-3 pe-lg-0 pe-auto">
|
||||
<div class="card shadow-sm consumer-status-card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col d-flex justify-content-between align-items-center">
|
||||
@if (getStatus().length > 0) {
|
||||
<p class="m-0 small text-muted">{{getStatusSummary()}}</p>
|
||||
}
|
||||
@if (getStatusCompleted().length > 0) {
|
||||
<a class="btn-link" (click)="dismissCompleted()" [routerLink]="[]" >
|
||||
<span class="me-1" i18n="This button dismisses all status messages about processed documents on the dashboard (failed and successful)">Dismiss completed</span>
|
||||
<i-bs name="check2-all"></i-bs>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@for (status of getStatus(); track status) {
|
||||
<div>
|
||||
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
|
||||
</div>
|
||||
}
|
||||
@if (getStatusHidden().length) {
|
||||
<div class="alerts-hidden">
|
||||
@if (!alertsExpanded) {
|
||||
<p class="mt-3 mb-0 text-center">
|
||||
<span i18n="This is shown as a summary line when there are more than 5 document in the processing pipeline.">{getStatusHidden().length, plural, =1 {One more document} other {{{getStatusHidden().length}} more documents}}</span>
|
||||
•
|
||||
<a [routerLink]="[]" (click)="alertsExpanded = !alertsExpanded" aria-controls="hiddenAlerts" [attr.aria-expanded]="alertsExpanded" i18n>Show all</a>
|
||||
</p>
|
||||
}
|
||||
<div #hiddenAlerts="ngbCollapse" [(ngbCollapse)]="!alertsExpanded">
|
||||
@for (status of getStatusHidden(); track status) {
|
||||
<div>
|
||||
<ng-container [ngTemplateOutlet]="consumerAlert" [ngTemplateOutletContext]="{ $implicit: status }"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</pngx-widget-frame>
|
||||
|
||||
<ng-template #consumerAlert let-status>
|
||||
<div class="row d-flex justify-content-end">
|
||||
<div class="col col-lg-4 col-xl-3">
|
||||
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
|
||||
<h6 class="alert-heading">{{status.filename}}</h6>
|
||||
@if (!isFinished(status) || (isFinished(status) && !status.documentId)) {
|
||||
<p class="mb-0 pb-1">{{status.message}}</p>
|
||||
}
|
||||
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
|
||||
<div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
@if (isFinished(status)) {
|
||||
<div>
|
||||
@if (status.documentId) {
|
||||
<button class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
||||
<small i18n>Open document</small>
|
||||
<i-bs name="arrow-right-short"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<ngb-alert type="secondary" class="mt-2 mb-0" [dismissible]="isFinished(status)" (closed)="dismiss(status)">
|
||||
<h6 class="alert-heading">{{status.filename}}</h6>
|
||||
@if (!isFinished(status) || (isFinished(status) && !status.documentId)) {
|
||||
<p class="mb-0 pb-1">{{status.message}}</p>
|
||||
}
|
||||
<ngb-progressbar [value]="status.getProgress()" [max]="1" [type]="getStatusColor(status)"></ngb-progressbar>
|
||||
<div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
@if (isFinished(status)) {
|
||||
<div>
|
||||
@if (status.documentId) {
|
||||
<button class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
|
||||
<small i18n>Open document</small>
|
||||
<i-bs name="arrow-right-short"></i-bs>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</ngb-alert>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ngb-alert>
|
||||
</ng-template>
|
||||
|
||||
@@ -35,3 +35,7 @@ form {
|
||||
::ng-deep .ngx-file-drop__drop-zone--over {
|
||||
background-color: var(--pngx-primary-faded) !important;
|
||||
}
|
||||
|
||||
.consumer-status-card {
|
||||
background-color: rgba(var(--bs-body-bg-rgb), .95) !important;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { UploadDocumentsService } from 'src/app/services/upload-documents.servic
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { UploadFileWidgetComponent } from './upload-file-widget.component'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const FAILED_STATUSES = [new FileStatus()]
|
||||
const WORKING_STATUSES = [new FileStatus(), new FileStatus()]
|
||||
@@ -73,6 +74,7 @@ describe('UploadFileWidgetComponent', () => {
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgbAlertModule,
|
||||
DragDropModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -147,12 +149,16 @@ describe('UploadFileWidgetComponent', () => {
|
||||
|
||||
it('should allow dismissing all alerts', fakeAsync(() => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
component.alertsExpanded = true
|
||||
fixture.detectChanges()
|
||||
jest
|
||||
.spyOn(component, 'getStatusCompleted')
|
||||
.mockImplementation(() => SUCCESS_STATUSES)
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
|
||||
component.dismissCompleted()
|
||||
tick(1000)
|
||||
fixture.detectChanges()
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(6)
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(10)
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
@@ -116,6 +116,11 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
|
||||
dismissCompleted() {
|
||||
this.alerts.forEach((a) => a.close())
|
||||
if (this.alertsExpanded) {
|
||||
this.getStatusCompleted().forEach((status) =>
|
||||
this.consumerStatusService.dismiss(status)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public onFileSelected(event: Event) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<pngx-page-header [(title)]="title">
|
||||
@if (getContentType() === 'application/pdf' && !useNativePdfViewer) {
|
||||
@if (contentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
|
||||
<div class="input-group input-group-sm me-2 d-none d-md-flex">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
||||
@@ -106,7 +106,7 @@
|
||||
@for (fieldInstance of document?.custom_fields; track fieldInstance; let i = $index) {
|
||||
<div [formGroup]="customFieldFormFields.controls[i]">
|
||||
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {
|
||||
@case (PaperlessCustomFieldDataType.String) {
|
||||
@case (CustomFieldDataType.String) {
|
||||
<pngx-input-text formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
@@ -114,7 +114,7 @@
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-text>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Date) {
|
||||
@case (CustomFieldDataType.Date) {
|
||||
<pngx-input-date formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
@@ -122,7 +122,7 @@
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-date>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Integer) {
|
||||
@case (CustomFieldDataType.Integer) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
@@ -131,7 +131,7 @@
|
||||
[showAdd]="false"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Float) {
|
||||
@case (CustomFieldDataType.Float) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
@@ -141,7 +141,7 @@
|
||||
[step]=".1"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Monetary) {
|
||||
@case (CustomFieldDataType.Monetary) {
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
@@ -151,14 +151,14 @@
|
||||
[step]=".01"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Boolean) {
|
||||
@case (CustomFieldDataType.Boolean) {
|
||||
<pngx-input-check formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"></pngx-input-check>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Url) {
|
||||
@case (CustomFieldDataType.Url) {
|
||||
<pngx-input-url formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
@@ -166,7 +166,7 @@
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-url>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.DocumentLink) {
|
||||
@case (CustomFieldDataType.DocumentLink) {
|
||||
<pngx-input-document-link formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[parentDocumentID]="documentId"
|
||||
@@ -260,7 +260,9 @@
|
||||
<a ngbNavLink i18n>Preview</a>
|
||||
@if (!pdfPreview.offsetParent) {
|
||||
<ng-template ngbNavContent>
|
||||
<ng-container *ngTemplateOutlet="previewContent"></ng-container>
|
||||
<div class="position-relative">
|
||||
<ng-container *ngTemplateOutlet="previewContent"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
</li>
|
||||
@@ -295,16 +297,6 @@
|
||||
|
||||
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
|
||||
<ng-container *ngTemplateOutlet="previewContent"></ng-container>
|
||||
@if (renderAsPlainText) {
|
||||
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
|
||||
}
|
||||
@if (requiresPassword) {
|
||||
<div class="password-prompt">
|
||||
<form>
|
||||
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -332,34 +324,45 @@
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (getContentType() === 'application/pdf') {
|
||||
@if (!useNativePdfViewer ) {
|
||||
<div class="preview-sticky pdf-viewer-container">
|
||||
<pngx-pdf-viewer
|
||||
[src]="{ url: previewUrl, password: password }"
|
||||
[original-size]="false"
|
||||
[show-borders]="true"
|
||||
[show-all]="true"
|
||||
[(page)]="previewCurrentPage"
|
||||
[zoom-scale]="previewZoomScale"
|
||||
[zoom]="previewZoomSetting"
|
||||
(error)="onError($event)"
|
||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
||||
</pngx-pdf-viewer>
|
||||
} @else {
|
||||
@switch (contentRenderType) {
|
||||
@case (ContentRenderType.PDF) {
|
||||
@if (!useNativePdfViewer) {
|
||||
<div class="preview-sticky pdf-viewer-container">
|
||||
<pngx-pdf-viewer
|
||||
[src]="{ url: previewUrl, password: password }"
|
||||
[original-size]="false"
|
||||
[show-borders]="true"
|
||||
[show-all]="true"
|
||||
[(page)]="previewCurrentPage"
|
||||
[zoom-scale]="previewZoomScale"
|
||||
[zoom]="previewZoomSetting"
|
||||
(error)="onError($event)"
|
||||
(after-load-complete)="pdfPreviewLoaded($event)">
|
||||
</pngx-pdf-viewer>
|
||||
</div>
|
||||
} @else {
|
||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||
}
|
||||
}
|
||||
@case (ContentRenderType.Text) {
|
||||
<div class="preview-sticky bg-light p-3 overflow-auto" width="100%">{{previewText}}</div>
|
||||
}
|
||||
@case (ContentRenderType.Image) {
|
||||
<div class="preview-sticky">
|
||||
<img [src]="previewUrl | safeUrl" width="100%" height="100%" alt="{{title}}" />
|
||||
</div>
|
||||
}
|
||||
@case (ContentRenderType.Other) {
|
||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||
}
|
||||
}
|
||||
@if (requiresPassword) {
|
||||
<div class="password-prompt">
|
||||
<form>
|
||||
<input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
|
||||
</form>
|
||||
</div>
|
||||
} @else {
|
||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||
}
|
||||
}
|
||||
@if (renderAsPlainText) {
|
||||
<div [innerText]="previewText" class="preview-sticky bg-light p-3 overflow-auto" width="100%"></div>
|
||||
}
|
||||
@if (showPasswordField) {
|
||||
<div class="password-prompt">
|
||||
<form>
|
||||
<input autocomplete="" autofocus="true" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
@@ -58,3 +58,9 @@ textarea.rtl {
|
||||
border-top-right-radius: var(--bs-border-radius-sm);
|
||||
border-bottom-right-radius: var(--bs-border-radius-sm);
|
||||
}
|
||||
|
||||
.preview-sticky img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
@@ -70,6 +73,8 @@ import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/
|
||||
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { PdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
const doc: Document = {
|
||||
id: 3,
|
||||
@@ -90,12 +95,12 @@ const doc: Document = {
|
||||
{
|
||||
created: new Date(),
|
||||
note: 'note 1',
|
||||
user: 1,
|
||||
user: { id: 1, username: 'user1' },
|
||||
},
|
||||
{
|
||||
created: new Date(),
|
||||
note: 'note 2',
|
||||
user: 2,
|
||||
user: { id: 2, username: 'user2' },
|
||||
},
|
||||
],
|
||||
custom_fields: [
|
||||
@@ -135,6 +140,7 @@ describe('DocumentDetailComponent', () => {
|
||||
let documentListViewService: DocumentListViewService
|
||||
let settingsService: SettingsService
|
||||
let customFieldsService: CustomFieldsService
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
let currentUserCan = true
|
||||
let currentUserHasObjectPermissions = true
|
||||
@@ -250,6 +256,7 @@ describe('DocumentDetailComponent', () => {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModalModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -264,6 +271,7 @@ describe('DocumentDetailComponent', () => {
|
||||
settingsService.currentUser = { id: 1 }
|
||||
customFieldsService = TestBed.inject(CustomFieldsService)
|
||||
fixture = TestBed.createComponent(DocumentDetailComponent)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
@@ -348,6 +356,26 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(component.documentForm.disabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not attempt to retrieve objects if user does not have permissions', () => {
|
||||
currentUserCan = false
|
||||
initNormally()
|
||||
expect(component.correspondents).toBeUndefined()
|
||||
expect(component.documentTypes).toBeUndefined()
|
||||
expect(component.storagePaths).toBeUndefined()
|
||||
expect(component.users).toBeUndefined()
|
||||
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/correspondents/`
|
||||
)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/document_types/`
|
||||
)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/storage_paths/`
|
||||
)
|
||||
currentUserCan = true
|
||||
})
|
||||
|
||||
it('should support creating document type', () => {
|
||||
initNormally()
|
||||
let openModal: NgbModalRef
|
||||
@@ -679,6 +707,7 @@ describe('DocumentDetailComponent', () => {
|
||||
|
||||
it('should support Enter key in password field', () => {
|
||||
initNormally()
|
||||
component.metadata = { has_archive_version: true }
|
||||
component.onError({ name: 'PasswordException' }) // normally dispatched by pdf viewer
|
||||
fixture.detectChanges()
|
||||
expect(component.password).toBeUndefined()
|
||||
@@ -994,6 +1023,53 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should change preview element by render type', () => {
|
||||
component.metadata = { has_archive_version: true }
|
||||
initNormally()
|
||||
fixture.detectChanges()
|
||||
expect(component.contentRenderType).toEqual(component.ContentRenderType.PDF)
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('pdf-viewer-container'))
|
||||
).not.toBeUndefined()
|
||||
|
||||
component.metadata = {
|
||||
has_archive_version: false,
|
||||
original_mime_type: 'text/plain',
|
||||
}
|
||||
fixture.detectChanges()
|
||||
expect(component.contentRenderType).toEqual(
|
||||
component.ContentRenderType.Text
|
||||
)
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('div.preview-sticky'))
|
||||
).not.toBeUndefined()
|
||||
|
||||
component.metadata = {
|
||||
has_archive_version: false,
|
||||
original_mime_type: 'image/jpg',
|
||||
}
|
||||
fixture.detectChanges()
|
||||
expect(component.contentRenderType).toEqual(
|
||||
component.ContentRenderType.Image
|
||||
)
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('.preview-sticky img'))
|
||||
).not.toBeUndefined()
|
||||
|
||||
component.metadata = {
|
||||
has_archive_version: false,
|
||||
original_mime_type:
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
}
|
||||
fixture.detectChanges()
|
||||
expect(component.contentRenderType).toEqual(
|
||||
component.ContentRenderType.Other
|
||||
)
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('object.preview-sticky'))
|
||||
).not.toBeUndefined()
|
||||
})
|
||||
|
||||
function initNormally() {
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
|
||||
@@ -77,6 +77,14 @@ enum DocumentDetailNavIDs {
|
||||
Permissions = 6,
|
||||
}
|
||||
|
||||
enum ContentRenderType {
|
||||
PDF = 'pdf',
|
||||
Image = 'image',
|
||||
Text = 'text',
|
||||
Other = 'other',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
enum ZoomSetting {
|
||||
PageFit = 'page-fit',
|
||||
PageWidth = 'page-width',
|
||||
@@ -154,7 +162,9 @@ export class DocumentDetailComponent
|
||||
ogDate: Date
|
||||
|
||||
customFields: CustomField[]
|
||||
public readonly PaperlessCustomFieldDataType = CustomFieldDataType
|
||||
public readonly CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
public readonly ContentRenderType = ContentRenderType
|
||||
|
||||
@ViewChild('nav') nav: NgbNav
|
||||
@ViewChild('pdfPreview') set pdfPreview(element) {
|
||||
@@ -201,16 +211,22 @@ export class DocumentDetailComponent
|
||||
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
|
||||
}
|
||||
|
||||
getContentType() {
|
||||
return this.metadata?.has_archive_version
|
||||
get contentRenderType(): ContentRenderType {
|
||||
if (!this.metadata) return ContentRenderType.Unknown
|
||||
const contentType = this.metadata?.has_archive_version
|
||||
? 'application/pdf'
|
||||
: this.metadata?.original_mime_type
|
||||
}
|
||||
|
||||
get renderAsPlainText(): boolean {
|
||||
return ['text/plain', 'application/csv', 'text/csv'].includes(
|
||||
this.getContentType()
|
||||
)
|
||||
if (contentType === 'application/pdf') {
|
||||
return ContentRenderType.PDF
|
||||
} else if (
|
||||
['text/plain', 'application/csv', 'text/csv'].includes(contentType)
|
||||
) {
|
||||
return ContentRenderType.Text
|
||||
} else if (contentType?.indexOf('image/') === 0) {
|
||||
return ContentRenderType.Image
|
||||
}
|
||||
return ContentRenderType.Other
|
||||
}
|
||||
|
||||
get isRTL() {
|
||||
@@ -234,25 +250,50 @@ export class DocumentDetailComponent
|
||||
Object.assign(this.document, docValues)
|
||||
})
|
||||
|
||||
this.correspondentService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
|
||||
this.userService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => (this.users = result.results))
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.Correspondent
|
||||
)
|
||||
) {
|
||||
this.correspondentService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
}
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.DocumentType
|
||||
)
|
||||
) {
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
}
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.StoragePath
|
||||
)
|
||||
) {
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
}
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
PermissionType.User
|
||||
)
|
||||
) {
|
||||
this.userService
|
||||
.listAll()
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => (this.users = result.results))
|
||||
}
|
||||
|
||||
this.getCustomFields()
|
||||
|
||||
@@ -446,7 +487,7 @@ export class DocumentDetailComponent
|
||||
this.metadata = result
|
||||
},
|
||||
error: (error) => {
|
||||
this.metadata = null
|
||||
this.metadata = {} // allow display to fallback to <object> tag
|
||||
this.toastService.showError(
|
||||
$localize`Error retrieving metadata`,
|
||||
error
|
||||
@@ -589,7 +630,9 @@ export class DocumentDetailComponent
|
||||
.update(this.document)
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: () => {
|
||||
next: (docValues) => {
|
||||
// in case data changed while saving eg removing inbox_tags
|
||||
this.documentForm.patchValue(docValues)
|
||||
this.store.next(this.documentForm.value)
|
||||
this.toastService.showInfo($localize`Document saved successfully.`)
|
||||
close && this.close()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { MetadataCollapseComponent } from './metadata-collapse.component'
|
||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
|
||||
const metadata = [
|
||||
{
|
||||
@@ -25,7 +26,7 @@ describe('MetadataCollapseComponent', () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [MetadataCollapseComponent],
|
||||
providers: [],
|
||||
imports: [NgbCollapseModule],
|
||||
imports: [NgbCollapseModule, NgxBootstrapIconsModule.pick(allIcons)],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(MetadataCollapseComponent)
|
||||
|
||||
@@ -17,51 +17,63 @@
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||
<label class="me-2" i18n>Edit:</label>
|
||||
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(opened)="openTagsDropdown()"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
[documentCounts]="tagDocumentCounts"
|
||||
(apply)="setTags($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(opened)="openCorrespondentDropdown()"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
[documentCounts]="correspondentDocumentCounts"
|
||||
(apply)="setCorrespondents($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(opened)="openDocumentTypeDropdown()"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
[documentCounts]="documentTypeDocumentCounts"
|
||||
(apply)="setDocumentTypes($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(opened)="openStoragePathDropdown()"
|
||||
[(selectionModel)]="storagePathsSelectionModel"
|
||||
[documentCounts]="storagePathDocumentCounts"
|
||||
(apply)="setStoragePaths($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
|
||||
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createTag.bind(this)"
|
||||
(opened)="openTagsDropdown()"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
[documentCounts]="tagDocumentCounts"
|
||||
(apply)="setTags($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createCorrespondent.bind(this)"
|
||||
(opened)="openCorrespondentDropdown()"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
[documentCounts]="correspondentDocumentCounts"
|
||||
(apply)="setCorrespondents($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createDocumentType.bind(this)"
|
||||
(opened)="openDocumentTypeDropdown()"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
[documentCounts]="documentTypeDocumentCounts"
|
||||
(apply)="setDocumentTypes($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[disabled]="!userCanEditAll"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createStoragePath.bind(this)"
|
||||
(opened)="openStoragePathDropdown()"
|
||||
[(selectionModel)]="storagePathsSelectionModel"
|
||||
[documentCounts]="storagePathDocumentCounts"
|
||||
(apply)="setStoragePaths($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<div class="btn-toolbar">
|
||||
|
||||
@@ -40,6 +40,18 @@ import { PermissionsGroupComponent } from '../../common/input/permissions/permis
|
||||
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { SwitchComponent } from '../../common/input/switch/switch.component'
|
||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
import { Results } from 'src/app/data/results'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
|
||||
const selectionData: SelectionData = {
|
||||
selected_tags: [
|
||||
@@ -63,6 +75,10 @@ describe('BulkEditorComponent', () => {
|
||||
let documentService: DocumentService
|
||||
let toastService: ToastService
|
||||
let modalService: NgbModal
|
||||
let tagService: TagService
|
||||
let correspondentsService: CorrespondentService
|
||||
let documentTypeService: DocumentTypeService
|
||||
let storagePathService: StoragePathService
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -80,6 +96,7 @@ describe('BulkEditorComponent', () => {
|
||||
SelectComponent,
|
||||
PermissionsGroupComponent,
|
||||
PermissionsUserComponent,
|
||||
SwitchComponent,
|
||||
],
|
||||
providers: [
|
||||
PermissionsService,
|
||||
@@ -153,6 +170,7 @@ describe('BulkEditorComponent', () => {
|
||||
NgbModule,
|
||||
NgbModalModule,
|
||||
NgSelectModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -161,6 +179,10 @@ describe('BulkEditorComponent', () => {
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
tagService = TestBed.inject(TagService)
|
||||
correspondentsService = TestBed.inject(CorrespondentService)
|
||||
documentTypeService = TestBed.inject(DocumentTypeService)
|
||||
storagePathService = TestBed.inject(StoragePathService)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture = TestBed.createComponent(BulkEditorComponent)
|
||||
@@ -849,7 +871,18 @@ describe('BulkEditorComponent', () => {
|
||||
fixture.detectChanges()
|
||||
component.setPermissions()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirmClicked.next()
|
||||
const perms = {
|
||||
permissions: {
|
||||
view_users: [],
|
||||
change_users: [],
|
||||
view_groups: [],
|
||||
change_groups: [],
|
||||
},
|
||||
}
|
||||
modal.componentInstance.confirmClicked.emit({
|
||||
permissions: perms,
|
||||
merge: true,
|
||||
})
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
@@ -857,7 +890,10 @@ describe('BulkEditorComponent', () => {
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'set_permissions',
|
||||
parameters: undefined,
|
||||
parameters: {
|
||||
permissions: perms.permissions,
|
||||
merge: true,
|
||||
},
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
@@ -866,4 +902,198 @@ describe('BulkEditorComponent', () => {
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should not attempt to retrieve objects if user does not have permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
expect(component.tags).toBeUndefined()
|
||||
expect(component.correspondents).toBeUndefined()
|
||||
expect(component.documentTypes).toBeUndefined()
|
||||
expect(component.storagePaths).toBeUndefined()
|
||||
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/correspondents/`
|
||||
)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/document_types/`
|
||||
)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/storage_paths/`
|
||||
)
|
||||
})
|
||||
|
||||
it('should support create new tag', () => {
|
||||
const name = 'New Tag'
|
||||
const newTag = { id: 101, name: 'New Tag' }
|
||||
const tags: Results<Tag> = {
|
||||
results: [
|
||||
{ id: 1, name: 'Tag 1' },
|
||||
{ id: 2, name: 'Tag 2' },
|
||||
],
|
||||
count: 2,
|
||||
all: [1, 2],
|
||||
}
|
||||
|
||||
const modalInstance = {
|
||||
componentInstance: {
|
||||
dialogMode: EditDialogMode.CREATE,
|
||||
object: { name },
|
||||
succeeded: of(newTag),
|
||||
},
|
||||
}
|
||||
const tagListAllSpy = jest.spyOn(tagService, 'listAll')
|
||||
tagListAllSpy.mockReturnValue(of(tags))
|
||||
|
||||
const tagSelectionModelToggleSpy = jest.spyOn(
|
||||
component.tagSelectionModel,
|
||||
'toggle'
|
||||
)
|
||||
|
||||
const modalServiceOpenSpy = jest.spyOn(modalService, 'open')
|
||||
modalServiceOpenSpy.mockReturnValue(modalInstance as any)
|
||||
|
||||
component.createTag(name)
|
||||
|
||||
expect(modalServiceOpenSpy).toHaveBeenCalledWith(TagEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
expect(tagListAllSpy).toHaveBeenCalled()
|
||||
|
||||
expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id)
|
||||
expect(component.tags).toEqual(tags.results)
|
||||
})
|
||||
|
||||
it('should support create new correspondent', () => {
|
||||
const name = 'New Correspondent'
|
||||
const newCorrespondent = { id: 101, name: 'New Correspondent' }
|
||||
const correspondents: Results<Correspondent> = {
|
||||
results: [
|
||||
{ id: 1, name: 'Correspondent 1' },
|
||||
{ id: 2, name: 'Correspondent 2' },
|
||||
],
|
||||
count: 2,
|
||||
all: [1, 2],
|
||||
}
|
||||
|
||||
const modalInstance = {
|
||||
componentInstance: {
|
||||
dialogMode: EditDialogMode.CREATE,
|
||||
object: { name },
|
||||
succeeded: of(newCorrespondent),
|
||||
},
|
||||
}
|
||||
const correspondentsListAllSpy = jest.spyOn(
|
||||
correspondentsService,
|
||||
'listAll'
|
||||
)
|
||||
correspondentsListAllSpy.mockReturnValue(of(correspondents))
|
||||
|
||||
const correspondentSelectionModelToggleSpy = jest.spyOn(
|
||||
component.correspondentSelectionModel,
|
||||
'toggle'
|
||||
)
|
||||
|
||||
const modalServiceOpenSpy = jest.spyOn(modalService, 'open')
|
||||
modalServiceOpenSpy.mockReturnValue(modalInstance as any)
|
||||
|
||||
component.createCorrespondent(name)
|
||||
|
||||
expect(modalServiceOpenSpy).toHaveBeenCalledWith(
|
||||
CorrespondentEditDialogComponent,
|
||||
{ backdrop: 'static' }
|
||||
)
|
||||
expect(correspondentsListAllSpy).toHaveBeenCalled()
|
||||
|
||||
expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newCorrespondent.id
|
||||
)
|
||||
expect(component.correspondents).toEqual(correspondents.results)
|
||||
})
|
||||
|
||||
it('should support create new document type', () => {
|
||||
const name = 'New Document Type'
|
||||
const newDocumentType = { id: 101, name: 'New Document Type' }
|
||||
const documentTypes: Results<DocumentType> = {
|
||||
results: [
|
||||
{ id: 1, name: 'Document Type 1' },
|
||||
{ id: 2, name: 'Document Type 2' },
|
||||
],
|
||||
count: 2,
|
||||
all: [1, 2],
|
||||
}
|
||||
|
||||
const modalInstance = {
|
||||
componentInstance: {
|
||||
dialogMode: EditDialogMode.CREATE,
|
||||
object: { name },
|
||||
succeeded: of(newDocumentType),
|
||||
},
|
||||
}
|
||||
const documentTypesListAllSpy = jest.spyOn(documentTypeService, 'listAll')
|
||||
documentTypesListAllSpy.mockReturnValue(of(documentTypes))
|
||||
|
||||
const documentTypeSelectionModelToggleSpy = jest.spyOn(
|
||||
component.documentTypeSelectionModel,
|
||||
'toggle'
|
||||
)
|
||||
|
||||
const modalServiceOpenSpy = jest.spyOn(modalService, 'open')
|
||||
modalServiceOpenSpy.mockReturnValue(modalInstance as any)
|
||||
|
||||
component.createDocumentType(name)
|
||||
|
||||
expect(modalServiceOpenSpy).toHaveBeenCalledWith(
|
||||
DocumentTypeEditDialogComponent,
|
||||
{ backdrop: 'static' }
|
||||
)
|
||||
expect(documentTypesListAllSpy).toHaveBeenCalled()
|
||||
|
||||
expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newDocumentType.id
|
||||
)
|
||||
expect(component.documentTypes).toEqual(documentTypes.results)
|
||||
})
|
||||
|
||||
it('should support create new storage path', () => {
|
||||
const name = 'New Storage Path'
|
||||
const newStoragePath = { id: 101, name: 'New Storage Path' }
|
||||
const storagePaths: Results<StoragePath> = {
|
||||
results: [
|
||||
{ id: 1, name: 'Storage Path 1' },
|
||||
{ id: 2, name: 'Storage Path 2' },
|
||||
],
|
||||
count: 2,
|
||||
all: [1, 2],
|
||||
}
|
||||
|
||||
const modalInstance = {
|
||||
componentInstance: {
|
||||
dialogMode: EditDialogMode.CREATE,
|
||||
object: { name },
|
||||
succeeded: of(newStoragePath),
|
||||
},
|
||||
}
|
||||
const storagePathsListAllSpy = jest.spyOn(storagePathService, 'listAll')
|
||||
storagePathsListAllSpy.mockReturnValue(of(storagePaths))
|
||||
|
||||
const storagePathsSelectionModelToggleSpy = jest.spyOn(
|
||||
component.storagePathsSelectionModel,
|
||||
'toggle'
|
||||
)
|
||||
|
||||
const modalServiceOpenSpy = jest.spyOn(modalService, 'open')
|
||||
modalServiceOpenSpy.mockReturnValue(modalInstance as any)
|
||||
|
||||
component.createStoragePath(name)
|
||||
|
||||
expect(modalServiceOpenSpy).toHaveBeenCalledWith(
|
||||
StoragePathEditDialogComponent,
|
||||
{ backdrop: 'static' }
|
||||
)
|
||||
expect(storagePathsListAllSpy).toHaveBeenCalled()
|
||||
|
||||
expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith(
|
||||
newStoragePath.id
|
||||
)
|
||||
expect(component.storagePaths).toEqual(storagePaths.results)
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user