mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-18 17:36:25 +00:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16acc2d6ad | ||
|
|
c2c9a953d3 | ||
|
|
530f4a8b28 | ||
|
|
8eb1dc4f62 | ||
|
|
a2b87fe012 | ||
|
|
3dcb973adb | ||
|
|
8e8810cbaa | ||
|
|
b0aeec4c43 | ||
|
|
1ac298f6ff | ||
|
|
6d5f4e92cc | ||
|
|
416ad13aaf | ||
|
|
a7e1299194 | ||
|
|
a12e1fae72 | ||
|
|
f525ac0af6 | ||
|
|
58bf9c552b | ||
|
|
22d257cd1f | ||
|
|
f1bf1ddc54 | ||
|
|
6015cc0e4a | ||
|
|
f9926d77d5 | ||
|
|
4f85dcecfc | ||
|
|
30c31a3d4c | ||
|
|
c64667d396 | ||
|
|
9f6613fe05 | ||
|
|
ea47af7034 | ||
|
|
d46abeff01 | ||
|
|
2b39697ffb | ||
|
|
4b00a72ff5 | ||
|
|
e590b2482e | ||
|
|
eb7dd80410 | ||
|
|
86338465fb | ||
|
|
a41dbdd12c | ||
|
|
1e10a438cd | ||
|
|
ab34ea724d | ||
|
|
fd8bfe1a80 | ||
|
|
9043f45350 | ||
|
|
5921e6d13e | ||
|
|
ee2bfe2350 | ||
|
|
0957a7ca8e | ||
|
|
f4e75c7fb7 | ||
|
|
fae0e3b405 | ||
|
|
ef335517ce | ||
|
|
e2be166e67 | ||
|
|
37e34d92de | ||
|
|
bd35030c59 | ||
|
|
a82e3771ae | ||
|
|
3115106dc1 | ||
|
|
d623af9c41 | ||
|
|
355a434a07 | ||
|
|
8da2535a65 | ||
|
|
5963dfe41b | ||
|
|
c6dcaa0472 | ||
|
|
21063a5c22 | ||
|
|
ba2f51bed1 | ||
|
|
bbf64b7e93 | ||
|
|
3b6ce16f1c | ||
|
|
46e6be319f | ||
|
|
e6d6f21d33 | ||
|
|
77b9b79a9e | ||
|
|
f0016ad70c | ||
|
|
054468ffc2 | ||
|
|
607c1282e3 | ||
|
|
3f4f4444f7 | ||
|
|
54372b5618 | ||
|
|
670a3f6c7f | ||
|
|
35a4d3fb54 | ||
|
|
fb81612ed1 | ||
|
|
c5d622279c | ||
|
|
b93f655039 | ||
|
|
428ffb4729 | ||
|
|
061f33fb05 | ||
|
|
da058b915b | ||
|
|
cf869b1356 | ||
|
|
05e294fc81 | ||
|
|
bd904d9e6b | ||
|
|
a7ac719711 | ||
|
|
370f6ddb3b | ||
|
|
3861e84f89 | ||
|
|
76001105b8 | ||
|
|
d47cc9460b | ||
|
|
fbeb03c377 | ||
|
|
2b13fa4712 | ||
|
|
98255f8e14 |
3
.codespellrc
Normal file
3
.codespellrc
Normal file
@@ -0,0 +1,3 @@
|
||||
[codespell]
|
||||
write-changes = True
|
||||
ignore-words-list = criterias,afterall,valeu,ureue,equest,ure
|
||||
50
.github/workflows/ci.yml
vendored
50
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Install python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
-
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
-
|
||||
name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run mkdocs gh-deploy --force --no-history
|
||||
-
|
||||
name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: documentation
|
||||
path: site/
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
-
|
||||
name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
cache: "pipenv"
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
-
|
||||
name: Upload coverage
|
||||
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backend-coverage-report
|
||||
path: src/coverage.xml
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
|
||||
|
||||
install-frontend-depedendencies:
|
||||
name: "Install Frontend Dependendencies"
|
||||
name: "Install Frontend Dependencies"
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- pre-commit
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- name: Cache frontend depdendencies
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 'src-ui/package-lock.json'
|
||||
- name: Cache frontend depdendencies
|
||||
- name: Cache frontend dependencies
|
||||
id: cache-frontend-deps
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -238,7 +238,7 @@ jobs:
|
||||
-
|
||||
name: Upload Jest coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jest-coverage-report-${{ matrix.shard-index }}
|
||||
path: |
|
||||
@@ -253,9 +253,9 @@ jobs:
|
||||
-
|
||||
name: Upload Playwright test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
name: playwright-report-${{ matrix.shard-index }}
|
||||
path: src-ui/playwright-report
|
||||
retention-days: 7
|
||||
|
||||
@@ -269,10 +269,18 @@ jobs:
|
||||
-
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Download frontend coverage
|
||||
uses: actions/download-artifact@v3
|
||||
name: Download frontend jest coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: src-ui/coverage/
|
||||
pattern: jest-coverage-report-*
|
||||
-
|
||||
name: Download frontend playwright coverage
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: src-ui/coverage/
|
||||
pattern: playwright-report-*
|
||||
merge-multiple: true
|
||||
-
|
||||
name: Upload frontend coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
@@ -285,7 +293,7 @@ jobs:
|
||||
files: '!coverage.xml'
|
||||
-
|
||||
name: Download backend coverage
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: backend-coverage-report
|
||||
path: src/
|
||||
@@ -416,7 +424,7 @@ jobs:
|
||||
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
|
||||
-
|
||||
name: Upload frontend artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-compiled
|
||||
path: src/documents/static/frontend/
|
||||
@@ -435,7 +443,7 @@ jobs:
|
||||
-
|
||||
name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
@@ -461,13 +469,13 @@ jobs:
|
||||
sudo apt-get install -qq --no-install-recommends gettext liblept5
|
||||
-
|
||||
name: Download frontend artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-compiled
|
||||
path: src/documents/static/frontend/
|
||||
-
|
||||
name: Download documentation artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: documentation
|
||||
path: docs/_build/html/
|
||||
@@ -533,7 +541,7 @@ jobs:
|
||||
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
||||
-
|
||||
name: Upload release artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release
|
||||
path: dist/paperless-ngx.tar.xz
|
||||
@@ -552,7 +560,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Download release artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release
|
||||
path: ./
|
||||
@@ -603,7 +611,7 @@ jobs:
|
||||
ref: main
|
||||
-
|
||||
name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||
cache: "pipenv"
|
||||
|
||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -51,4 +51,4 @@ jobs:
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
1
.github/workflows/crowdin.yml
vendored
1
.github/workflows/crowdin.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
push:
|
||||
paths: [
|
||||
'src/locale/**',
|
||||
'src-ui/messages.xlf',
|
||||
'src-ui/src/locale/**'
|
||||
]
|
||||
branches: [ dev ]
|
||||
|
||||
4
.github/workflows/repo-maintenance.yml
vendored
4
.github/workflows/repo-maintenance.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
name: 'Stale'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
days-before-stale: 7
|
||||
days-before-close: 14
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`)
|
||||
|
||||
for (const discussion of result.repository.discussions.nodes) {
|
||||
console.log(`Closing dicussion #${discussion.number} (${discussion.id})`)
|
||||
console.log(`Closing discussion #${discussion.number} (${discussion.id})`)
|
||||
|
||||
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
|
||||
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
|
||||
|
||||
@@ -11,6 +11,8 @@ repos:
|
||||
- id: check-json
|
||||
exclude: "tsconfig.*json"
|
||||
- id: check-yaml
|
||||
args:
|
||||
- "--unsafe"
|
||||
- id: check-toml
|
||||
- id: check-executables-have-shebangs
|
||||
- id: end-of-file-fixer
|
||||
@@ -26,6 +28,14 @@ repos:
|
||||
- svg
|
||||
- id: check-case-conflict
|
||||
- id: detect-private-key
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
hooks:
|
||||
- id: codespell
|
||||
exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
|
||||
exclude_types:
|
||||
- pofile
|
||||
- json
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: 'v3.1.0'
|
||||
hooks:
|
||||
@@ -37,11 +47,11 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.1.5'
|
||||
rev: 'v0.1.11'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.11.0
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
# Dockerfile hooks
|
||||
|
||||
@@ -189,7 +189,7 @@ RUN set -eux \
|
||||
&& chmod 755 /usr/local/bin/paperless_cmd.sh \
|
||||
&& mv flower-conditional.sh /usr/local/bin/flower-conditional.sh \
|
||||
&& chmod 755 /usr/local/bin/flower-conditional.sh \
|
||||
&& echo "Installing managment commands" \
|
||||
&& echo "Installing management commands" \
|
||||
&& chmod +x install_management_commands.sh \
|
||||
&& ./install_management_commands.sh
|
||||
|
||||
@@ -270,3 +270,5 @@ ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["/usr/local/bin/paperless_cmd.sh"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=5 CMD [ "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000" ]
|
||||
|
||||
4
Pipfile
4
Pipfile
@@ -7,7 +7,7 @@ name = "pypi"
|
||||
dateparser = "~=1.2"
|
||||
# WARNING: django does not use semver.
|
||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||
django = "~=4.2.8"
|
||||
django = "~=4.2.9"
|
||||
django-auditlog = "*"
|
||||
django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
@@ -57,7 +57,7 @@ zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"}
|
||||
|
||||
[dev-packages]
|
||||
# Linting
|
||||
black = "==23.11.0"
|
||||
black = "*"
|
||||
pre-commit = "*"
|
||||
ruff = "*"
|
||||
# Testing
|
||||
|
||||
1591
Pipfile.lock
generated
1591
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# Docker Compose file for running paperless testing with actual gotenberg
|
||||
# and Tika containers for a more end to end test of the Tika related functionality
|
||||
# Can be used locally or by the CI to start the nessecary containers with the
|
||||
# Can be used locally or by the CI to start the necessary containers with the
|
||||
# correct networking for the tests
|
||||
|
||||
version: "3.7"
|
||||
|
||||
@@ -60,11 +60,6 @@ services:
|
||||
- tika
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
|
||||
@@ -54,11 +54,6 @@ services:
|
||||
- broker
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -73,7 +68,6 @@ services:
|
||||
PAPERLESS_DBPASS: paperless # only needed if non-default password
|
||||
PAPERLESS_DBPORT: 3306
|
||||
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
||||
@@ -54,11 +54,6 @@ services:
|
||||
- broker
|
||||
ports:
|
||||
- "8010:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
|
||||
@@ -58,11 +58,6 @@ services:
|
||||
- tika
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
|
||||
@@ -52,11 +52,6 @@ services:
|
||||
- broker
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -67,7 +62,6 @@ services:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_DBHOST: db
|
||||
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
||||
@@ -47,11 +47,6 @@ services:
|
||||
- tika
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
|
||||
@@ -38,11 +38,6 @@ services:
|
||||
- broker
|
||||
ports:
|
||||
- "8000:8000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- data:/usr/src/paperless/data
|
||||
- media:/usr/src/paperless/media
|
||||
@@ -52,7 +47,6 @@ services:
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
|
||||
|
||||
volumes:
|
||||
data:
|
||||
media:
|
||||
|
||||
@@ -86,13 +86,13 @@ initialize() {
|
||||
"${CONSUME_DIR}"; do
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
echo "Creating directory ${dir}"
|
||||
mkdir "${dir}"
|
||||
mkdir --parents "${dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
local -r tmp_dir="/tmp/paperless"
|
||||
echo "Creating directory ${tmp_dir}"
|
||||
mkdir -p "${tmp_dir}"
|
||||
mkdir --parents "${tmp_dir}"
|
||||
|
||||
set +e
|
||||
echo "Adjusting permissions of paperless files. This may take a while."
|
||||
|
||||
@@ -136,6 +136,11 @@ script can access the following relevant environment variables set:
|
||||
be triggered, leading to failures as two tasks work on the
|
||||
same document path
|
||||
|
||||
!!! warning
|
||||
|
||||
If your script modifies `DOCUMENT_WORKING_PATH` in a non-deterministic
|
||||
way, this may allow duplicate documents to be stored
|
||||
|
||||
A simple but common example for this would be creating a simple script
|
||||
like this:
|
||||
|
||||
@@ -608,7 +613,7 @@ scan a completely new "odd numbered pages" one. The old staging file will get di
|
||||
|
||||
The collation feature can be used together with the [subdirs as tags](configuration.md#consume_config)
|
||||
feature (but this is not a requirement). Just create a correctly named double-sided subdir
|
||||
in the hierachy and upload your scans there. For example, both `double-sided/foo/bar` as
|
||||
in the hierarchy and upload your scans there. For example, both `double-sided/foo/bar` as
|
||||
well as `foo/bar/double-sided` will cause the collated document to be treated as if it
|
||||
were uploaded into `foo/bar` and receive both `foo` and `bar` tags, but not `double-sided`.
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ most of the available filters and ordering fields.
|
||||
|
||||
The API provides the following main endpoints:
|
||||
|
||||
- `/api/consumption_templates/`: Full CRUD support.
|
||||
- `/api/correspondents/`: Full CRUD support.
|
||||
- `/api/custom_fields/`: Full CRUD support.
|
||||
- `/api/documents/`: Full CRUD support, except POSTing new documents.
|
||||
@@ -24,6 +23,7 @@ The API provides the following main endpoints:
|
||||
- `/api/tags/`: Full CRUD support.
|
||||
- `/api/tasks/`: Read-only.
|
||||
- `/api/users/`: Full CRUD support.
|
||||
- `/api/workflows/`: Full CRUD support.
|
||||
|
||||
All of these endpoints except for the logging endpoint allow you to
|
||||
fetch (and edit and delete where appropriate) individual objects by
|
||||
@@ -274,6 +274,7 @@ The endpoint supports the following optional form fields:
|
||||
- `correspondent`: Specify the ID of a correspondent that the consumer
|
||||
should use for the document.
|
||||
- `document_type`: Similar to correspondent.
|
||||
- `storage_path`: Similar to correspondent.
|
||||
- `tags`: Similar to correspondent. Specify this multiple times to
|
||||
have multiple tags added to the document.
|
||||
- `archive_serial_number`: An optional archive serial number to set.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB |
BIN
docs/assets/screenshots/workflow.png
Normal file
BIN
docs/assets/screenshots/workflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
@@ -1,5 +1,212 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 2.3.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: triggered workflow assignment of customfield fails if field exists in v2.3.1 [@shamoon](https://github.com/shamoon) ([#5302](https://github.com/paperless-ngx/paperless-ngx/pull/5302))
|
||||
- Fix: Decoding of user arguments for OCR [@stumpylog](https://github.com/stumpylog) ([#5307](https://github.com/paperless-ngx/paperless-ngx/pull/5307))
|
||||
- Fix: empty workflow trigger match field cannot be saved in v.2.3.1 [@shamoon](https://github.com/shamoon) ([#5301](https://github.com/paperless-ngx/paperless-ngx/pull/5301))
|
||||
- Fix: Use local time for added/updated workflow triggers [@stumpylog](https://github.com/stumpylog) ([#5304](https://github.com/paperless-ngx/paperless-ngx/pull/5304))
|
||||
- Fix: workflow edit form loses unsaved changes [@shamoon](https://github.com/shamoon) ([#5299](https://github.com/paperless-ngx/paperless-ngx/pull/5299))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Fix: triggered workflow assignment of customfield fails if field exists in v2.3.1 [@shamoon](https://github.com/shamoon) ([#5302](https://github.com/paperless-ngx/paperless-ngx/pull/5302))
|
||||
- Fix: Decoding of user arguments for OCR [@stumpylog](https://github.com/stumpylog) ([#5307](https://github.com/paperless-ngx/paperless-ngx/pull/5307))
|
||||
- Fix: empty workflow trigger match field cannot be saved in v.2.3.1 [@shamoon](https://github.com/shamoon) ([#5301](https://github.com/paperless-ngx/paperless-ngx/pull/5301))
|
||||
- Fix: Use local time for added/updated workflow triggers [@stumpylog](https://github.com/stumpylog) ([#5304](https://github.com/paperless-ngx/paperless-ngx/pull/5304))
|
||||
- Fix: workflow edit form loses unsaved changes [@shamoon](https://github.com/shamoon) ([#5299](https://github.com/paperless-ngx/paperless-ngx/pull/5299))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.3.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: edit workflow form not displaying trigger settings [@shamoon](https://github.com/shamoon) ([#5276](https://github.com/paperless-ngx/paperless-ngx/pull/5276))
|
||||
- Fix: Prevent passing 0 pages to OCRMyPDF [@stumpylog](https://github.com/stumpylog) ([#5275](https://github.com/paperless-ngx/paperless-ngx/pull/5275))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>2 changes</summary>
|
||||
|
||||
- Fix: edit workflow form not displaying trigger settings [@shamoon](https://github.com/shamoon) ([#5276](https://github.com/paperless-ngx/paperless-ngx/pull/5276))
|
||||
- Fix: Prevent passing 0 pages to OCRMyPDF [@stumpylog](https://github.com/stumpylog) ([#5275](https://github.com/paperless-ngx/paperless-ngx/pull/5275))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.3.0
|
||||
|
||||
### Notable Changes
|
||||
|
||||
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
|
||||
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
|
||||
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
|
||||
- Enhancement: fetch mails in bulk [@falkenbt](https://github.com/falkenbt) ([#5249](https://github.com/paperless-ngx/paperless-ngx/pull/5249))
|
||||
- Enhancement: add parameter to post_document API [@bevanjkay](https://github.com/bevanjkay) ([#5217](https://github.com/paperless-ngx/paperless-ngx/pull/5217))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Chore: Replaces deprecated Django alias with standard library [@stumpylog](https://github.com/stumpylog) ([#5262](https://github.com/paperless-ngx/paperless-ngx/pull/5262))
|
||||
- Fix: Crash in barcode ASN reading when the file type isn't supported [@stumpylog](https://github.com/stumpylog) ([#5261](https://github.com/paperless-ngx/paperless-ngx/pull/5261))
|
||||
- Fix: Allows pre-consume scripts to modify the working path again [@stumpylog](https://github.com/stumpylog) ([#5260](https://github.com/paperless-ngx/paperless-ngx/pull/5260))
|
||||
- Change: Use fnmatch for more sane workflow path matching [@shamoon](https://github.com/shamoon) ([#5250](https://github.com/paperless-ngx/paperless-ngx/pull/5250))
|
||||
- Fix: zip exports not respecting the --delete option [@stumpylog](https://github.com/stumpylog) ([#5245](https://github.com/paperless-ngx/paperless-ngx/pull/5245))
|
||||
- Fix: correctly format tip admonition [@ChrisRBe](https://github.com/ChrisRBe) ([#5229](https://github.com/paperless-ngx/paperless-ngx/pull/5229))
|
||||
- Fix: filename format remove none when part of directory [@shamoon](https://github.com/shamoon) ([#5210](https://github.com/paperless-ngx/paperless-ngx/pull/5210))
|
||||
- Fix: Improve Performance for Listing and Paginating Documents [@antoinelibert](https://github.com/antoinelibert) ([#5195](https://github.com/paperless-ngx/paperless-ngx/pull/5195))
|
||||
- Fix: Disable custom field remove button if user does not have permissions [@shamoon](https://github.com/shamoon) ([#5194](https://github.com/paperless-ngx/paperless-ngx/pull/5194))
|
||||
- Fix: overlapping button focus highlight on login [@shamoon](https://github.com/shamoon) ([#5193](https://github.com/paperless-ngx/paperless-ngx/pull/5193))
|
||||
- Fix: symmetric doc links with target doc value None [@shamoon](https://github.com/shamoon) ([#5187](https://github.com/paperless-ngx/paperless-ngx/pull/5187))
|
||||
- Fix: setting empty doc link with docs to be removed [@shamoon](https://github.com/shamoon) ([#5174](https://github.com/paperless-ngx/paperless-ngx/pull/5174))
|
||||
- Enhancement: improve validation of custom field values [@shamoon](https://github.com/shamoon) ([#5166](https://github.com/paperless-ngx/paperless-ngx/pull/5166))
|
||||
- Fix: type casting of db values for 'shared by me' filter [@shamoon](https://github.com/shamoon) ([#5155](https://github.com/paperless-ngx/paperless-ngx/pull/5155))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix: correctly format tip admonition [@ChrisRBe](https://github.com/ChrisRBe) ([#5229](https://github.com/paperless-ngx/paperless-ngx/pull/5229))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore(deps): Bump the actions group with 5 updates [@dependabot](https://github.com/dependabot) ([#5203](https://github.com/paperless-ngx/paperless-ngx/pull/5203))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>4 changes</summary>
|
||||
|
||||
- Chore(deps): Bump the actions group with 5 updates [@dependabot](https://github.com/dependabot) ([#5203](https://github.com/paperless-ngx/paperless-ngx/pull/5203))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#5204](https://github.com/paperless-ngx/paperless-ngx/pull/5204))
|
||||
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot) ([#5207](https://github.com/paperless-ngx/paperless-ngx/pull/5207))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5205](https://github.com/paperless-ngx/paperless-ngx/pull/5205))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>21 changes</summary>
|
||||
|
||||
- Chore: Replaces deprecated Django alias with standard library [@stumpylog](https://github.com/stumpylog) ([#5262](https://github.com/paperless-ngx/paperless-ngx/pull/5262))
|
||||
- Fix: Crash in barcode ASN reading when the file type isn't supported [@stumpylog](https://github.com/stumpylog) ([#5261](https://github.com/paperless-ngx/paperless-ngx/pull/5261))
|
||||
- Fix: Allows pre-consume scripts to modify the working path again [@stumpylog](https://github.com/stumpylog) ([#5260](https://github.com/paperless-ngx/paperless-ngx/pull/5260))
|
||||
- Enhancement: add basic filters for listing of custom fields [@shamoon](https://github.com/shamoon) ([#5257](https://github.com/paperless-ngx/paperless-ngx/pull/5257))
|
||||
- Change: Use fnmatch for more sane workflow path matching [@shamoon](https://github.com/shamoon) ([#5250](https://github.com/paperless-ngx/paperless-ngx/pull/5250))
|
||||
- Enhancement: fetch mails in bulk [@falkenbt](https://github.com/falkenbt) ([#5249](https://github.com/paperless-ngx/paperless-ngx/pull/5249))
|
||||
- Fix: zip exports not respecting the --delete option [@stumpylog](https://github.com/stumpylog) ([#5245](https://github.com/paperless-ngx/paperless-ngx/pull/5245))
|
||||
- Enhancement: add parameter to post_document API [@bevanjkay](https://github.com/bevanjkay) ([#5217](https://github.com/paperless-ngx/paperless-ngx/pull/5217))
|
||||
- Feature: Workflows [@shamoon](https://github.com/shamoon) ([#5121](https://github.com/paperless-ngx/paperless-ngx/pull/5121))
|
||||
- Fix: filename format remove none when part of directory [@shamoon](https://github.com/shamoon) ([#5210](https://github.com/paperless-ngx/paperless-ngx/pull/5210))
|
||||
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 10 updates [@dependabot](https://github.com/dependabot) ([#5204](https://github.com/paperless-ngx/paperless-ngx/pull/5204))
|
||||
- Chore(deps-dev): Bump [@<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.10.4 to 20.10.6 in /src-ui @dependabot) ([#5207](https://github.com/paperless-ngx/paperless-ngx/pull/5207))
|
||||
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5205](https://github.com/paperless-ngx/paperless-ngx/pull/5205))
|
||||
- Fix: Improve Performance for Listing and Paginating Documents [@antoinelibert](https://github.com/antoinelibert) ([#5195](https://github.com/paperless-ngx/paperless-ngx/pull/5195))
|
||||
- Fix: Disable custom field remove button if user does not have permissions [@shamoon](https://github.com/shamoon) ([#5194](https://github.com/paperless-ngx/paperless-ngx/pull/5194))
|
||||
- Fix: overlapping button focus highlight on login [@shamoon](https://github.com/shamoon) ([#5193](https://github.com/paperless-ngx/paperless-ngx/pull/5193))
|
||||
- Fix: symmetric doc links with target doc value None [@shamoon](https://github.com/shamoon) ([#5187](https://github.com/paperless-ngx/paperless-ngx/pull/5187))
|
||||
- Fix: setting empty doc link with docs to be removed [@shamoon](https://github.com/shamoon) ([#5174](https://github.com/paperless-ngx/paperless-ngx/pull/5174))
|
||||
- Feature: Allow setting backend configuration settings via the UI [@stumpylog](https://github.com/stumpylog) ([#5126](https://github.com/paperless-ngx/paperless-ngx/pull/5126))
|
||||
- Enhancement: improve validation of custom field values [@shamoon](https://github.com/shamoon) ([#5166](https://github.com/paperless-ngx/paperless-ngx/pull/5166))
|
||||
- Fix: type casting of db values for 'shared by me' filter [@shamoon](https://github.com/shamoon) ([#5155](https://github.com/paperless-ngx/paperless-ngx/pull/5155))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.2.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: saving doc links with no value [@shamoon](https://github.com/shamoon) ([#5144](https://github.com/paperless-ngx/paperless-ngx/pull/5144))
|
||||
- Fix: allow multiple consumption templates to assign the same custom field [@shamoon](https://github.com/shamoon) ([#5142](https://github.com/paperless-ngx/paperless-ngx/pull/5142))
|
||||
- Fix: some dropdowns broken in 2.2.0 [@shamoon](https://github.com/shamoon) ([#5134](https://github.com/paperless-ngx/paperless-ngx/pull/5134))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>3 changes</summary>
|
||||
|
||||
- Fix: saving doc links with no value [@shamoon](https://github.com/shamoon) ([#5144](https://github.com/paperless-ngx/paperless-ngx/pull/5144))
|
||||
- Fix: allow multiple consumption templates to assign the same custom field [@shamoon](https://github.com/shamoon) ([#5142](https://github.com/paperless-ngx/paperless-ngx/pull/5142))
|
||||
- Fix: some dropdowns broken in 2.2.0 [@shamoon](https://github.com/shamoon) ([#5134](https://github.com/paperless-ngx/paperless-ngx/pull/5134))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.2.0
|
||||
|
||||
### Features
|
||||
|
||||
- Enhancement: Add tooltip for select dropdown items [@shamoon](https://github.com/shamoon) ([#5070](https://github.com/paperless-ngx/paperless-ngx/pull/5070))
|
||||
- Chore: Update Angular to v17 including new Angular control-flow [@shamoon](https://github.com/shamoon) ([#4980](https://github.com/paperless-ngx/paperless-ngx/pull/4980))
|
||||
- Enhancement: symmetric document links [@shamoon](https://github.com/shamoon) ([#4907](https://github.com/paperless-ngx/paperless-ngx/pull/4907))
|
||||
- Enhancement: shared icon \& shared by me filter [@shamoon](https://github.com/shamoon) ([#4859](https://github.com/paperless-ngx/paperless-ngx/pull/4859))
|
||||
- Enhancement: Improved popup preview, respect embedded viewer, error handling [@shamoon](https://github.com/shamoon) ([#4947](https://github.com/paperless-ngx/paperless-ngx/pull/4947))
|
||||
- Enhancement: Allow deletion of documents via the fuzzy matching command [@stumpylog](https://github.com/stumpylog) ([#4957](https://github.com/paperless-ngx/paperless-ngx/pull/4957))
|
||||
- Enhancement: document link field fixes [@shamoon](https://github.com/shamoon) ([#5020](https://github.com/paperless-ngx/paperless-ngx/pull/5020))
|
||||
- Enhancement: above and below doc detail save buttons [@shamoon](https://github.com/shamoon) ([#5008](https://github.com/paperless-ngx/paperless-ngx/pull/5008))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Case where a mail attachment has no filename to use [@stumpylog](https://github.com/stumpylog) ([#5117](https://github.com/paperless-ngx/paperless-ngx/pull/5117))
|
||||
- Fix: Disable auto-login for API token requests [@shamoon](https://github.com/shamoon) ([#5094](https://github.com/paperless-ngx/paperless-ngx/pull/5094))
|
||||
- Fix: update ASN regex to support Unicode [@eukub](https://github.com/eukub) ([#5099](https://github.com/paperless-ngx/paperless-ngx/pull/5099))
|
||||
- Fix: ensure CSRF-Token on Index view [@baflo](https://github.com/baflo) ([#5082](https://github.com/paperless-ngx/paperless-ngx/pull/5082))
|
||||
- Fix: Stop auto-refresh logs / tasks after close [@shamoon](https://github.com/shamoon) ([#5089](https://github.com/paperless-ngx/paperless-ngx/pull/5089))
|
||||
- Fix: Make the admin panel accessible when using a large number of documents [@bogdal](https://github.com/bogdal) ([#5052](https://github.com/paperless-ngx/paperless-ngx/pull/5052))
|
||||
- Fix: dont allow null property via API [@shamoon](https://github.com/shamoon) ([#5063](https://github.com/paperless-ngx/paperless-ngx/pull/5063))
|
||||
- Fix: Updates Ghostscript to 10.02.1 for more bug fixes to it [@stumpylog](https://github.com/stumpylog) ([#5040](https://github.com/paperless-ngx/paperless-ngx/pull/5040))
|
||||
- Fix: allow system keyboard shortcuts in date fields [@shamoon](https://github.com/shamoon) ([#5009](https://github.com/paperless-ngx/paperless-ngx/pull/5009))
|
||||
- Fix password change detection on profile edit [@shamoon](https://github.com/shamoon) ([#5028](https://github.com/paperless-ngx/paperless-ngx/pull/5028))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Documentation: organize API endpoints [@dgsponer](https://github.com/dgsponer) ([#5077](https://github.com/paperless-ngx/paperless-ngx/pull/5077))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Bulk backend update [@stumpylog](https://github.com/stumpylog) ([#5061](https://github.com/paperless-ngx/paperless-ngx/pull/5061))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>5 changes</summary>
|
||||
|
||||
- Chore: Bulk backend update [@stumpylog](https://github.com/stumpylog) ([#5061](https://github.com/paperless-ngx/paperless-ngx/pull/5061))
|
||||
- Chore(deps): Bump the django group with 3 updates [@dependabot](https://github.com/dependabot) ([#5046](https://github.com/paperless-ngx/paperless-ngx/pull/5046))
|
||||
- Chore(deps): Bump the major-versions group with 1 update [@dependabot](https://github.com/dependabot) ([#5047](https://github.com/paperless-ngx/paperless-ngx/pull/5047))
|
||||
- Chore(deps): Bump the small-changes group with 6 updates [@dependabot](https://github.com/dependabot) ([#5048](https://github.com/paperless-ngx/paperless-ngx/pull/5048))
|
||||
- Fix: Updates Ghostscript to 10.02.1 for more bug fixes to it [@stumpylog](https://github.com/stumpylog) ([#5040](https://github.com/paperless-ngx/paperless-ngx/pull/5040))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>20 changes</summary>
|
||||
|
||||
- Fix: Case where a mail attachment has no filename to use [@stumpylog](https://github.com/stumpylog) ([#5117](https://github.com/paperless-ngx/paperless-ngx/pull/5117))
|
||||
- Fix: Disable auto-login for API token requests [@shamoon](https://github.com/shamoon) ([#5094](https://github.com/paperless-ngx/paperless-ngx/pull/5094))
|
||||
- Fix: update ASN regex to support Unicode [@eukub](https://github.com/eukub) ([#5099](https://github.com/paperless-ngx/paperless-ngx/pull/5099))
|
||||
- Fix: ensure CSRF-Token on Index view [@baflo](https://github.com/baflo) ([#5082](https://github.com/paperless-ngx/paperless-ngx/pull/5082))
|
||||
- Fix: Stop auto-refresh logs / tasks after close [@shamoon](https://github.com/shamoon) ([#5089](https://github.com/paperless-ngx/paperless-ngx/pull/5089))
|
||||
- Enhancement: Add tooltip for select dropdown items [@shamoon](https://github.com/shamoon) ([#5070](https://github.com/paperless-ngx/paperless-ngx/pull/5070))
|
||||
- Fix: Make the admin panel accessible when using a large number of documents [@bogdal](https://github.com/bogdal) ([#5052](https://github.com/paperless-ngx/paperless-ngx/pull/5052))
|
||||
- Chore: Update Angular to v17 including new Angular control-flow [@shamoon](https://github.com/shamoon) ([#4980](https://github.com/paperless-ngx/paperless-ngx/pull/4980))
|
||||
- Fix: dont allow null property via API [@shamoon](https://github.com/shamoon) ([#5063](https://github.com/paperless-ngx/paperless-ngx/pull/5063))
|
||||
- Enhancement: symmetric document links [@shamoon](https://github.com/shamoon) ([#4907](https://github.com/paperless-ngx/paperless-ngx/pull/4907))
|
||||
- Enhancement: shared icon \& shared by me filter [@shamoon](https://github.com/shamoon) ([#4859](https://github.com/paperless-ngx/paperless-ngx/pull/4859))
|
||||
- Chore(deps): Bump the django group with 3 updates [@dependabot](https://github.com/dependabot) ([#5046](https://github.com/paperless-ngx/paperless-ngx/pull/5046))
|
||||
- Chore(deps): Bump the major-versions group with 1 update [@dependabot](https://github.com/dependabot) ([#5047](https://github.com/paperless-ngx/paperless-ngx/pull/5047))
|
||||
- Chore(deps): Bump the small-changes group with 6 updates [@dependabot](https://github.com/dependabot) ([#5048](https://github.com/paperless-ngx/paperless-ngx/pull/5048))
|
||||
- Enhancement: Improved popup preview, respect embedded viewer, error handling [@shamoon](https://github.com/shamoon) ([#4947](https://github.com/paperless-ngx/paperless-ngx/pull/4947))
|
||||
- Enhancement: Add {original_filename}, {added_time} to title placeholders [@TTT7275](https://github.com/TTT7275) ([#4972](https://github.com/paperless-ngx/paperless-ngx/pull/4972))
|
||||
- Feature: Allow deletion of documents via the fuzzy matching command [@stumpylog](https://github.com/stumpylog) ([#4957](https://github.com/paperless-ngx/paperless-ngx/pull/4957))
|
||||
- Fix: allow system keyboard shortcuts in date fields [@shamoon](https://github.com/shamoon) ([#5009](https://github.com/paperless-ngx/paperless-ngx/pull/5009))
|
||||
- Enhancement: document link field fixes [@shamoon](https://github.com/shamoon) ([#5020](https://github.com/paperless-ngx/paperless-ngx/pull/5020))
|
||||
- Fix password change detection on profile edit [@shamoon](https://github.com/shamoon) ([#5028](https://github.com/paperless-ngx/paperless-ngx/pull/5028))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 2.1.3
|
||||
|
||||
### Bug Fixes
|
||||
@@ -220,7 +427,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
||||
- Enhancement: support default permissions for object creation via frontend [@shamoon](https://github.com/shamoon) ([#4233](https://github.com/paperless-ngx/paperless-ngx/pull/4233))
|
||||
- Fix: Set permissions before declaring volumes for rootless [@stumpylog](https://github.com/stumpylog) ([#4225](https://github.com/paperless-ngx/paperless-ngx/pull/4225))
|
||||
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
|
||||
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
||||
- Enhancement: Allow the user the specify the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
||||
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
||||
- Chore: update docker image and ci to node 20 [@shamoon](https://github.com/shamoon) ([#4184](https://github.com/paperless-ngx/paperless-ngx/pull/4184))
|
||||
- Fix: Trim unneeded libraries from Docker image [@stumpylog](https://github.com/stumpylog) ([#4183](https://github.com/paperless-ngx/paperless-ngx/pull/4183))
|
||||
@@ -430,7 +637,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
||||
- Enhancement: bulk edit object permissions [@shamoon](https://github.com/shamoon) ([#4176](https://github.com/paperless-ngx/paperless-ngx/pull/4176))
|
||||
- Fix: completely hide upload widget if user does not have permissions [@nawramm](https://github.com/nawramm) ([#4198](https://github.com/paperless-ngx/paperless-ngx/pull/4198))
|
||||
- Fix: application of theme color vars at root [@shamoon](https://github.com/shamoon) ([#4193](https://github.com/paperless-ngx/paperless-ngx/pull/4193))
|
||||
- Enhancement: Allow the user the specifiy the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
||||
- Enhancement: Allow the user the specify the export zip file name [@stumpylog](https://github.com/stumpylog) ([#4189](https://github.com/paperless-ngx/paperless-ngx/pull/4189))
|
||||
- Feature: Share links [@shamoon](https://github.com/shamoon) ([#3996](https://github.com/paperless-ngx/paperless-ngx/pull/3996))
|
||||
- Chore: change dark mode to use Bootstrap's color modes [@lkster](https://github.com/lkster) ([#4174](https://github.com/paperless-ngx/paperless-ngx/pull/4174))
|
||||
- Fix: support storage path placeholder via API [@shamoon](https://github.com/shamoon) ([#4179](https://github.com/paperless-ngx/paperless-ngx/pull/4179))
|
||||
@@ -462,11 +669,11 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: ghostscript rendering error doesnt trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
|
||||
- Fix: ghostscript rendering error doesn't trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
|
||||
|
||||
### All App Changes
|
||||
|
||||
- Fix: ghostscript rendering error doesnt trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
|
||||
- Fix: ghostscript rendering error doesn't trigger frontend failure message [@shamoon](https://github.com/shamoon) ([#4092](https://github.com/paperless-ngx/paperless-ngx/pull/4092))
|
||||
|
||||
## paperless-ngx 1.17.3
|
||||
|
||||
@@ -1133,7 +1340,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
||||
|
||||
### Documentation
|
||||
|
||||
- Whitespace changes, making sure the example is correcly aligned [@denilsonsa](https://github.com/denilsonsa) ([#3089](https://github.com/paperless-ngx/paperless-ngx/pull/3089))
|
||||
- Whitespace changes, making sure the example is correctly aligned [@denilsonsa](https://github.com/denilsonsa) ([#3089](https://github.com/paperless-ngx/paperless-ngx/pull/3089))
|
||||
- Docs: Include additional information about barcodes [@stumpylog](https://github.com/stumpylog) ([#2889](https://github.com/paperless-ngx/paperless-ngx/pull/2889))
|
||||
- Fix formatting in Setup documentation page [@igrybkov](https://github.com/igrybkov) ([#2880](https://github.com/paperless-ngx/paperless-ngx/pull/2880))
|
||||
- [Documentation] Update docker-compose steps to support podman [@white-gecko](https://github.com/white-gecko) ([#2855](https://github.com/paperless-ngx/paperless-ngx/pull/2855))
|
||||
@@ -1188,7 +1395,7 @@ Exports generated in Paperless-ngx v2.0.0–2.0.1 will **not** contain consumpti
|
||||
- Fix: update PaperlessTask on hard failures [@shamoon](https://github.com/shamoon) ([#3062](https://github.com/paperless-ngx/paperless-ngx/pull/3062))
|
||||
- Bump typescript from 4.8.4 to 4.9.5 in /src-ui [@dependabot](https://github.com/dependabot) ([#3071](https://github.com/paperless-ngx/paperless-ngx/pull/3071))
|
||||
- Bulk Bump npm packages 04.23 [@dependabot](https://github.com/dependabot) ([#3068](https://github.com/paperless-ngx/paperless-ngx/pull/3068))
|
||||
- Fix: Hide UI tour steps if user doesnt have permissions [@shamoon](https://github.com/shamoon) ([#3060](https://github.com/paperless-ngx/paperless-ngx/pull/3060))
|
||||
- Fix: Hide UI tour steps if user doesn't have permissions [@shamoon](https://github.com/shamoon) ([#3060](https://github.com/paperless-ngx/paperless-ngx/pull/3060))
|
||||
- Fix: Hide Permissions tab if user cannot view users [@shamoon](https://github.com/shamoon) ([#3061](https://github.com/paperless-ngx/paperless-ngx/pull/3061))
|
||||
- v1.14.0 delete document fixes [@shamoon](https://github.com/shamoon) ([#3020](https://github.com/paperless-ngx/paperless-ngx/pull/3020))
|
||||
- Bump wait-on from 6.0.1 to 7.0.1 in /src-ui [@dependabot](https://github.com/dependabot) ([#2990](https://github.com/paperless-ngx/paperless-ngx/pull/2990))
|
||||
@@ -1393,7 +1600,7 @@ older comments. The Docker image will automatically perform this reindex, bare m
|
||||
- [Docs] Add Paperless Mobile app to docs [@astubenbord](https://github.com/astubenbord) ([#2378](https://github.com/paperless-ngx/paperless-ngx/pull/2378))
|
||||
- Tiny spelling change [@veverkap](https://github.com/veverkap) ([#2369](https://github.com/paperless-ngx/paperless-ngx/pull/2369))
|
||||
- Documentation: update build instructions to remove deprecated [@shamoon](https://github.com/shamoon) ([#2334](https://github.com/paperless-ngx/paperless-ngx/pull/2334))
|
||||
- [Documentation] Add note that PAPERLESS_URL cant contain a path [@shamoon](https://github.com/shamoon) ([#2319](https://github.com/paperless-ngx/paperless-ngx/pull/2319))
|
||||
- [Documentation] Add note that PAPERLESS_URL can't contain a path [@shamoon](https://github.com/shamoon) ([#2319](https://github.com/paperless-ngx/paperless-ngx/pull/2319))
|
||||
- [Documentation] Add v1.11.3 changelog [@github-actions](https://github.com/github-actions) ([#2311](https://github.com/paperless-ngx/paperless-ngx/pull/2311))
|
||||
|
||||
### Maintenance
|
||||
@@ -1724,7 +1931,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
|
||||
|
||||
### All App Changes
|
||||
|
||||
- Add info that re-do OCR doesnt automatically refresh content [@shamoon](https://github.com/shamoon) ([#2025](https://github.com/paperless-ngx/paperless-ngx/pull/2025))
|
||||
- Add info that re-do OCR doesn't automatically refresh content [@shamoon](https://github.com/shamoon) ([#2025](https://github.com/paperless-ngx/paperless-ngx/pull/2025))
|
||||
- Bugfix: Fix created_date being a string [@stumpylog](https://github.com/stumpylog) ([#2023](https://github.com/paperless-ngx/paperless-ngx/pull/2023))
|
||||
- Bugfix: Fixes an issue with mixed text and images when redoing OCR [@stumpylog](https://github.com/stumpylog) ([#2017](https://github.com/paperless-ngx/paperless-ngx/pull/2017))
|
||||
- Bugfix: Don't allow exceptions during date parsing to fail consume [@stumpylog](https://github.com/stumpylog) ([#1998](https://github.com/paperless-ngx/paperless-ngx/pull/1998))
|
||||
@@ -2135,7 +2342,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
|
||||
- Fix local Docker image building [\@stumpylog](https://github.com/stumpylog) ([\#849](https://github.com/paperless-ngx/paperless-ngx/pull/849))
|
||||
- Fix: show errors on invalid date input [\@shamoon](https://github.com/shamoon) ([\#862](https://github.com/paperless-ngx/paperless-ngx/pull/862))
|
||||
- Fix: Older dates do not display on frontend [\@shamoon](https://github.com/shamoon) ([\#852](https://github.com/paperless-ngx/paperless-ngx/pull/852))
|
||||
- Fixes IMAP UTF8 Authenication [\@stumpylog](https://github.com/stumpylog) ([\#725](https://github.com/paperless-ngx/paperless-ngx/pull/725))
|
||||
- Fixes IMAP UTF8 Authentication [\@stumpylog](https://github.com/stumpylog) ([\#725](https://github.com/paperless-ngx/paperless-ngx/pull/725))
|
||||
- Fix password field remains visible [\@shamoon](https://github.com/shamoon) ([\#840](https://github.com/paperless-ngx/paperless-ngx/pull/840))
|
||||
- Fixes Pillow build for armv7 [\@stumpylog](https://github.com/stumpylog) ([\#815](https://github.com/paperless-ngx/paperless-ngx/pull/815))
|
||||
- Update frontend localization source file [\@shamoon](https://github.com/shamoon) ([\#814](https://github.com/paperless-ngx/paperless-ngx/pull/814))
|
||||
@@ -2256,7 +2463,7 @@ Versions 1.11.1 and 1.11.2 contain bug fixes from v1.11.0 that prevented use of
|
||||
[\@shamoon](https://github.com/shamoon) ([\#313](https://github.com/paperless-ngx/paperless-ngx/pull/313))
|
||||
- Fix imap tools bug [\@stumpylog](https://github.com/stumpylog)
|
||||
([\#393](https://github.com/paperless-ngx/paperless-ngx/pull/393))
|
||||
- Fix filterable dropdown buttons arent translated
|
||||
- Fix filterable dropdown buttons aren't translated
|
||||
[\@shamoon](https://github.com/shamoon) ([\#366](https://github.com/paperless-ngx/paperless-ngx/pull/366))
|
||||
- Fix 224: "Auto-detected date is day before receipt date"
|
||||
[\@a17t](https://github.com/a17t) ([\#246](https://github.com/paperless-ngx/paperless-ngx/pull/246))
|
||||
@@ -3092,7 +3299,7 @@ primarily.
|
||||
[OCRmyPDF](https://github.com/jbarlow83/OCRmyPDF) to perform OCR
|
||||
on documents. It still uses tesseract under the hood, but the
|
||||
PDF parser of Paperless has changed considerably and will behave
|
||||
different for some douments.
|
||||
different for some documents.
|
||||
- OCRmyPDF creates archived PDF/A documents with embedded text
|
||||
that can be selected in the front end.
|
||||
- Paperless stores archived versions of documents alongside with
|
||||
@@ -3143,7 +3350,7 @@ primarily.
|
||||
crash.
|
||||
- Mail handling no longer exits entirely when encountering errors.
|
||||
It will skip the account/rule/message on which the error
|
||||
occured.
|
||||
occurred.
|
||||
- Assigning correspondents from mail sender names failed for very
|
||||
long names. Paperless no longer assigns correspondents in these
|
||||
cases.
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
Paperless provides a wide range of customizations. Depending on how you
|
||||
run paperless, these settings have to be defined in different places.
|
||||
|
||||
Certain configuration options may be set via the UI. This currently includes
|
||||
common [OCR](#ocr) related settings. If set, these will take preference over the
|
||||
settings via environment variables. If not set, the environment setting or applicable
|
||||
default will be utilized instead.
|
||||
|
||||
- If you run paperless on docker, `paperless.conf` is not used.
|
||||
Rather, configure paperless by copying necessary options to
|
||||
`docker-compose.env`.
|
||||
@@ -660,11 +665,13 @@ completely.
|
||||
|
||||
Specifying 1 here will only use the first page.
|
||||
|
||||
The value must be greater than or equal to 1 to be used.
|
||||
|
||||
When combined with `PAPERLESS_OCR_MODE=redo` or
|
||||
`PAPERLESS_OCR_MODE=force`, paperless will not modify any text it
|
||||
finds on excluded pages and copy it verbatim.
|
||||
|
||||
Defaults to 0, which disables this feature and always uses all
|
||||
Defaults to unset, which disables this feature and always uses all
|
||||
pages.
|
||||
|
||||
#### [`PAPERLESS_OCR_IMAGE_DPI=<num>`](#PAPERLESS_OCR_IMAGE_DPI) {#PAPERLESS_OCR_IMAGE_DPI}
|
||||
@@ -678,7 +685,7 @@ fails, it uses this value as a fallback.
|
||||
|
||||
Set this to the DPI your scanner produces images at.
|
||||
|
||||
Default is none, which will automatically calculate image DPI so
|
||||
Defaults to unset, which will automatically calculate image DPI so
|
||||
that the produced PDF documents are A4 sized.
|
||||
|
||||
#### [`PAPERLESS_OCR_MAX_IMAGE_PIXELS=<num>`](#PAPERLESS_OCR_MAX_IMAGE_PIXELS) {#PAPERLESS_OCR_MAX_IMAGE_PIXELS}
|
||||
@@ -1310,6 +1317,10 @@ specified as "chi-tra".
|
||||
|
||||
Defaults to none, which does not install any additional languages.
|
||||
|
||||
!!! warning
|
||||
|
||||
This option must not be used in rootless containers.
|
||||
|
||||
#### [`PAPERLESS_ENABLE_FLOWER=<defined>`](#PAPERLESS_ENABLE_FLOWER) {#PAPERLESS_ENABLE_FLOWER}
|
||||
|
||||
: If this environment variable is defined, the Celery monitoring tool
|
||||
|
||||
@@ -18,6 +18,7 @@ physical documents into a searchable online archive so you can keep, well, _less
|
||||
## Features
|
||||
|
||||
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
|
||||
- _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way.
|
||||
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
|
||||
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
|
||||
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
|
||||
@@ -41,7 +42,7 @@ physical documents into a searchable online archive so you can keep, well, _less
|
||||
- Configure multiple accounts and rules for each account.
|
||||
- After processing, paperless can perform actions on the messages such as marking as read, deleting and more.
|
||||
- A built-in robust **multi-user permissions** system that supports 'global' permissions as well as per document or object.
|
||||
- A powerful templating system that gives you more control over the consumption pipeline.
|
||||
- A powerful workflow system that gives you even more control.
|
||||
- **Optimized** for multi core systems: Paperless-ngx consumes multiple documents in parallel.
|
||||
- The integrated sanity checker makes sure that your document archive is in good health.
|
||||
|
||||
@@ -156,9 +157,9 @@ Tag, correspondent, document type and storage path editing.
|
||||
|
||||
</div>
|
||||
<div class="grid-half-right" markdown>
|
||||
Consumption templates provide finer control over the document pipeline.
|
||||
Workflows provide finer control over the document pipeline and trigger actions.
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
|
||||
@@ -28,7 +28,8 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
1. Make sure that Docker and Docker Compose are installed.
|
||||
|
||||
!!! tip
|
||||
See the Docker installation instructions at https://docs.docker.com/engine/install/
|
||||
|
||||
See the Docker installation instructions at https://docs.docker.com/engine/install/
|
||||
|
||||
2. Download and run the installation script:
|
||||
|
||||
@@ -72,7 +73,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
|
||||
If you want to use the included `docker-compose.*.yml` file, you
|
||||
need to have at least Docker version **17.09.0** and Docker Compose
|
||||
version **v2**. To check do: `docker compose -v` or `docker -v`
|
||||
version **v2**. To check do: `docker compose version` or `docker -v`
|
||||
|
||||
See the [Docker installation guide](https://docs.docker.com/engine/install/) on how to install the current
|
||||
version of Docker for your operating system or Linux distribution of
|
||||
@@ -95,7 +96,7 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
||||
```
|
||||
|
||||
Don't change the part after the colon or paperless wont find your
|
||||
Don't change the part after the colon or paperless won't find your
|
||||
documents.
|
||||
|
||||
You may also need to change the default port that the webserver will
|
||||
@@ -120,6 +121,10 @@ steps described in [Docker setup](#docker_hub) automatically.
|
||||
|
||||
**Rootless**
|
||||
|
||||
!!! warning
|
||||
|
||||
It is currently not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`.
|
||||
|
||||
If you want to run Paperless as a rootless container, you will need
|
||||
to do the following in your `docker-compose.yml`:
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ command:
|
||||
You might encounter errors such as:
|
||||
|
||||
```shell-session
|
||||
The following error occured while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
|
||||
The following error occurred while consuming document.pdf: [Errno 13] Permission denied: '/usr/src/paperless/src/../consume/document.pdf'
|
||||
```
|
||||
|
||||
This happens when paperless does not have permission to delete files
|
||||
|
||||
124
docs/usage.md
124
docs/usage.md
@@ -149,7 +149,7 @@ different means. These are as follows:
|
||||
- **Flag:** Sets the 'important' flag on mails with consumed
|
||||
documents. Paperless will not consume flagged mails.
|
||||
- **Move to folder:** Moves consumed mails out of the way so that
|
||||
paperless wont consume them again.
|
||||
paperless won't consume them again.
|
||||
- **Add custom Tag:** Adds a custom tag to mails with consumed
|
||||
documents (the IMAP standard calls these "keywords"). Paperless
|
||||
will not consume mails already tagged. Not all mail servers support
|
||||
@@ -238,7 +238,7 @@ do not have an owner set.
|
||||
|
||||
### Default permissions
|
||||
|
||||
Default permissions for documents can be set using consumption templates.
|
||||
Default permissions for documents can be set using workflows.
|
||||
|
||||
For objects created via the web UI (tags, doc types, etc.) the default is to set the current user
|
||||
as owner and no extra permissions, but you explicitly set these under Settings > Permissions.
|
||||
@@ -255,29 +255,80 @@ permissions can be granted to limit access to certain parts of the UI (and corre
|
||||
In order to enable the password reset feature you will need to setup an SMTP backend, see
|
||||
[`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST)
|
||||
|
||||
## Consumption templates
|
||||
## Workflows
|
||||
|
||||
Consumption templates were introduced in v2.0 and allow for finer control over what metadata (tags, doc
|
||||
types) and permissions (owner, privileges) are assigned to documents during consumption. In general,
|
||||
templates are applied sequentially (by sort order) but subsequent templates will never override an
|
||||
assignment from a preceding template. The same is true for mail rules, e.g. if you set the correspondent
|
||||
in a mail rule any subsequent consumption templates that are applied _will not_ overwrite this. The
|
||||
exception to this is assignments that can be multiple e.g. tags and permissions, which will be merged.
|
||||
!!! note
|
||||
|
||||
Consumption templates allow you to filter by:
|
||||
v2.3 added "Workflows" and existing "Consumption Templates" were converted automatically to the new more powerful format.
|
||||
|
||||
Workflows allow hooking into the Paperless-ngx document pipeline, for example to alter what metadata (tags, doc types) and
|
||||
permissions (owner, privileges) are assigned to documents. Workflows can have multiple 'triggers' and 'actions'. Triggers
|
||||
are events (with optional filtering rules) that will cause the workflow to be run and actions are the set of sequential
|
||||
actions to apply.
|
||||
|
||||
In general, workflows and any actions they contain are applied sequentially by sort order. For "assignment" actions, subsequent
|
||||
workflow actions will override previous assignments, except for assignments that accept multiple items e.g. tags, custom
|
||||
fields and permissions, which will be merged.
|
||||
|
||||
### Workflow Triggers
|
||||
|
||||
Currently, there are three events that correspond to workflow trigger 'types':
|
||||
|
||||
1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption
|
||||
folder or API), file path, file name, mail rule
|
||||
2. **Document Added**: _after_ a document is added. At this time, file path and source information is no longer available,
|
||||
but the document content has been extracted and metadata such as document type, tags, etc. have been set, so these can now
|
||||
be used for filtering.
|
||||
3. **Document Updated**: when a document is updated. Similar to 'added' events, triggers can include filtering by content matching,
|
||||
tags, doc type, or correspondent.
|
||||
|
||||
The following flow diagram illustrates the three trigger types:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
consumption{"Matching
|
||||
'Consumption'
|
||||
trigger(s)"}
|
||||
|
||||
added{"Matching
|
||||
'Added'
|
||||
trigger(s)"}
|
||||
|
||||
updated{"Matching
|
||||
'Updated'
|
||||
trigger(s)"}
|
||||
|
||||
A[New Document] --> consumption
|
||||
consumption --> |Yes| C[Workflow Actions Run]
|
||||
consumption --> |No| D
|
||||
C --> D[Document Added]
|
||||
D -- Paperless-ngx 'matching' of tags, etc. --> added
|
||||
added --> |Yes| F[Workflow Actions Run]
|
||||
added --> |No| G
|
||||
F --> G[Document Finalized]
|
||||
H[Existing Document Changed] --> updated
|
||||
updated --> |Yes| J[Workflow Actions Run]
|
||||
updated --> |No| K
|
||||
J --> K[Document Saved]
|
||||
```
|
||||
|
||||
#### Filters {#workflow-trigger-filters}
|
||||
|
||||
Workflows allow you to filter by:
|
||||
|
||||
- Source, e.g. documents uploaded via consume folder, API (& the web UI) and mail fetch
|
||||
- File name, including wildcards e.g. \*.pdf will apply to all pdfs
|
||||
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
|
||||
example, automatically assigning documents to different owners based on the upload directory.
|
||||
- Mail rule. Choosing this option will force 'mail fetch' to be the template source.
|
||||
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
|
||||
- Content matching (`Added` and `Updated` triggers only). Filter document content using the matching settings.
|
||||
- Tags (`Added` and `Updated` triggers only). Filter for documents with any of the specified tags
|
||||
- Document type (`Added` and `Updated` triggers only). Filter documents with this doc type
|
||||
- Correspondent (`Added` and `Updated` triggers only). Filter documents with this correspondent
|
||||
|
||||
!!! note
|
||||
### Workflow Actions
|
||||
|
||||
You must include a file name filter, a path filter or a mail rule filter. Use * for either to apply
|
||||
to all files.
|
||||
|
||||
Consumption templates can assign:
|
||||
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
|
||||
@@ -285,21 +336,11 @@ Consumption templates can assign:
|
||||
- View and / or edit permissions to users or groups
|
||||
- Custom fields. Note that no value for the field will be set
|
||||
|
||||
### Consumption template permissions
|
||||
#### Title placeholders
|
||||
|
||||
All users who have application permissions for editing consumption templates can see the same set
|
||||
of templates. In other words, templates themselves intentionally do not have an owner or permissions.
|
||||
|
||||
Given their potentially far-reaching capabilities, you may want to restrict access to templates.
|
||||
|
||||
Upon migration, existing installs will grant access to consumption templates to users who can add
|
||||
documents (and superusers who can always access all parts of the app).
|
||||
|
||||
### Title placeholders
|
||||
|
||||
Consumption template titles can include placeholders, _only for items that are assigned within the template_.
|
||||
This is because at the time of consumption (when the title is to be set), no automatic tags etc. have been
|
||||
applied. You can use the following placeholders:
|
||||
Workflow titles can include placeholders but the available options differ depending on the type of
|
||||
workflow trigger. This is because at the time of consumption (when the title is to be set), no automatic tags etc. have been
|
||||
applied. You can use the following placeholders with any trigger type:
|
||||
|
||||
- `{correspondent}`: assigned correspondent name
|
||||
- `{document_type}`: assigned document type name
|
||||
@@ -314,6 +355,27 @@ applied. You can use the following placeholders:
|
||||
- `{added_time}`: added time in HH:MM format
|
||||
- `{original_filename}`: original file name without extension
|
||||
|
||||
The following placeholders are only available for "added" or "updated" triggers
|
||||
|
||||
- `{created}`: created datetime
|
||||
- `{created_year}`: created year
|
||||
- `{created_year_short}`: created year
|
||||
- `{created_month}`: created month
|
||||
- `{created_month_name}`: created month name
|
||||
- `{created_month_name_short}`: created month short name
|
||||
- `{created_day}`: created day
|
||||
- `{created_time}`: created time in HH:MM format
|
||||
|
||||
### Workflow permissions
|
||||
|
||||
All users who have application permissions for editing workflows can see the same set
|
||||
of workflows. In other words, workflows themselves intentionally do not have an owner or permissions.
|
||||
|
||||
Given their potentially far-reaching capabilities, you may want to restrict access to workflows.
|
||||
|
||||
Upon migration, existing installs will grant access to workflows to users who can add
|
||||
documents (and superusers who can always access all parts of the app).
|
||||
|
||||
## Custom Fields {#custom-fields}
|
||||
|
||||
Paperless-ngx supports the use of custom fields for documents as of v2.0, allowing a user
|
||||
@@ -349,7 +411,7 @@ The following custom field types are supported:
|
||||
|
||||
## Share Links
|
||||
|
||||
Paperless-ngx added the abiltiy to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
|
||||
Paperless-ngx added the ability to create shareable links to files in version 2.0. You can find the button for this on the document detail screen.
|
||||
|
||||
- Share links do not require a user to login and thus link directly to a file.
|
||||
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
|
||||
|
||||
@@ -44,6 +44,11 @@ markdown_extensions:
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- footnotes
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
strict: true
|
||||
nav:
|
||||
- index.md
|
||||
|
||||
1325
src-ui/messages.xlf
1325
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
327
src-ui/package-lock.json
generated
327
src-ui/package-lock.json
generated
@@ -10,14 +10,14 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^17.0.4",
|
||||
"@angular/common": "~17.0.7",
|
||||
"@angular/compiler": "~17.0.7",
|
||||
"@angular/core": "~17.0.7",
|
||||
"@angular/forms": "~17.0.7",
|
||||
"@angular/localize": "~17.0.7",
|
||||
"@angular/platform-browser": "~17.0.7",
|
||||
"@angular/platform-browser-dynamic": "~17.0.7",
|
||||
"@angular/router": "~17.0.7",
|
||||
"@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",
|
||||
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
|
||||
"@ng-select/ng-select": "^12.0.4",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
@@ -37,21 +37,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "17.0.0",
|
||||
"@angular-devkit/build-angular": "~17.0.7",
|
||||
"@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.7",
|
||||
"@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.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@types/node": "^20.10.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^13.1.4",
|
||||
@@ -107,12 +107,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/architect": {
|
||||
"version": "0.1700.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.7.tgz",
|
||||
"integrity": "sha512-32uitQKsYLGXAKoXBsmOnPsTt9pS+b9cnFI9ZvBFVhJ31I2EOM7vGcMFalhTxdB/DkVHk4TyO78efV0V26DwCA==",
|
||||
"version": "0.1700.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.8.tgz",
|
||||
"integrity": "sha512-SWVr3CvwO6T0yW2ytszCwBT1g92vyFkwbVUxqE93urYnoD8PvP+81GH5YwVjHQTgvhP4eXQMGZ9hpHx57VOrWQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "17.0.7",
|
||||
"@angular-devkit/core": "17.0.8",
|
||||
"rxjs": "7.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -122,15 +122,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.7.tgz",
|
||||
"integrity": "sha512-AtEzLk6n6BXqQzk0Bsupe6GV0IgUe7RbpBfqROi+NZqMA7OUAHCX3xA6M68Qu+5KxBtW7T5lHeZZ7iP/y39wtQ==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.8.tgz",
|
||||
"integrity": "sha512-u7R5yX92ZxOL/LfxiKGGqlBo86100sJ5Rabavn8DeGtYP8N0qgwCcNwlW2zaMoUlkw2geMnxcxIX5VJI4iFPUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "2.2.1",
|
||||
"@angular-devkit/architect": "0.1700.7",
|
||||
"@angular-devkit/build-webpack": "0.1700.7",
|
||||
"@angular-devkit/core": "17.0.7",
|
||||
"@angular-devkit/architect": "0.1700.8",
|
||||
"@angular-devkit/build-webpack": "0.1700.8",
|
||||
"@angular-devkit/core": "17.0.8",
|
||||
"@babel/core": "7.23.2",
|
||||
"@babel/generator": "7.23.0",
|
||||
"@babel/helper-annotate-as-pure": "7.22.5",
|
||||
@@ -141,7 +141,7 @@
|
||||
"@babel/preset-env": "7.23.2",
|
||||
"@babel/runtime": "7.23.2",
|
||||
"@discoveryjs/json-ext": "0.5.7",
|
||||
"@ngtools/webpack": "17.0.7",
|
||||
"@ngtools/webpack": "17.0.8",
|
||||
"@vitejs/plugin-basic-ssl": "1.0.1",
|
||||
"ansi-colors": "4.1.3",
|
||||
"autoprefixer": "10.4.16",
|
||||
@@ -245,12 +245,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/build-webpack": {
|
||||
"version": "0.1700.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.7.tgz",
|
||||
"integrity": "sha512-B9Mg/qYDpE5my8PJ3VPQyRSUV0Oq1bFUzU8s0ZpqEZl1URKc04pm0LtLmebrMIcUZgDiGk0RHaD+O1E9IV/bdQ==",
|
||||
"version": "0.1700.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.8.tgz",
|
||||
"integrity": "sha512-GA7QlCAlYB3uBkRaUYgIC/Vfajb9jMmouwYiAAEm34ZyP3ThFjdqsYd/A/exnuESt5o6Bh++C/PI34sV3lawRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.1700.7",
|
||||
"@angular-devkit/architect": "0.1700.8",
|
||||
"rxjs": "7.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -264,9 +264,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/core": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.7.tgz",
|
||||
"integrity": "sha512-vATobHo5O5tJba424hJfQWLb40GzvZPNsI74dcgSUTgrDph8ksmk5xB9OvEvf0INorQZ2IMphj/VIWj4/+JqSA==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.8.tgz",
|
||||
"integrity": "sha512-gI8+SOwGUwr0WOlFrhLjohLolMzcguuoR0LTZEcGjdXvQyPgH4NDSRIIrfWCdu+ZVhfy76o3zQYdYc9QN8NrjQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "8.12.0",
|
||||
@@ -291,12 +291,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/schematics": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.7.tgz",
|
||||
"integrity": "sha512-BY11OkJkM3xyXcvyD7x5kGY/c8Ufd4AfPvI0D9imhVxbns45Q48b1DlvCQvSnCJ/s+OwnkrYb/Efa70ZiaGu8A==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.8.tgz",
|
||||
"integrity": "sha512-syo814SVWfJvne448IijjZvpWbuqJsEutdNqHWLTewTfX2U3KrIAr/XRVcXQMuyMvLCDiuxjMgEJxOIP7mcIPw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "17.0.7",
|
||||
"@angular-devkit/core": "17.0.8",
|
||||
"jsonc-parser": "3.2.0",
|
||||
"magic-string": "0.30.5",
|
||||
"ora": "5.4.1",
|
||||
@@ -423,15 +423,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cli": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.7.tgz",
|
||||
"integrity": "sha512-oSa0GVAQNA7wFbLJYeaO3kV4iUcbKEqXDLxcIE8s1GfHddBOlXH2P1T4fXonCBl5qvV+joP0G0+fs7I0w2utZQ==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.8.tgz",
|
||||
"integrity": "sha512-yZXYNLAFv9u2qypsVqtS+rRCsnjsIPYXr6TcI/r5buzOtC7UQ2lleYsWJqX47SsyGMk/o3gaYg5Bj2I5mmRDLA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.1700.7",
|
||||
"@angular-devkit/core": "17.0.7",
|
||||
"@angular-devkit/schematics": "17.0.7",
|
||||
"@schematics/angular": "17.0.7",
|
||||
"@angular-devkit/architect": "0.1700.8",
|
||||
"@angular-devkit/core": "17.0.8",
|
||||
"@angular-devkit/schematics": "17.0.8",
|
||||
"@schematics/angular": "17.0.8",
|
||||
"@yarnpkg/lockfile": "1.1.0",
|
||||
"ansi-colors": "4.1.3",
|
||||
"ini": "4.1.1",
|
||||
@@ -457,9 +457,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/common": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.7.tgz",
|
||||
"integrity": "sha512-bPPL6x0KOAOTxKSE2j4EWmEUOnqZYzOYiHzroa5b9UEyA9NvGkd9bm3zIxw8xcndRj1Ehcmvpi6KBLcYBBbWfg==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.8.tgz",
|
||||
"integrity": "sha512-fFfwtdg7H+OkqnvV/ENu8F8KGfgIiH16DDbQqYY5KQyyQB+SMsoVW29F1fGx6Y30s7ZlsLOy6cHhgrw74itkSw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -467,14 +467,14 @@
|
||||
"node": "^18.13.0 || >=20.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "17.0.7",
|
||||
"@angular/core": "17.0.8",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.7.tgz",
|
||||
"integrity": "sha512-QHPuLti2c2tGZmOGZ0cfCHo4LxiHUkC27I0aZFDyQSSQqEI5obQGVlEREHysw0nsS3sYIcLvqcwcKcRtXlXtxQ==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.8.tgz",
|
||||
"integrity": "sha512-48jWypuhBGTrUUbkz1vB9gjbKKZ3hpuJ2DUUncd331Yw4tqkqZQbBa/E3ei4IHiCxEvW2uX3lI4AwlhuozmUtA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -482,7 +482,7 @@
|
||||
"node": "^18.13.0 || >=20.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "17.0.7"
|
||||
"@angular/core": "17.0.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@angular/core": {
|
||||
@@ -491,9 +491,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/compiler-cli": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.7.tgz",
|
||||
"integrity": "sha512-YnL38idjIYtl3BXYpv+sVJKWGbUjHT6eyQSQVAfO/1AwWqVa21K9hnE+Q37VmUKEcKFMnQembeuErA+KVsGI6A==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.8.tgz",
|
||||
"integrity": "sha512-ny2SMVgl+icjMuU5ZM57yFGUrhjR0hNxfCn0otAD3jUFliz/Onu9l6EPRKA5Cr8MZx3mg3rTLSBMD17YT8rsOg==",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.23.2",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||
@@ -513,14 +513,14 @@
|
||||
"node": "^18.13.0 || >=20.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "17.0.7",
|
||||
"@angular/compiler": "17.0.8",
|
||||
"typescript": ">=5.2 <5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/core": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.7.tgz",
|
||||
"integrity": "sha512-mEkelXkzEi6+A9GjdKOSGGzQAfo1iAjVTn6YsplNUeGE5JgDZYZ7sXGQqs0Lin7dzJxnPAgGjCOl7SpWLXIPSQ==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.8.tgz",
|
||||
"integrity": "sha512-tzYsK24LdkNuKNJK6efF4XOqspvF/qOe9j/n1Y61a6mNvFwsJFGbcmdZMby4hI/YRm6oIDoIIFjSep8ycp6Pbw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -533,9 +533,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/forms": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.7.tgz",
|
||||
"integrity": "sha512-28BxRxEmgZIofGwVp6s2v3ri/kuWW+/EY/ZXhavlWKJEh4ATJl72k0RkRWNcQi4wnvn0Qb8tFdnVJnvRZvvKEw==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.8.tgz",
|
||||
"integrity": "sha512-WZBHbMQjaSovAzOMhKqZN+m7eUPGfOzh9rKFKvj6UQLIJ9qSpEpqlvL0omU1z/47s3XXeLiBzomMiRfQISJvvw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -543,16 +543,16 @@
|
||||
"node": "^18.13.0 || >=20.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "17.0.7",
|
||||
"@angular/core": "17.0.7",
|
||||
"@angular/platform-browser": "17.0.7",
|
||||
"@angular/common": "17.0.8",
|
||||
"@angular/core": "17.0.8",
|
||||
"@angular/platform-browser": "17.0.8",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/localize": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.0.7.tgz",
|
||||
"integrity": "sha512-avYYQ8zin2thzvsH2YP3WxlwkvOzjNEXxjv4yyZBx6wul68e/753kQK/0RmSUYaBpDTUEZYzrPpDay00TKwBOA==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.0.8.tgz",
|
||||
"integrity": "sha512-1zW8qWKNMH3r/x4KpwzzUmVY+iN76vYdhjA6gzZDnpJxpon9eyljNEildj9+zSWeNUr2LgJ6HnkIX9q1f3mXfA==",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.23.2",
|
||||
"fast-glob": "3.3.1",
|
||||
@@ -567,14 +567,14 @@
|
||||
"node": "^18.13.0 || >=20.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/compiler": "17.0.7",
|
||||
"@angular/compiler-cli": "17.0.7"
|
||||
"@angular/compiler": "17.0.8",
|
||||
"@angular/compiler-cli": "17.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.7.tgz",
|
||||
"integrity": "sha512-bm9/wt51nc/MPjft/FlRNIgFSeLjDtfJOT7M32Rt6kOHhNKSK7ZTPWdMe9ahuHSbAhLzd0G/4NsT5sKrWSeVZg==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.8.tgz",
|
||||
"integrity": "sha512-XaI+p2AxQaIHzR761lhPUf4OcOp46WDW0IfbvOzaezHE+8r81joZyVSDQPgXSa/aRfI58YhcfUavuGqyU3PphA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -582,9 +582,9 @@
|
||||
"node": "^18.13.0 || >=20.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/animations": "17.0.7",
|
||||
"@angular/common": "17.0.7",
|
||||
"@angular/core": "17.0.7"
|
||||
"@angular/animations": "17.0.8",
|
||||
"@angular/common": "17.0.8",
|
||||
"@angular/core": "17.0.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@angular/animations": {
|
||||
@@ -593,9 +593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/platform-browser-dynamic": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.7.tgz",
|
||||
"integrity": "sha512-OquwUX9fLWA2JUZW5Jm6atk0CPt0sA7Tg24eGLsr6g1XfTS7jRZprlGaa72NgPLnQVV6m84o/ZiNYS6yPmq1Gg==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.8.tgz",
|
||||
"integrity": "sha512-BIXNKnfBZb8sdluQ7WIhIXFuVnsJJ0SV+aiMKzQ7B6XhWoAXZQnlvON2thydjIIVuCvaF3YmWTbILI2K8YZ2jQ==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -603,16 +603,16 @@
|
||||
"node": "^18.13.0 || >=20.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "17.0.7",
|
||||
"@angular/compiler": "17.0.7",
|
||||
"@angular/core": "17.0.7",
|
||||
"@angular/platform-browser": "17.0.7"
|
||||
"@angular/common": "17.0.8",
|
||||
"@angular/compiler": "17.0.8",
|
||||
"@angular/core": "17.0.8",
|
||||
"@angular/platform-browser": "17.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/router": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.7.tgz",
|
||||
"integrity": "sha512-rUFPe1uDlYYw6+3Gq68czW7WxBH7zT/D3UsT1otqwUV4RnQQsVze4fIit9FqJh7tuP4y3WpB4XBNf7p7Oi6TJw==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.8.tgz",
|
||||
"integrity": "sha512-ptphcRe1RG/mIS60R7ZPilkkrxautqB0sOhds3h5VP3g628G1a2HWzvnmvjEfpJWDMFivV32VJMMBtTLqGr+0Q==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -620,9 +620,9 @@
|
||||
"node": "^18.13.0 || >=20.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "17.0.7",
|
||||
"@angular/core": "17.0.7",
|
||||
"@angular/platform-browser": "17.0.7",
|
||||
"@angular/common": "17.0.8",
|
||||
"@angular/core": "17.0.8",
|
||||
"@angular/platform-browser": "17.0.8",
|
||||
"rxjs": "^6.5.3 || ^7.4.0"
|
||||
}
|
||||
},
|
||||
@@ -2895,9 +2895,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
|
||||
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
|
||||
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -3941,9 +3941,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ngtools/webpack": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.7.tgz",
|
||||
"integrity": "sha512-gwhUhpwXn0trwwKdSu9WlJbEcLt+s/2fPwoD9lZ0y3wXfrOogsfcNBJKeO5BZf1h+A3AWt7ePmgrZXSJM+865Q==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.8.tgz",
|
||||
"integrity": "sha512-wx0XBMrbpDeailK2uIhp/ZVMC3GK3BWwJjUu5SbT4BFrcoi2Zd9/9m0RCBAY54UXLBCqKd+ih7pJ6JSvprZmWw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.13.0 || >=20.9.0",
|
||||
@@ -4459,13 +4459,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@schematics/angular": {
|
||||
"version": "17.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.7.tgz",
|
||||
"integrity": "sha512-d7QKmcKrM4owb/2bR7Ipf23roiNbvbD/x7reNhQAtKAPLSHJ3Ulkf1+Yv+dj+9f+K7y9SBviEUSrD27BQ9WaxQ==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.8.tgz",
|
||||
"integrity": "sha512-1h5mwKFv1B/L5JWZ0mxnC4ms06iwnSi/w+GgRZPeM3P5BpuZuvAkFiClNnM55iLlQJXRQioPNLM3sOsz7spR6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "17.0.7",
|
||||
"@angular-devkit/schematics": "17.0.7",
|
||||
"@angular-devkit/core": "17.0.8",
|
||||
"@angular-devkit/schematics": "17.0.8",
|
||||
"jsonc-parser": "3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4878,9 +4878,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
|
||||
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
|
||||
"version": "20.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
|
||||
"integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
@@ -4896,9 +4896,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
|
||||
"integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==",
|
||||
"version": "6.9.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
|
||||
"integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
@@ -4995,16 +4995,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.14.0.tgz",
|
||||
"integrity": "sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz",
|
||||
"integrity": "sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.14.0",
|
||||
"@typescript-eslint/type-utils": "6.14.0",
|
||||
"@typescript-eslint/utils": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0",
|
||||
"@typescript-eslint/scope-manager": "6.17.0",
|
||||
"@typescript-eslint/type-utils": "6.17.0",
|
||||
"@typescript-eslint/utils": "6.17.0",
|
||||
"@typescript-eslint/visitor-keys": "6.17.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -5030,13 +5030,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz",
|
||||
"integrity": "sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.17.0.tgz",
|
||||
"integrity": "sha512-hDXcWmnbtn4P2B37ka3nil3yi3VCQO2QEB9gBiHJmQp5wmyQWqnjA85+ZcE8c4FqnaB6lBwMrPkgd4aBYz3iNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "6.14.0",
|
||||
"@typescript-eslint/utils": "6.14.0",
|
||||
"@typescript-eslint/typescript-estree": "6.17.0",
|
||||
"@typescript-eslint/utils": "6.17.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -5057,17 +5057,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.14.0.tgz",
|
||||
"integrity": "sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.17.0.tgz",
|
||||
"integrity": "sha512-LofsSPjN/ITNkzV47hxas2JCsNCEnGhVvocfyOcLzT9c/tSZE7SfhS/iWtzP1lKNOEfLhRTZz6xqI8N2RzweSQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.14.0",
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/typescript-estree": "6.14.0",
|
||||
"@typescript-eslint/scope-manager": "6.17.0",
|
||||
"@typescript-eslint/types": "6.17.0",
|
||||
"@typescript-eslint/typescript-estree": "6.17.0",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5082,15 +5082,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz",
|
||||
"integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.17.0.tgz",
|
||||
"integrity": "sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.14.0",
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/typescript-estree": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0",
|
||||
"@typescript-eslint/scope-manager": "6.17.0",
|
||||
"@typescript-eslint/types": "6.17.0",
|
||||
"@typescript-eslint/typescript-estree": "6.17.0",
|
||||
"@typescript-eslint/visitor-keys": "6.17.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5110,13 +5110,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz",
|
||||
"integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz",
|
||||
"integrity": "sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0"
|
||||
"@typescript-eslint/types": "6.17.0",
|
||||
"@typescript-eslint/visitor-keys": "6.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -5211,9 +5211,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz",
|
||||
"integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.17.0.tgz",
|
||||
"integrity": "sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -5224,16 +5224,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz",
|
||||
"integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz",
|
||||
"integrity": "sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0",
|
||||
"@typescript-eslint/types": "6.17.0",
|
||||
"@typescript-eslint/visitor-keys": "6.17.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "9.0.3",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -5250,6 +5251,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "6.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
|
||||
@@ -5350,12 +5375,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz",
|
||||
"integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==",
|
||||
"version": "6.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz",
|
||||
"integrity": "sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/types": "6.17.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8687,15 +8712,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
|
||||
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
|
||||
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@eslint/js": "8.55.0",
|
||||
"@eslint/js": "8.56.0",
|
||||
"@humanwhocodes/config-array": "^0.11.13",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^17.0.4",
|
||||
"@angular/common": "~17.0.7",
|
||||
"@angular/compiler": "~17.0.7",
|
||||
"@angular/core": "~17.0.7",
|
||||
"@angular/forms": "~17.0.7",
|
||||
"@angular/localize": "~17.0.7",
|
||||
"@angular/platform-browser": "~17.0.7",
|
||||
"@angular/platform-browser-dynamic": "~17.0.7",
|
||||
"@angular/router": "~17.0.7",
|
||||
"@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",
|
||||
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
|
||||
"@ng-select/ng-select": "^12.0.4",
|
||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||
@@ -39,21 +39,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "17.0.0",
|
||||
"@angular-devkit/build-angular": "~17.0.7",
|
||||
"@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.7",
|
||||
"@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.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@types/node": "^20.10.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "^13.1.4",
|
||||
|
||||
@@ -21,10 +21,11 @@ import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
} from './services/permissions.service'
|
||||
import { ConsumptionTemplatesComponent } from './components/manage/consumption-templates/consumption-templates.component'
|
||||
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
||||
import { MailComponent } from './components/manage/mail/mail.component'
|
||||
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
|
||||
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
|
||||
import { ConfigComponent } from './components/admin/config/config.component'
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
@@ -179,6 +180,17 @@ export const routes: Routes = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
component: ConfigComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.Admin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tasks',
|
||||
component: TasksComponent,
|
||||
@@ -202,13 +214,13 @@ export const routes: Routes = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'templates',
|
||||
component: ConsumptionTemplatesComponent,
|
||||
path: 'workflows',
|
||||
component: WorkflowsComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
requiredPermission: {
|
||||
action: PermissionAction.View,
|
||||
type: PermissionType.ConsumptionTemplate,
|
||||
type: PermissionType.Workflow,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -176,9 +176,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
},
|
||||
},
|
||||
{
|
||||
anchorId: 'tour.consumption-templates',
|
||||
content: $localize`Consumption templates give you finer control over the document ingestion process.`,
|
||||
route: '/templates',
|
||||
anchorId: 'tour.workflows',
|
||||
content: $localize`Workflows give you more control over the document pipeline.`,
|
||||
route: '/workflows',
|
||||
backdropConfig: {
|
||||
offset: 0,
|
||||
},
|
||||
|
||||
@@ -95,8 +95,8 @@ import { UsernamePipe } from './pipes/username.pipe'
|
||||
import { LogoComponent } from './components/common/logo/logo.component'
|
||||
import { IsNumberPipe } from './pipes/is-number.pipe'
|
||||
import { ShareLinksDropdownComponent } from './components/common/share-links-dropdown/share-links-dropdown.component'
|
||||
import { ConsumptionTemplatesComponent } from './components/manage/consumption-templates/consumption-templates.component'
|
||||
import { ConsumptionTemplateEditDialogComponent } from './components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component'
|
||||
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
|
||||
import { WorkflowEditDialogComponent } from './components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
||||
import { MailComponent } from './components/manage/mail/mail.component'
|
||||
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
@@ -108,6 +108,8 @@ import { ProfileEditDialogComponent } from './components/common/profile-edit-dia
|
||||
import { PdfViewerComponent } from './components/common/pdf-viewer/pdf-viewer.component'
|
||||
import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component'
|
||||
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
||||
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
||||
import { ConfigComponent } from './components/admin/config/config.component'
|
||||
|
||||
import localeAf from '@angular/common/locales/af'
|
||||
import localeAr from '@angular/common/locales/ar'
|
||||
@@ -251,8 +253,8 @@ function initializeApp(settings: SettingsService) {
|
||||
LogoComponent,
|
||||
IsNumberPipe,
|
||||
ShareLinksDropdownComponent,
|
||||
ConsumptionTemplatesComponent,
|
||||
ConsumptionTemplateEditDialogComponent,
|
||||
WorkflowsComponent,
|
||||
WorkflowEditDialogComponent,
|
||||
MailComponent,
|
||||
UsersAndGroupsComponent,
|
||||
FileDropComponent,
|
||||
@@ -263,6 +265,8 @@ function initializeApp(settings: SettingsService) {
|
||||
PdfViewerComponent,
|
||||
DocumentLinkComponent,
|
||||
PreviewPopupComponent,
|
||||
SwitchComponent,
|
||||
ConfigComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
54
src-ui/src/app/components/admin/config/config.component.html
Normal file
54
src-ui/src/app/components/admin/config/config.component.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<pngx-page-header title="Application Configuration" subTitle="Global Paperless-ngx configuration options" i18n-title i18n-subTitle></pngx-page-header>
|
||||
|
||||
<form [formGroup]="configForm" (ngSubmit)="saveConfig()" class="pb-4">
|
||||
|
||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
||||
@for (category of optionCategories; track category) {
|
||||
<li [ngbNavItem]="category">
|
||||
<a ngbNavLink i18n>{{category}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="p-3">
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
|
||||
@for (option of getCategoryOptions(category); track option.key) {
|
||||
<div class="col">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="card-title">
|
||||
<h6>
|
||||
{{option.title}}
|
||||
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#info-circle"/>
|
||||
</svg>
|
||||
</a>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="mb-n3">
|
||||
@switch (option.type) {
|
||||
@case (ConfigOptionType.Select) { <pngx-input-select [formControlName]="option.key" [error]="errors[option.key]" [items]="option.choices" [allowNull]="true"></pngx-input-select> }
|
||||
@case (ConfigOptionType.Number) { <pngx-input-number [formControlName]="option.key" [error]="errors[option.key]" [showAdd]="false"></pngx-input-number> }
|
||||
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [showUnsetNote]="true" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
|
||||
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" (click)="discardChanges()" class="btn btn-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
103
src-ui/src/app/components/admin/config/config.component.spec.ts
Normal file
103
src-ui/src/app/components/admin/config/config.component.spec.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
|
||||
import { ConfigComponent } from './config.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { OutputTypeConfig } from 'src/app/data/paperless-config'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { TextComponent } from '../../common/input/text/text.component'
|
||||
import { NumberComponent } from '../../common/input/number/number.component'
|
||||
import { SwitchComponent } from '../../common/input/switch/switch.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { SelectComponent } from '../../common/input/select/select.component'
|
||||
|
||||
describe('ConfigComponent', () => {
|
||||
let component: ConfigComponent
|
||||
let fixture: ComponentFixture<ConfigComponent>
|
||||
let configService: ConfigService
|
||||
let toastService: ToastService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ConfigComponent,
|
||||
TextComponent,
|
||||
SelectComponent,
|
||||
NumberComponent,
|
||||
SwitchComponent,
|
||||
PageHeaderComponent,
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
BrowserModule,
|
||||
NgbModule,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
configService = TestBed.inject(ConfigService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
fixture = TestBed.createComponent(ConfigComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should load config on init, show error if necessary', () => {
|
||||
const getSpy = jest.spyOn(configService, 'getConfig')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
getSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('Error getting config'))
|
||||
)
|
||||
component.ngOnInit()
|
||||
expect(getSpy).toHaveBeenCalled()
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
getSpy.mockReturnValueOnce(
|
||||
of({ output_type: OutputTypeConfig.PDF_A } as any)
|
||||
)
|
||||
component.ngOnInit()
|
||||
expect(component.initialConfig).toEqual({
|
||||
output_type: OutputTypeConfig.PDF_A,
|
||||
})
|
||||
})
|
||||
|
||||
it('should save config, show error if necessary', () => {
|
||||
const saveSpy = jest.spyOn(configService, 'saveConfig')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
saveSpy.mockReturnValueOnce(
|
||||
throwError(() => new Error('Error saving config'))
|
||||
)
|
||||
component.saveConfig()
|
||||
expect(saveSpy).toHaveBeenCalled()
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
saveSpy.mockReturnValueOnce(
|
||||
of({ output_type: OutputTypeConfig.PDF_A } as any)
|
||||
)
|
||||
component.saveConfig()
|
||||
expect(component.initialConfig).toEqual({
|
||||
output_type: OutputTypeConfig.PDF_A,
|
||||
})
|
||||
})
|
||||
|
||||
it('should support discard changes', () => {
|
||||
component.initialConfig = { output_type: OutputTypeConfig.PDF_A2 } as any
|
||||
component.configForm.patchValue({ output_type: OutputTypeConfig.PDF_A })
|
||||
component.discardChanges()
|
||||
expect(component.configForm.get('output_type').value).toEqual(
|
||||
OutputTypeConfig.PDF_A2
|
||||
)
|
||||
})
|
||||
|
||||
it('should support JSON validation for e.g. user_args', () => {
|
||||
component.configForm.patchValue({ user_args: '{ foo bar }' })
|
||||
expect(component.errors).toEqual({ user_args: 'Invalid JSON' })
|
||||
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
||||
expect(component.errors).toEqual({ user_args: null })
|
||||
})
|
||||
})
|
||||
163
src-ui/src/app/components/admin/config/config.component.ts
Normal file
163
src-ui/src/app/components/admin/config/config.component.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { AbstractControl, FormControl, FormGroup } from '@angular/forms'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
Subject,
|
||||
Subscription,
|
||||
first,
|
||||
takeUntil,
|
||||
} from 'rxjs'
|
||||
import {
|
||||
PaperlessConfigOptions,
|
||||
ConfigCategory,
|
||||
ConfigOption,
|
||||
ConfigOptionType,
|
||||
PaperlessConfig,
|
||||
} from 'src/app/data/paperless-config'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-config',
|
||||
templateUrl: './config.component.html',
|
||||
styleUrl: './config.component.scss',
|
||||
})
|
||||
export class ConfigComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
public readonly ConfigOptionType = ConfigOptionType
|
||||
|
||||
// generated dynamically
|
||||
public configForm = new FormGroup({})
|
||||
|
||||
public errors = {}
|
||||
|
||||
get optionCategories(): string[] {
|
||||
return Object.values(ConfigCategory)
|
||||
}
|
||||
|
||||
getCategoryOptions(category: string): ConfigOption[] {
|
||||
return PaperlessConfigOptions.filter((o) => o.category === category)
|
||||
}
|
||||
|
||||
public loading: boolean = false
|
||||
|
||||
initialConfig: PaperlessConfig
|
||||
store: BehaviorSubject<any>
|
||||
storeSub: Subscription
|
||||
isDirty$: Observable<boolean>
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService
|
||||
) {
|
||||
super()
|
||||
this.configForm.addControl('id', new FormControl())
|
||||
PaperlessConfigOptions.forEach((option) => {
|
||||
this.configForm.addControl(option.key, new FormControl())
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true
|
||||
this.configService
|
||||
.getConfig()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (config) => {
|
||||
this.loading = false
|
||||
this.initialize(config)
|
||||
},
|
||||
error: (e) => {
|
||||
this.loading = false
|
||||
this.toastService.showError($localize`Error retrieving config`, e)
|
||||
},
|
||||
})
|
||||
|
||||
// validate JSON inputs
|
||||
PaperlessConfigOptions.filter(
|
||||
(o) => o.type === ConfigOptionType.JSON
|
||||
).forEach((option) => {
|
||||
this.configForm
|
||||
.get(option.key)
|
||||
.addValidators((control: AbstractControl) => {
|
||||
if (!control.value || control.value.toString().length === 0)
|
||||
return null
|
||||
try {
|
||||
JSON.parse(control.value)
|
||||
} catch (e) {
|
||||
return [
|
||||
{
|
||||
user_args: e,
|
||||
},
|
||||
]
|
||||
}
|
||||
return null
|
||||
})
|
||||
this.configForm.get(option.key).statusChanges.subscribe((status) => {
|
||||
this.errors[option.key] =
|
||||
status === 'INVALID' ? $localize`Invalid JSON` : null
|
||||
})
|
||||
this.configForm.get(option.key).updateValueAndValidity()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribeNotifier.next(true)
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
private initialize(config: PaperlessConfig) {
|
||||
if (!this.store) {
|
||||
this.store = new BehaviorSubject(config)
|
||||
|
||||
this.store
|
||||
.asObservable()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((state) => {
|
||||
this.configForm.patchValue(state, { emitEvent: false })
|
||||
})
|
||||
|
||||
this.isDirty$ = dirtyCheck(this.configForm, this.store.asObservable())
|
||||
}
|
||||
this.configForm.patchValue(config)
|
||||
|
||||
this.initialConfig = config
|
||||
}
|
||||
|
||||
getDocsUrl(key: string) {
|
||||
return `https://docs.paperless-ngx.com/configuration/#${key}`
|
||||
}
|
||||
|
||||
public saveConfig() {
|
||||
this.loading = true
|
||||
this.configService
|
||||
.saveConfig(this.configForm.value as PaperlessConfig)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier), first())
|
||||
.subscribe({
|
||||
next: (config) => {
|
||||
this.loading = false
|
||||
this.initialize(config)
|
||||
this.store.next(config)
|
||||
this.toastService.showInfo($localize`Configuration updated`)
|
||||
},
|
||||
error: (e) => {
|
||||
this.loading = false
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred updating configuration`,
|
||||
e
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public discardChanges() {
|
||||
this.configForm.reset(this.initialConfig)
|
||||
}
|
||||
}
|
||||
@@ -363,7 +363,7 @@ export class SettingsComponent
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didnt save
|
||||
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didn't save
|
||||
this.storeSub && this.storeSub.unsubscribe()
|
||||
this.settings.organizingSidebarSavedViews = false
|
||||
}
|
||||
|
||||
@@ -235,14 +235,14 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }"
|
||||
tourAnchor="tour.consumption-templates">
|
||||
<a class="nav-link" routerLink="templates" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Consumption templates" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Workflow }"
|
||||
tourAnchor="tour.workflows">
|
||||
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-earmark-ruled" />
|
||||
</svg><span> <ng-container i18n>Templates</ng-container></span>
|
||||
<use xlink:href="assets/bootstrap-icons.svg#boxes" />
|
||||
</svg><span> <ng-container i18n>Workflows</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
|
||||
@@ -271,6 +271,15 @@
|
||||
</svg><span> <ng-container i18n>Settings</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||
<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">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sliders2-vertical" />
|
||||
</svg><span> <ng-container i18n>Configuration</ng-container></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
|
||||
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
|
||||
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('AppFrameComponent', () => {
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support collapsable menu', () => {
|
||||
it('should support collapsible menu', () => {
|
||||
const button: HTMLButtonElement = (
|
||||
fixture.nativeElement as HTMLDivElement
|
||||
).querySelector('button[data-toggle=collapse]')
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h5 class="border-bottom pb-2" i18n>Filters</h5>
|
||||
<p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p>
|
||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="border-bottom pb-2" i18n>Assignments</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#consumption-templates'>documentation</a>." [error]="error?.assign_title"></pngx-input-text>
|
||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
|
||||
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
|
||||
<div>
|
||||
<label class="form-label" i18n>Assign view permissions</label>
|
||||
<div class="mb-2">
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label" i18n>Assign edit permissions</label>
|
||||
<div>
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@if (error?.non_field_errors) {
|
||||
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormGroup, FormControl } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import {
|
||||
DocumentSource,
|
||||
ConsumptionTemplate,
|
||||
} from 'src/app/data/consumption-template'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { EditDialogComponent } from '../edit-dialog.component'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { MailRule } from 'src/app/data/mail-rule'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
|
||||
export const DOCUMENT_SOURCE_OPTIONS = [
|
||||
{
|
||||
id: DocumentSource.ConsumeFolder,
|
||||
name: $localize`Consume Folder`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.ApiUpload,
|
||||
name: $localize`API Upload`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.MailFetch,
|
||||
name: $localize`Mail Fetch`,
|
||||
},
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-consumption-template-edit-dialog',
|
||||
templateUrl: './consumption-template-edit-dialog.component.html',
|
||||
styleUrls: ['./consumption-template-edit-dialog.component.scss'],
|
||||
})
|
||||
export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<ConsumptionTemplate> {
|
||||
templates: ConsumptionTemplate[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
storagePaths: StoragePath[]
|
||||
mailRules: MailRule[]
|
||||
customFields: CustomField[]
|
||||
|
||||
constructor(
|
||||
service: ConsumptionTemplateService,
|
||||
activeModal: NgbActiveModal,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService,
|
||||
storagePathService: StoragePathService,
|
||||
mailRuleService: MailRuleService,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService,
|
||||
customFieldsService: CustomFieldsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
|
||||
correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
|
||||
storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
|
||||
mailRuleService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.mailRules = result.results))
|
||||
|
||||
customFieldsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new consumption template`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit consumption template`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(null),
|
||||
account: new FormControl(null),
|
||||
filter_filename: new FormControl(null),
|
||||
filter_path: new FormControl(null),
|
||||
filter_mailrule: new FormControl(null),
|
||||
order: new FormControl(null),
|
||||
sources: new FormControl([]),
|
||||
assign_title: new FormControl(null),
|
||||
assign_tags: new FormControl([]),
|
||||
assign_owner: new FormControl(null),
|
||||
assign_document_type: new FormControl(null),
|
||||
assign_correspondent: new FormControl(null),
|
||||
assign_storage_path: new FormControl(null),
|
||||
assign_view_users: new FormControl([]),
|
||||
assign_view_groups: new FormControl([]),
|
||||
assign_change_users: new FormControl([]),
|
||||
assign_change_groups: new FormControl([]),
|
||||
assign_custom_fields: new FormControl([]),
|
||||
})
|
||||
}
|
||||
|
||||
get sourceOptions() {
|
||||
return DOCUMENT_SOURCE_OPTIONS
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export abstract class EditDialogComponent<
|
||||
})
|
||||
}
|
||||
|
||||
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
|
||||
// wait to enable close button so it doesn't steal focus from input since its the first clickable element in the DOM
|
||||
setTimeout(() => {
|
||||
this.closeEnabled = true
|
||||
})
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-switch i18n-title title="Enabled" formControlName="enabled" [error]="error?.enabled"></pngx-input-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div ngbAccordion>
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button ngbAccordionButton i18n>Triggers</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="d-flex">
|
||||
<p class="p-2" i18n>Trigger Workflow On:</p>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addTrigger()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Trigger</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordion [closeOthers]="true">
|
||||
@for (trigger of object?.triggers; track trigger; let i = $index){
|
||||
<div ngbAccordionItem>
|
||||
<div ngbAccordionHeader>
|
||||
<button ngbAccordionButton>{{i + 1}}. {{getTriggerTypeOptionName(triggerFields.controls[i].value.type)}}
|
||||
@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)">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template [ngTemplateOutlet]="triggerForm" [ngTemplateOutletContext]="{ formGroup: triggerFields.controls[i], trigger: trigger }"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button class="btn-lg" ngbAccordionButton i18n>Actions</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<div class="d-flex">
|
||||
<p class="p-2" i18n>Apply Actions:</p>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Action</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
|
||||
@for (action of object?.actions; track action; let i = $index){
|
||||
<div ngbAccordionItem cdkDrag [formGroup]="actionFields.controls[i]">
|
||||
<div ngbAccordionHeader>
|
||||
<button ngbAccordionButton>{{i + 1}}. {{getActionTypeOptionName(actionFields.controls[i].value.type)}}
|
||||
@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)">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
|
||||
<input type="hidden" formControlName="id" />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
|
||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
|
||||
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
|
||||
<div>
|
||||
<label class="form-label" i18n>Assign view permissions</label>
|
||||
<div class="mb-2">
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label" i18n>Assign edit permissions</label>
|
||||
<div>
|
||||
<div class="row mb-1">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@if (error?.non_field_errors) {
|
||||
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template #triggerForm let-formGroup="formGroup" let-trigger="trigger">
|
||||
<div [formGroup]="formGroup">
|
||||
<input type="hidden" formControlName="id" />
|
||||
<pngx-input-select i18n-title title="Trigger type" [horizontal]="true" [items]="triggerTypeOptions" formControlName="type"></pngx-input-select>
|
||||
<p class="small" i18n>Trigger for documents that match <em>all</em> filters specified below.</p>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
|
||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a>" [error]="error?.filter_path"></pngx-input-text>
|
||||
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
||||
}
|
||||
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
||||
<pngx-input-select i18n-title title="Content matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
<pngx-input-text i18n-title title="Content matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||
}
|
||||
@if (patternRequired) {
|
||||
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
||||
<div class="col-md-6">
|
||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Has tags" formControlName="filter_has_tags"></pngx-input-tags>
|
||||
<pngx-input-select i18n-title title="Has correspondent" [items]="correspondents" [allowNull]="true" formControlName="filter_has_correspondent"></pngx-input-select>
|
||||
<pngx-input-select i18n-title title="Has document type" [items]="documentTypes" [allowNull]="true" formControlName="filter_has_document_type"></pngx-input-select>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,5 @@
|
||||
.btn.text-danger {
|
||||
&:hover, &:focus {
|
||||
color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;
|
||||
}
|
||||
}
|
||||
@@ -18,24 +18,69 @@ import { PermissionsUserComponent } from '../../input/permissions/permissions-us
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TagsComponent } from '../../input/tags/tags.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { SwitchComponent } from '../../input/switch/switch.component'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { ConsumptionTemplateEditDialogComponent } from './consumption-template-edit-dialog.component'
|
||||
import {
|
||||
DOCUMENT_SOURCE_OPTIONS,
|
||||
WORKFLOW_ACTION_OPTIONS,
|
||||
WORKFLOW_TYPE_OPTIONS,
|
||||
WorkflowEditDialogComponent,
|
||||
} from './workflow-edit-dialog.component'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import {
|
||||
WorkflowTriggerType,
|
||||
DocumentSource,
|
||||
} from 'src/app/data/workflow-trigger'
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop'
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/app/data/workflow-action'
|
||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'
|
||||
|
||||
describe('ConsumptionTemplateEditDialogComponent', () => {
|
||||
let component: ConsumptionTemplateEditDialogComponent
|
||||
const workflow: Workflow = {
|
||||
name: 'Workflow 1',
|
||||
id: 1,
|
||||
order: 1,
|
||||
enabled: true,
|
||||
triggers: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowTriggerType.Consumption,
|
||||
sources: [DocumentSource.ConsumeFolder],
|
||||
filter_filename: '*',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: 'foo',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_owner: 2,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('WorkflowEditDialogComponent', () => {
|
||||
let component: WorkflowEditDialogComponent
|
||||
let settingsService: SettingsService
|
||||
let fixture: ComponentFixture<ConsumptionTemplateEditDialogComponent>
|
||||
let fixture: ComponentFixture<WorkflowEditDialogComponent>
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ConsumptionTemplateEditDialogComponent,
|
||||
WorkflowEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
NumberComponent,
|
||||
SwitchComponent,
|
||||
TagsComponent,
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
@@ -113,7 +158,7 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ConsumptionTemplateEditDialogComponent)
|
||||
fixture = TestBed.createComponent(WorkflowEditDialogComponent)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 99, username: 'user99' }
|
||||
component = fixture.componentInstance
|
||||
@@ -121,15 +166,71 @@ describe('ConsumptionTemplateEditDialogComponent', () => {
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
it('should support create and edit modes, support adding triggers and actions on new workflow', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
expect(component.object).toBeUndefined()
|
||||
component.addAction()
|
||||
expect(component.object).not.toBeUndefined()
|
||||
expect(component.object.actions).toHaveLength(1)
|
||||
component.object = undefined
|
||||
component.addTrigger()
|
||||
expect(component.object).not.toBeUndefined()
|
||||
expect(component.object.triggers).toHaveLength(1)
|
||||
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return source options, type options, type name', () => {
|
||||
// coverage
|
||||
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
|
||||
expect(component.triggerTypeOptions).toEqual(WORKFLOW_TYPE_OPTIONS)
|
||||
expect(
|
||||
component.getTriggerTypeOptionName(WorkflowTriggerType.DocumentAdded)
|
||||
).toEqual('Document Added')
|
||||
expect(component.getTriggerTypeOptionName(null)).toEqual('')
|
||||
expect(component.sourceOptions).toEqual(DOCUMENT_SOURCE_OPTIONS)
|
||||
expect(component.actionTypeOptions).toEqual(WORKFLOW_ACTION_OPTIONS)
|
||||
expect(
|
||||
component.getActionTypeOptionName(WorkflowActionType.Assignment)
|
||||
).toEqual('Assignment')
|
||||
expect(component.getActionTypeOptionName(null)).toEqual('')
|
||||
})
|
||||
|
||||
it('should support add and remove triggers and actions', () => {
|
||||
component.object = workflow
|
||||
component.addTrigger()
|
||||
expect(component.object.triggers.length).toEqual(2)
|
||||
component.addAction()
|
||||
expect(component.object.actions.length).toEqual(3)
|
||||
component.removeTrigger(1)
|
||||
expect(component.object.triggers.length).toEqual(1)
|
||||
component.removeAction(1)
|
||||
expect(component.object.actions.length).toEqual(2)
|
||||
})
|
||||
|
||||
it('should update order and remove ids from actions on drag n drop', () => {
|
||||
const action1 = workflow.actions[0]
|
||||
const action2 = workflow.actions[1]
|
||||
component.object = workflow
|
||||
component.ngOnInit()
|
||||
component.onActionDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
|
||||
WorkflowAction[]
|
||||
>)
|
||||
expect(component.object.actions).toEqual([action2, action1])
|
||||
expect(action1.id).toBeNull()
|
||||
expect(action2.id).toBeNull()
|
||||
})
|
||||
|
||||
it('should not include auto matching in algorithms', () => {
|
||||
expect(component.getMatchingAlgorithms()).not.toContain(
|
||||
MATCHING_ALGORITHMS.find((a) => a.id === MATCH_AUTO)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,320 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { FormGroup, FormControl, FormArray } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { first } from 'rxjs'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { EditDialogComponent } from '../edit-dialog.component'
|
||||
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||
import { MailRule } from 'src/app/data/mail-rule'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import {
|
||||
DocumentSource,
|
||||
WorkflowTrigger,
|
||||
WorkflowTriggerType,
|
||||
} from 'src/app/data/workflow-trigger'
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/app/data/workflow-action'
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'
|
||||
import {
|
||||
MATCHING_ALGORITHMS,
|
||||
MATCH_AUTO,
|
||||
MATCH_NONE,
|
||||
} from 'src/app/data/matching-model'
|
||||
|
||||
export const DOCUMENT_SOURCE_OPTIONS = [
|
||||
{
|
||||
id: DocumentSource.ConsumeFolder,
|
||||
name: $localize`Consume Folder`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.ApiUpload,
|
||||
name: $localize`API Upload`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.MailFetch,
|
||||
name: $localize`Mail Fetch`,
|
||||
},
|
||||
]
|
||||
|
||||
export const WORKFLOW_TYPE_OPTIONS = [
|
||||
{
|
||||
id: WorkflowTriggerType.Consumption,
|
||||
name: $localize`Consumption Started`,
|
||||
},
|
||||
{
|
||||
id: WorkflowTriggerType.DocumentAdded,
|
||||
name: $localize`Document Added`,
|
||||
},
|
||||
{
|
||||
id: WorkflowTriggerType.DocumentUpdated,
|
||||
name: $localize`Document Updated`,
|
||||
},
|
||||
]
|
||||
|
||||
export const WORKFLOW_ACTION_OPTIONS = [
|
||||
{
|
||||
id: WorkflowActionType.Assignment,
|
||||
name: $localize`Assignment`,
|
||||
},
|
||||
]
|
||||
|
||||
const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
|
||||
(a) => a.id !== MATCH_AUTO
|
||||
)
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-workflow-edit-dialog',
|
||||
templateUrl: './workflow-edit-dialog.component.html',
|
||||
styleUrls: ['./workflow-edit-dialog.component.scss'],
|
||||
})
|
||||
export class WorkflowEditDialogComponent
|
||||
extends EditDialogComponent<Workflow>
|
||||
implements OnInit
|
||||
{
|
||||
public WorkflowTriggerType = WorkflowTriggerType
|
||||
|
||||
templates: Workflow[]
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
storagePaths: StoragePath[]
|
||||
mailRules: MailRule[]
|
||||
customFields: CustomField[]
|
||||
|
||||
expandedItem: number = null
|
||||
|
||||
constructor(
|
||||
service: WorkflowService,
|
||||
activeModal: NgbActiveModal,
|
||||
correspondentService: CorrespondentService,
|
||||
documentTypeService: DocumentTypeService,
|
||||
storagePathService: StoragePathService,
|
||||
mailRuleService: MailRuleService,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService,
|
||||
customFieldsService: CustomFieldsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
|
||||
correspondentService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
|
||||
storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
|
||||
mailRuleService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.mailRules = result.results))
|
||||
|
||||
customFieldsService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.customFields = result.results))
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new workflow`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit workflow`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(null),
|
||||
order: new FormControl(null),
|
||||
enabled: new FormControl(true),
|
||||
triggers: new FormArray([]),
|
||||
actions: new FormArray([]),
|
||||
})
|
||||
}
|
||||
|
||||
getMatchingAlgorithms() {
|
||||
// No auto matching
|
||||
return TRIGGER_MATCHING_ALGORITHMS
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit()
|
||||
this.updateAllTriggerActionFields()
|
||||
}
|
||||
|
||||
get triggerFields(): FormArray {
|
||||
return this.objectForm.get('triggers') as FormArray
|
||||
}
|
||||
|
||||
get actionFields(): FormArray {
|
||||
return this.objectForm.get('actions') as FormArray
|
||||
}
|
||||
|
||||
private createTriggerField(
|
||||
trigger: WorkflowTrigger,
|
||||
emitEvent: boolean = false
|
||||
) {
|
||||
this.triggerFields.push(
|
||||
new FormGroup({
|
||||
id: new FormControl(trigger.id),
|
||||
type: new FormControl(trigger.type),
|
||||
sources: new FormControl(trigger.sources),
|
||||
filter_filename: new FormControl(trigger.filter_filename),
|
||||
filter_path: new FormControl(trigger.filter_path),
|
||||
filter_mailrule: new FormControl(trigger.filter_mailrule),
|
||||
matching_algorithm: new FormControl(trigger.matching_algorithm),
|
||||
match: new FormControl(trigger.match),
|
||||
is_insensitive: new FormControl(trigger.is_insensitive),
|
||||
filter_has_tags: new FormControl(trigger.filter_has_tags),
|
||||
filter_has_correspondent: new FormControl(
|
||||
trigger.filter_has_correspondent
|
||||
),
|
||||
filter_has_document_type: new FormControl(
|
||||
trigger.filter_has_document_type
|
||||
),
|
||||
}),
|
||||
{ emitEvent }
|
||||
)
|
||||
}
|
||||
|
||||
private createActionField(
|
||||
action: WorkflowAction,
|
||||
emitEvent: boolean = false
|
||||
) {
|
||||
this.actionFields.push(
|
||||
new FormGroup({
|
||||
id: new FormControl(action.id),
|
||||
type: new FormControl(action.type),
|
||||
assign_title: new FormControl(action.assign_title),
|
||||
assign_tags: new FormControl(action.assign_tags),
|
||||
assign_owner: new FormControl(action.assign_owner),
|
||||
assign_document_type: new FormControl(action.assign_document_type),
|
||||
assign_correspondent: new FormControl(action.assign_correspondent),
|
||||
assign_storage_path: new FormControl(action.assign_storage_path),
|
||||
assign_view_users: new FormControl(action.assign_view_users),
|
||||
assign_view_groups: new FormControl(action.assign_view_groups),
|
||||
assign_change_users: new FormControl(action.assign_change_users),
|
||||
assign_change_groups: new FormControl(action.assign_change_groups),
|
||||
assign_custom_fields: new FormControl(action.assign_custom_fields),
|
||||
}),
|
||||
{ emitEvent }
|
||||
)
|
||||
}
|
||||
|
||||
private updateAllTriggerActionFields(emitEvent: boolean = false) {
|
||||
this.triggerFields.clear({ emitEvent: false })
|
||||
this.object?.triggers.forEach((trigger) => {
|
||||
this.createTriggerField(trigger, emitEvent)
|
||||
})
|
||||
|
||||
this.actionFields.clear({ emitEvent: false })
|
||||
this.object?.actions.forEach((action) => {
|
||||
this.createActionField(action, emitEvent)
|
||||
})
|
||||
}
|
||||
|
||||
get sourceOptions() {
|
||||
return DOCUMENT_SOURCE_OPTIONS
|
||||
}
|
||||
|
||||
get triggerTypeOptions() {
|
||||
return WORKFLOW_TYPE_OPTIONS
|
||||
}
|
||||
|
||||
getTriggerTypeOptionName(type: WorkflowTriggerType): string {
|
||||
return this.triggerTypeOptions.find((t) => t.id === type)?.name ?? ''
|
||||
}
|
||||
|
||||
addTrigger() {
|
||||
if (!this.object) {
|
||||
this.object = Object.assign({}, this.objectForm.value)
|
||||
}
|
||||
const trigger: WorkflowTrigger = {
|
||||
type: WorkflowTriggerType.Consumption,
|
||||
sources: [],
|
||||
filter_filename: null,
|
||||
filter_path: null,
|
||||
filter_mailrule: null,
|
||||
filter_has_tags: [],
|
||||
filter_has_correspondent: null,
|
||||
filter_has_document_type: null,
|
||||
matching_algorithm: MATCH_NONE,
|
||||
match: '',
|
||||
is_insensitive: true,
|
||||
}
|
||||
this.object.triggers.push(trigger)
|
||||
this.createTriggerField(trigger)
|
||||
}
|
||||
|
||||
get actionTypeOptions() {
|
||||
return WORKFLOW_ACTION_OPTIONS
|
||||
}
|
||||
|
||||
getActionTypeOptionName(type: WorkflowActionType): string {
|
||||
return this.actionTypeOptions.find((t) => t.id === type)?.name ?? ''
|
||||
}
|
||||
|
||||
addAction() {
|
||||
if (!this.object) {
|
||||
this.object = Object.assign({}, this.objectForm.value)
|
||||
}
|
||||
const action: WorkflowAction = {
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: null,
|
||||
assign_tags: [],
|
||||
assign_document_type: null,
|
||||
assign_correspondent: null,
|
||||
assign_storage_path: null,
|
||||
assign_owner: null,
|
||||
assign_view_users: [],
|
||||
assign_view_groups: [],
|
||||
assign_change_users: [],
|
||||
assign_change_groups: [],
|
||||
assign_custom_fields: [],
|
||||
}
|
||||
this.object.actions.push(action)
|
||||
this.createActionField(action)
|
||||
}
|
||||
|
||||
removeTrigger(index: number) {
|
||||
this.object.triggers.splice(index, 1).pop()
|
||||
this.triggerFields.removeAt(index)
|
||||
}
|
||||
|
||||
removeAction(index: number) {
|
||||
this.object.actions.splice(index, 1)
|
||||
this.actionFields.removeAt(index)
|
||||
}
|
||||
|
||||
onActionDrop(event: CdkDragDrop<WorkflowAction[]>) {
|
||||
moveItemInArray(
|
||||
this.object.actions,
|
||||
event.previousIndex,
|
||||
event.currentIndex
|
||||
)
|
||||
const actionField = this.actionFields.at(event.previousIndex)
|
||||
this.actionFields.removeAt(event.previousIndex)
|
||||
this.actionFields.insert(event.currentIndex, actionField)
|
||||
// removing id will effectively re-create the actions in this order
|
||||
this.object.actions.forEach((a) => (a.id = null))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
@if (title) {
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
}
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('NumberComponent', () => {
|
||||
component.step = 0.1
|
||||
component.writeValue(12.3456)
|
||||
expect(component.value).toEqual(12.3456)
|
||||
// float (step = .1) doesnt force 2 decimals
|
||||
// float (step = .1) doesn't force 2 decimals
|
||||
component.writeValue(11.1)
|
||||
expect(component.value).toEqual(11.1)
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('PasswordComponent', () => {
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// TODO: why doesn't this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.accordion {
|
||||
--bs-accordion-btn-padding-x: 0.75rem;
|
||||
--bs-accordion-btn-padding-y: 0.375rem;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
(clear)="clearLastSearchTerm()"
|
||||
(blur)="onBlur()">
|
||||
<ng-template ng-option-tmp let-item="item">
|
||||
<span [title]="item.name">{{item.name}}</span>
|
||||
<span [title]="item[bindLabel]">{{item[bindLabel]}}</span>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
@if (allowCreateNew) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<div class="mb-3">
|
||||
<div class="row">
|
||||
@if (!horizontal) {
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container col-md-3">
|
||||
<label class="form-label" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
|
||||
{{title}}
|
||||
@if (showUnsetNote && isUnset) {
|
||||
<svg class="sidebaricon-sm ms-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/>
|
||||
</svg>
|
||||
}
|
||||
</label>
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||
</svg> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div [ngClass]="{'col-md-9': horizontal, '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) {
|
||||
<label class="form-check-label" [class.text-muted]="showUnsetNote && isUnset" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
|
||||
{{title}}
|
||||
@if (showUnsetNote && isUnset) {
|
||||
<svg class="sidebaricon-sm ms-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#exclamation-triangle"/>
|
||||
</svg>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
@if (hint) {
|
||||
<div class="form-text text-muted">{{hint}}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ng-template #tipContent>
|
||||
<span class="text-light fst-italic" i18n>Note: value has not yet been set and will not apply until explicitly changed</span>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { SwitchComponent } from './switch.component'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
describe('SwitchComponent', () => {
|
||||
let component: SwitchComponent
|
||||
let fixture: ComponentFixture<SwitchComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SwitchComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgbTooltipModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(SwitchComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of checkbox', () => {
|
||||
input.checked = true
|
||||
input.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toBeTruthy()
|
||||
|
||||
input.checked = false
|
||||
input.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should show note if unset', () => {
|
||||
component.value = null
|
||||
expect(component.isUnset).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Component, Input, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SwitchComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'pngx-input-switch',
|
||||
templateUrl: './switch.component.html',
|
||||
styleUrls: ['./switch.component.scss'],
|
||||
})
|
||||
export class SwitchComponent extends AbstractInputComponent<boolean> {
|
||||
@Input()
|
||||
showUnsetNote: boolean = false
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
get isUnset(): boolean {
|
||||
return this.value === null || this.value === undefined
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
@if (title) {
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
}
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('TextComponent', () => {
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// TODO: why doesn't this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('TextComponent', () => {
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// TODO: why doesn't this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
|
||||
@@ -123,28 +123,73 @@
|
||||
<div [formGroup]="customFieldFormFields.controls[i]">
|
||||
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {
|
||||
@case (PaperlessCustomFieldDataType.String) {
|
||||
<pngx-input-text formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-text>
|
||||
<pngx-input-text formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-text>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Date) {
|
||||
<pngx-input-date formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-date>
|
||||
<pngx-input-date formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-date>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Integer) {
|
||||
<pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Float) {
|
||||
<pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".1" [error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[step]=".1"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Monetary) {
|
||||
<pngx-input-number formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".01" [error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
<pngx-input-number formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[showAdd]="false"
|
||||
[step]=".01"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-number>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Boolean) {
|
||||
<pngx-input-check formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true"></pngx-input-check>
|
||||
<pngx-input-check formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"></pngx-input-check>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.Url) {
|
||||
<pngx-input-url formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-url>
|
||||
<pngx-input-url formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-url>
|
||||
}
|
||||
@case (PaperlessCustomFieldDataType.DocumentLink) {
|
||||
<pngx-input-document-link formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [parentDocumentID]="documentId" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-document-link>
|
||||
<pngx-input-document-link formControlName="value"
|
||||
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
|
||||
[parentDocumentID]="documentId"
|
||||
[removable]="userIsOwner"
|
||||
(removed)="removeField(fieldInstance)"
|
||||
[horizontal]="true"
|
||||
[error]="getCustomFieldError(i)"></pngx-input-document-link>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('DocumentDetailComponent', () => {
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
|
||||
it('should update title before doc change if wasnt updated via debounce', fakeAsync(() => {
|
||||
it('should update title before doc change if was not updated via debounce', fakeAsync(() => {
|
||||
initNormally()
|
||||
component.titleInput.value = 'Foo Bar'
|
||||
component.titleInput.inputField.nativeElement.dispatchEvent(
|
||||
|
||||
@@ -157,7 +157,7 @@ export class DocumentDetailComponent
|
||||
|
||||
@ViewChild('nav') nav: NgbNav
|
||||
@ViewChild('pdfPreview') set pdfPreview(element) {
|
||||
// this gets called when compontent added or removed from DOM
|
||||
// this gets called when component added or removed from DOM
|
||||
if (
|
||||
element &&
|
||||
element.nativeElement.offsetParent !== null &&
|
||||
@@ -316,7 +316,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: (titleValue) => {
|
||||
// In the rare case when the field changed just after debounced event was fired.
|
||||
// We dont want to overwrite whats actually in the text field, so just return
|
||||
// We dont want to overwrite what's actually in the text field, so just return
|
||||
if (titleValue !== this.titleInput.value) return
|
||||
|
||||
this.title = titleValue
|
||||
|
||||
@@ -82,7 +82,7 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||
// only show notes with a match
|
||||
highlights = (this.document['__search_hit__'].note_highlights as string)
|
||||
.split(',')
|
||||
.filter((higlight) => higlight.includes('<span'))
|
||||
.filter((highlight) => highlight.includes('<span'))
|
||||
}
|
||||
return highlights
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ describe('FilterEditorComponent', () => {
|
||||
expect(component.textFilterTarget).toEqual('fulltext-morelike') // TEXT_FILTER_TARGET_FULLTEXT_MORELIKE
|
||||
expect(moreLikeSpy).toHaveBeenCalledWith(1)
|
||||
expect(component.textFilter).toEqual('Foo Bar')
|
||||
// we have to do this here because it cant be done by user input
|
||||
// we have to do this here because it can't be done by user input
|
||||
expect(component.filterRules).toEqual([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_MORELIKE,
|
||||
@@ -1264,7 +1264,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
dateCreatedAfter.nativeElement.value = '05/14/2023'
|
||||
// dateCreatedAfter.triggerEventHandler('change')
|
||||
// TODO: why isnt ngModel triggering this on change?
|
||||
// TODO: why isn't ngModel triggering this on change?
|
||||
component.dateCreatedAfter = '2023-05-14'
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
@@ -1284,7 +1284,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
dateCreatedBefore.nativeElement.value = '05/14/2023'
|
||||
// dateCreatedBefore.triggerEventHandler('change')
|
||||
// TODO: why isnt ngModel triggering this on change?
|
||||
// TODO: why isn't ngModel triggering this on change?
|
||||
component.dateCreatedBefore = '2023-05-14'
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
@@ -1341,7 +1341,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
dateAddedAfter.nativeElement.value = '05/14/2023'
|
||||
// dateAddedAfter.triggerEventHandler('change')
|
||||
// TODO: why isnt ngModel triggering this on change?
|
||||
// TODO: why isn't ngModel triggering this on change?
|
||||
component.dateAddedAfter = '2023-05-14'
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
@@ -1361,7 +1361,7 @@ describe('FilterEditorComponent', () => {
|
||||
|
||||
dateAddedBefore.nativeElement.value = '05/14/2023'
|
||||
// dateAddedBefore.triggerEventHandler('change')
|
||||
// TODO: why isnt ngModel triggering this on change?
|
||||
// TODO: why isn't ngModel triggering this on change?
|
||||
component.dateAddedBefore = '2023-05-14'
|
||||
fixture.detectChanges()
|
||||
tick(400)
|
||||
@@ -1524,7 +1524,7 @@ describe('FilterEditorComponent', () => {
|
||||
)
|
||||
ownerToggle.nativeElement.checked = true
|
||||
// ownerToggle.triggerEventHandler('change')
|
||||
// TODO: ngModel isnt doing this here
|
||||
// TODO: ngModel isn't doing this here
|
||||
component.permissionsSelectionModel.hideUnowned = true
|
||||
fixture.detectChanges()
|
||||
expect(component.filterRules).toEqual([
|
||||
|
||||
@@ -40,7 +40,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
|
||||
})
|
||||
|
||||
ngOnInit(): void {
|
||||
// wait to enable close button so it doesnt steal focus from input since its the first clickable element in the DOM
|
||||
// wait to enable close button so it doesn't steal focus from input since its the first clickable element in the DOM
|
||||
setTimeout(() => {
|
||||
this.closeEnabled = true
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('CorrespondentListComponent', () => {
|
||||
correspondentsService = TestBed.inject(CorrespondentService)
|
||||
})
|
||||
|
||||
// Tests are included in management-list.compontent.spec.ts
|
||||
// Tests are included in management-list.component.spec.ts
|
||||
|
||||
it('should use correct delete message', () => {
|
||||
jest.spyOn(correspondentsService, 'listFiltered').mockReturnValue(
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('DocumentTypeListComponent', () => {
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
// Tests are included in management-list.compontent.spec.ts
|
||||
// Tests are included in management-list.component.spec.ts
|
||||
|
||||
it('should use correct delete message', () => {
|
||||
expect(
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('StoragePathListComponent', () => {
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
// Tests are included in management-list.compontent.spec.ts
|
||||
// Tests are included in management-list.component.spec.ts
|
||||
|
||||
it('should use correct delete message', () => {
|
||||
expect(component.getDeleteMessage({ id: 1, name: 'StoragePath1' })).toEqual(
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('TagListComponent', () => {
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
// Tests are included in management-list.compontent.spec.ts
|
||||
// Tests are included in management-list.component.spec.ts
|
||||
|
||||
it('should use correct delete message', () => {
|
||||
expect(component.getDeleteMessage({ id: 1, name: 'Tag1' })).toEqual(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<pngx-page-header title="Consumption Templates" i18n-title>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ConsumptionTemplate }">
|
||||
<pngx-page-header title="Workflows" i18n-title>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editWorkflow()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Workflow }">
|
||||
<svg class="sidebaricon me-1" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg>
|
||||
<ng-container i18n>Add Template</ng-container>
|
||||
<ng-container i18n>Add Workflow</ng-container>
|
||||
</button>
|
||||
</pngx-page-header>
|
||||
|
||||
@@ -13,25 +13,27 @@
|
||||
<div class="row">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Sort order</div>
|
||||
<div class="col" i18n>Document Sources</div>
|
||||
<div class="col" i18n>Status</div>
|
||||
<div class="col" i18n>Triggers</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@for (template of templates; track template) {
|
||||
@for (workflow of workflows; track workflow.id) {
|
||||
<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)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><code>{{template.order}}</code></div>
|
||||
<div class="col d-flex align-items-center">{{getSourceList(template)}}</div>
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editWorkflow(workflow)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Workflow)">{{workflow.name}}</button></div>
|
||||
<div class="col d-flex align-items-center"><code>{{workflow.order}}</code></div>
|
||||
<div class="col d-flex align-items-center"><code> @if(workflow.enabled) { <ng-container i18n>Enabled</ng-container> } @else { <span i18n class="text-muted">Disabled</span> }</code></div>
|
||||
<div class="col d-flex align-items-center">{{getTypesList(workflow)}}</div>
|
||||
<div class="col">
|
||||
<div class="btn-group">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editTemplate(template)">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editWorkflow(workflow)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)">
|
||||
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteWorkflow(workflow)">
|
||||
<svg class="buttonicon-sm" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg> <ng-container i18n>Delete</ng-container>
|
||||
@@ -41,7 +43,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (templates.length === 0) {
|
||||
<li class="list-group-item" i18n>No templates defined.</li>
|
||||
@if (workflows.length === 0) {
|
||||
<li class="list-group-item" i18n>No workflows defined.</li>
|
||||
}
|
||||
</ul>
|
||||
@@ -9,55 +9,76 @@ import {
|
||||
NgbModalModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import {
|
||||
DocumentSource,
|
||||
ConsumptionTemplate,
|
||||
} from 'src/app/data/consumption-template'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||
import { ConsumptionTemplatesComponent } from './consumption-templates.component'
|
||||
import { ConsumptionTemplateEditDialogComponent } from '../../common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component'
|
||||
import { WorkflowsComponent } from './workflows.component'
|
||||
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import {
|
||||
DocumentSource,
|
||||
WorkflowTriggerType,
|
||||
} from 'src/app/data/workflow-trigger'
|
||||
import { WorkflowActionType } from 'src/app/data/workflow-action'
|
||||
|
||||
const templates: ConsumptionTemplate[] = [
|
||||
const workflows: Workflow[] = [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Template 1',
|
||||
order: 0,
|
||||
sources: [
|
||||
DocumentSource.ConsumeFolder,
|
||||
DocumentSource.ApiUpload,
|
||||
DocumentSource.MailFetch,
|
||||
name: 'Workflow 1',
|
||||
id: 1,
|
||||
order: 1,
|
||||
enabled: true,
|
||||
triggers: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowTriggerType.Consumption,
|
||||
sources: [DocumentSource.ConsumeFolder],
|
||||
filter_filename: '*',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: 'foo',
|
||||
},
|
||||
],
|
||||
filter_filename: 'foo',
|
||||
filter_path: 'bar',
|
||||
assign_tags: [1, 2, 3],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Template 2',
|
||||
order: 1,
|
||||
sources: [DocumentSource.MailFetch],
|
||||
filter_filename: null,
|
||||
filter_path: 'foo/bar',
|
||||
assign_owner: 1,
|
||||
name: 'Workflow 2',
|
||||
id: 2,
|
||||
order: 2,
|
||||
enabled: true,
|
||||
triggers: [
|
||||
{
|
||||
id: 2,
|
||||
type: WorkflowTriggerType.DocumentAdded,
|
||||
filter_filename: 'foo',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 2,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: 'bar',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
describe('ConsumptionTemplatesComponent', () => {
|
||||
let component: ConsumptionTemplatesComponent
|
||||
let fixture: ComponentFixture<ConsumptionTemplatesComponent>
|
||||
let consumptionTemplateService: ConsumptionTemplateService
|
||||
describe('WorkflowsComponent', () => {
|
||||
let component: WorkflowsComponent
|
||||
let fixture: ComponentFixture<WorkflowsComponent>
|
||||
let workflowService: WorkflowService
|
||||
let modalService: NgbModal
|
||||
let toastService: ToastService
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
ConsumptionTemplatesComponent,
|
||||
WorkflowsComponent,
|
||||
IfPermissionsDirective,
|
||||
PageHeaderComponent,
|
||||
ConfirmDialogComponent,
|
||||
@@ -81,18 +102,18 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
],
|
||||
})
|
||||
|
||||
consumptionTemplateService = TestBed.inject(ConsumptionTemplateService)
|
||||
jest.spyOn(consumptionTemplateService, 'listAll').mockReturnValue(
|
||||
workflowService = TestBed.inject(WorkflowService)
|
||||
jest.spyOn(workflowService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
count: templates.length,
|
||||
all: templates.map((o) => o.id),
|
||||
results: templates,
|
||||
count: workflows.length,
|
||||
all: workflows.map((o) => o.id),
|
||||
results: workflows,
|
||||
})
|
||||
)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
|
||||
fixture = TestBed.createComponent(ConsumptionTemplatesComponent)
|
||||
fixture = TestBed.createComponent(WorkflowsComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
@@ -108,8 +129,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
createButton.triggerEventHandler('click')
|
||||
|
||||
expect(modal).not.toBeUndefined()
|
||||
const editDialog =
|
||||
modal.componentInstance as ConsumptionTemplateEditDialogComponent
|
||||
const editDialog = modal.componentInstance as WorkflowEditDialogComponent
|
||||
|
||||
// fail first
|
||||
editDialog.failed.emit({ error: 'error creating item' })
|
||||
@@ -117,7 +137,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
expect(reloadSpy).not.toHaveBeenCalled()
|
||||
|
||||
// succeed
|
||||
editDialog.succeeded.emit(templates[0])
|
||||
editDialog.succeeded.emit(workflows[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalled()
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
@@ -133,9 +153,8 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
editButton.triggerEventHandler('click')
|
||||
|
||||
expect(modal).not.toBeUndefined()
|
||||
const editDialog =
|
||||
modal.componentInstance as ConsumptionTemplateEditDialogComponent
|
||||
expect(editDialog.object).toEqual(templates[0])
|
||||
const editDialog = modal.componentInstance as WorkflowEditDialogComponent
|
||||
expect(editDialog.object).toEqual(workflows[0])
|
||||
|
||||
// fail first
|
||||
editDialog.failed.emit({ error: 'error editing item' })
|
||||
@@ -143,7 +162,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
expect(reloadSpy).not.toHaveBeenCalled()
|
||||
|
||||
// succeed
|
||||
editDialog.succeeded.emit(templates[0])
|
||||
editDialog.succeeded.emit(workflows[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalled()
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
@@ -152,7 +171,7 @@ describe('ConsumptionTemplatesComponent', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const deleteSpy = jest.spyOn(consumptionTemplateService, 'delete')
|
||||
const deleteSpy = jest.spyOn(workflowService, 'delete')
|
||||
const reloadSpy = jest.spyOn(component, 'reload')
|
||||
|
||||
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[3]
|
||||
@@ -1,33 +1,33 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service'
|
||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { ConsumptionTemplate } from 'src/app/data/consumption-template'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import {
|
||||
ConsumptionTemplateEditDialogComponent,
|
||||
DOCUMENT_SOURCE_OPTIONS,
|
||||
} from '../../common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component'
|
||||
WorkflowEditDialogComponent,
|
||||
WORKFLOW_TYPE_OPTIONS,
|
||||
} from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-consumption-templates',
|
||||
templateUrl: './consumption-templates.component.html',
|
||||
styleUrls: ['./consumption-templates.component.scss'],
|
||||
selector: 'pngx-workflows',
|
||||
templateUrl: './workflows.component.html',
|
||||
styleUrls: ['./workflows.component.scss'],
|
||||
})
|
||||
export class ConsumptionTemplatesComponent
|
||||
export class WorkflowsComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit
|
||||
{
|
||||
public templates: ConsumptionTemplate[] = []
|
||||
public workflows: Workflow[] = []
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
constructor(
|
||||
private consumptionTemplateService: ConsumptionTemplateService,
|
||||
private workflowService: WorkflowService,
|
||||
public permissionsService: PermissionsService,
|
||||
private modalService: NgbModal,
|
||||
private toastService: ToastService
|
||||
@@ -40,68 +40,74 @@ export class ConsumptionTemplatesComponent
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.consumptionTemplateService
|
||||
this.workflowService
|
||||
.listAll()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((r) => {
|
||||
this.templates = r.results
|
||||
this.workflows = r.results
|
||||
})
|
||||
}
|
||||
|
||||
getSourceList(template: ConsumptionTemplate): string {
|
||||
return template.sources
|
||||
.map((id) => DOCUMENT_SOURCE_OPTIONS.find((s) => s.id === id).name)
|
||||
getTypesList(template: Workflow): string {
|
||||
return template.triggers
|
||||
.map(
|
||||
(trigger) =>
|
||||
WORKFLOW_TYPE_OPTIONS.find((t) => t.id === trigger.type).name
|
||||
)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
editTemplate(rule: ConsumptionTemplate) {
|
||||
const modal = this.modalService.open(
|
||||
ConsumptionTemplateEditDialogComponent,
|
||||
{
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
}
|
||||
)
|
||||
modal.componentInstance.dialogMode = rule
|
||||
editWorkflow(workflow: Workflow) {
|
||||
const modal = this.modalService.open(WorkflowEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode = workflow
|
||||
? EditDialogMode.EDIT
|
||||
: EditDialogMode.CREATE
|
||||
modal.componentInstance.object = rule
|
||||
if (workflow) {
|
||||
// quick "deep" clone so original doesn't get modified
|
||||
const clone = Object.assign({}, workflow)
|
||||
clone.actions = [...workflow.actions]
|
||||
clone.triggers = [...workflow.triggers]
|
||||
modal.componentInstance.object = clone
|
||||
}
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((newTemplate) => {
|
||||
.subscribe((newWorkflow) => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Saved template "${newTemplate.name}".`
|
||||
$localize`Saved workflow "${newWorkflow.name}".`
|
||||
)
|
||||
this.consumptionTemplateService.clearCache()
|
||||
this.workflowService.clearCache()
|
||||
this.reload()
|
||||
})
|
||||
modal.componentInstance.failed
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((e) => {
|
||||
this.toastService.showError($localize`Error saving template.`, e)
|
||||
this.toastService.showError($localize`Error saving workflow.`, e)
|
||||
})
|
||||
}
|
||||
|
||||
deleteTemplate(rule: ConsumptionTemplate) {
|
||||
deleteWorkflow(workflow: Workflow) {
|
||||
const modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm delete template`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this template.`
|
||||
modal.componentInstance.title = $localize`Confirm delete workflow`
|
||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete this workflow.`
|
||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||
modal.componentInstance.btnClass = 'btn-danger'
|
||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.consumptionTemplateService.delete(rule).subscribe({
|
||||
this.workflowService.delete(workflow).subscribe({
|
||||
next: () => {
|
||||
modal.close()
|
||||
this.toastService.showInfo($localize`Deleted template`)
|
||||
this.consumptionTemplateService.clearCache()
|
||||
this.toastService.showInfo($localize`Deleted workflow`)
|
||||
this.workflowService.clearCache()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error deleting template.`, e)
|
||||
this.toastService.showError($localize`Error deleting workflow.`, e)
|
||||
},
|
||||
})
|
||||
})
|
||||
183
src-ui/src/app/data/paperless-config.ts
Normal file
183
src-ui/src/app/data/paperless-config.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { ObjectWithId } from './object-with-id'
|
||||
|
||||
// see /src/paperless/models.py
|
||||
|
||||
export enum OutputTypeConfig {
|
||||
PDF = 'pdf',
|
||||
PDF_A = 'pdfa',
|
||||
PDF_A1 = 'pdfa-1',
|
||||
PDF_A2 = 'pdfa-2',
|
||||
PDF_A3 = 'pdfa-3',
|
||||
}
|
||||
|
||||
export enum ModeConfig {
|
||||
SKIP = 'skip',
|
||||
REDO = 'redo',
|
||||
FORCE = 'force',
|
||||
SKIP_NO_ARCHIVE = 'skip_noarchive',
|
||||
}
|
||||
|
||||
export enum ArchiveFileConfig {
|
||||
NEVER = 'never',
|
||||
WITH_TEXT = 'with_text',
|
||||
ALWAYS = 'always',
|
||||
}
|
||||
|
||||
export enum CleanConfig {
|
||||
CLEAN = 'clean',
|
||||
FINAL = 'clean-final',
|
||||
NONE = 'none',
|
||||
}
|
||||
|
||||
export enum ColorConvertConfig {
|
||||
UNCHANGED = 'LeaveColorUnchanged',
|
||||
RGB = 'RGB',
|
||||
INDEPENDENT = 'UseDeviceIndependentColor',
|
||||
GRAY = 'Gray',
|
||||
CMYK = 'CMYK',
|
||||
}
|
||||
|
||||
export enum ConfigOptionType {
|
||||
String = 'string',
|
||||
Number = 'number',
|
||||
Select = 'select',
|
||||
Boolean = 'boolean',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
export const ConfigCategory = {
|
||||
OCR: $localize`OCR Settings`,
|
||||
}
|
||||
|
||||
export interface ConfigOption {
|
||||
key: string
|
||||
title: string
|
||||
type: ConfigOptionType
|
||||
choices?: Array<{ id: string; name: string }>
|
||||
config_key?: string
|
||||
category: string
|
||||
}
|
||||
|
||||
function mapToItems(enumObj: Object): Array<{ id: string; name: string }> {
|
||||
return Object.keys(enumObj).map((key) => {
|
||||
return {
|
||||
id: enumObj[key],
|
||||
name: enumObj[key],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const PaperlessConfigOptions: ConfigOption[] = [
|
||||
{
|
||||
key: 'output_type',
|
||||
title: $localize`Output Type`,
|
||||
type: ConfigOptionType.Select,
|
||||
choices: mapToItems(OutputTypeConfig),
|
||||
config_key: 'PAPERLESS_OCR_OUTPUT_TYPE',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
title: $localize`Language`,
|
||||
type: ConfigOptionType.String,
|
||||
config_key: 'PAPERLESS_OCR_LANGUAGE',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'pages',
|
||||
title: $localize`Pages`,
|
||||
type: ConfigOptionType.Number,
|
||||
config_key: 'PAPERLESS_OCR_PAGES',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'mode',
|
||||
title: $localize`Mode`,
|
||||
type: ConfigOptionType.Select,
|
||||
choices: mapToItems(ModeConfig),
|
||||
config_key: 'PAPERLESS_OCR_MODE',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'skip_archive_file',
|
||||
title: $localize`Skip Archive File`,
|
||||
type: ConfigOptionType.Select,
|
||||
choices: mapToItems(ArchiveFileConfig),
|
||||
config_key: 'PAPERLESS_OCR_SKIP_ARCHIVE_FILE',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'image_dpi',
|
||||
title: $localize`Image DPI`,
|
||||
type: ConfigOptionType.Number,
|
||||
config_key: 'PAPERLESS_OCR_IMAGE_DPI',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'unpaper_clean',
|
||||
title: $localize`Clean`,
|
||||
type: ConfigOptionType.Select,
|
||||
choices: mapToItems(CleanConfig),
|
||||
config_key: 'PAPERLESS_OCR_CLEAN',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'deskew',
|
||||
title: $localize`Deskew`,
|
||||
type: ConfigOptionType.Boolean,
|
||||
config_key: 'PAPERLESS_OCR_DESKEW',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'rotate_pages',
|
||||
title: $localize`Rotate Pages`,
|
||||
type: ConfigOptionType.Boolean,
|
||||
config_key: 'PAPERLESS_OCR_ROTATE_PAGES',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'rotate_pages_threshold',
|
||||
title: $localize`Rotate Pages Threshold`,
|
||||
type: ConfigOptionType.Number,
|
||||
config_key: 'PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'max_image_pixels',
|
||||
title: $localize`Max Image Pixels`,
|
||||
type: ConfigOptionType.Number,
|
||||
config_key: 'PAPERLESS_OCR_MAX_IMAGE_PIXELS',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'color_conversion_strategy',
|
||||
title: $localize`Color Conversion Strategy`,
|
||||
type: ConfigOptionType.Select,
|
||||
choices: mapToItems(ColorConvertConfig),
|
||||
config_key: 'PAPERLESS_OCR_COLOR_CONVERSION_STRATEGY',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
{
|
||||
key: 'user_args',
|
||||
title: $localize`OCR Arguments`,
|
||||
type: ConfigOptionType.JSON,
|
||||
config_key: 'PAPERLESS_OCR_USER_ARGS',
|
||||
category: ConfigCategory.OCR,
|
||||
},
|
||||
]
|
||||
|
||||
export interface PaperlessConfig extends ObjectWithId {
|
||||
output_type: OutputTypeConfig
|
||||
pages: number
|
||||
language: string
|
||||
mode: ModeConfig
|
||||
skip_archive_file: ArchiveFileConfig
|
||||
image_dpi: number
|
||||
unpaper_clean: CleanConfig
|
||||
deskew: boolean
|
||||
rotate_pages: boolean
|
||||
rotate_pages_threshold: number
|
||||
max_image_pixels: number
|
||||
color_conversion_strategy: ColorConvertConfig
|
||||
user_args: object
|
||||
}
|
||||
@@ -1,23 +1,10 @@
|
||||
import { ObjectWithId } from './object-with-id'
|
||||
|
||||
export enum DocumentSource {
|
||||
ConsumeFolder = 1,
|
||||
ApiUpload = 2,
|
||||
MailFetch = 3,
|
||||
export enum WorkflowActionType {
|
||||
Assignment = 1,
|
||||
}
|
||||
|
||||
export interface ConsumptionTemplate extends ObjectWithId {
|
||||
name: string
|
||||
|
||||
order: number
|
||||
|
||||
sources: DocumentSource[]
|
||||
|
||||
filter_filename: string
|
||||
|
||||
filter_path?: string
|
||||
|
||||
filter_mailrule?: number // MailRule.id
|
||||
export interface WorkflowAction extends ObjectWithId {
|
||||
type: WorkflowActionType
|
||||
|
||||
assign_title?: string
|
||||
|
||||
37
src-ui/src/app/data/workflow-trigger.ts
Normal file
37
src-ui/src/app/data/workflow-trigger.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ObjectWithId } from './object-with-id'
|
||||
|
||||
export enum DocumentSource {
|
||||
ConsumeFolder = 1,
|
||||
ApiUpload = 2,
|
||||
MailFetch = 3,
|
||||
}
|
||||
|
||||
export enum WorkflowTriggerType {
|
||||
Consumption = 1,
|
||||
DocumentAdded = 2,
|
||||
DocumentUpdated = 3,
|
||||
}
|
||||
|
||||
export interface WorkflowTrigger extends ObjectWithId {
|
||||
type: WorkflowTriggerType
|
||||
|
||||
sources?: DocumentSource[]
|
||||
|
||||
filter_filename?: string
|
||||
|
||||
filter_path?: string
|
||||
|
||||
filter_mailrule?: number // MailRule.id
|
||||
|
||||
match?: string
|
||||
|
||||
matching_algorithm?: number
|
||||
|
||||
is_insensitive?: boolean
|
||||
|
||||
filter_has_tags?: number[] // Tag.id[]
|
||||
|
||||
filter_has_correspondent?: number // Correspondent.id
|
||||
|
||||
filter_has_document_type?: number // DocumentType.id
|
||||
}
|
||||
15
src-ui/src/app/data/workflow.ts
Normal file
15
src-ui/src/app/data/workflow.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ObjectWithId } from './object-with-id'
|
||||
import { WorkflowAction } from './workflow-action'
|
||||
import { WorkflowTrigger } from './workflow-trigger'
|
||||
|
||||
export interface Workflow extends ObjectWithId {
|
||||
name: string
|
||||
|
||||
order: number
|
||||
|
||||
enabled: boolean
|
||||
|
||||
triggers: WorkflowTrigger[]
|
||||
|
||||
actions: WorkflowAction[]
|
||||
}
|
||||
42
src-ui/src/app/services/config.service.spec.ts
Normal file
42
src-ui/src/app/services/config.service.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
|
||||
import { ConfigService } from './config.service'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { OutputTypeConfig, PaperlessConfig } from '../data/paperless-config'
|
||||
|
||||
describe('ConfigService', () => {
|
||||
let service: ConfigService
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
})
|
||||
service = TestBed.inject(ConfigService)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
})
|
||||
|
||||
it('should call correct API endpoint on get config', () => {
|
||||
service.getConfig().subscribe()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}config/`)
|
||||
.flush([{}])
|
||||
})
|
||||
|
||||
it('should call correct API endpoint on set config', () => {
|
||||
service
|
||||
.saveConfig({
|
||||
id: 1,
|
||||
output_type: OutputTypeConfig.PDF_A,
|
||||
} as PaperlessConfig)
|
||||
.subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}config/1/`
|
||||
)
|
||||
expect(req.request.method).toEqual('PATCH')
|
||||
})
|
||||
})
|
||||
27
src-ui/src/app/services/config.service.ts
Normal file
27
src-ui/src/app/services/config.service.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Observable, first, map } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { PaperlessConfig } from '../data/paperless-config'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConfigService {
|
||||
protected baseUrl: string = environment.apiBaseUrl + 'config/'
|
||||
|
||||
constructor(protected http: HttpClient) {}
|
||||
|
||||
getConfig(): Observable<PaperlessConfig> {
|
||||
return this.http.get<[PaperlessConfig]>(this.baseUrl).pipe(
|
||||
first(),
|
||||
map((configs) => configs[0])
|
||||
)
|
||||
}
|
||||
|
||||
saveConfig(config: PaperlessConfig): Observable<PaperlessConfig> {
|
||||
return this.http
|
||||
.patch<PaperlessConfig>(`${this.baseUrl}${config.id}/`, config)
|
||||
.pipe(first())
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export class ConsumerStatusService {
|
||||
this.statusWebSocket.onmessage = (ev) => {
|
||||
let statusMessage: WebsocketConsumerStatusMessage = JSON.parse(ev['data'])
|
||||
|
||||
// fallback if backend didnt restrict message
|
||||
// fallback if backend didn't restrict message
|
||||
if (
|
||||
statusMessage.owner_id &&
|
||||
statusMessage.owner_id !== this.settingsService.currentUser?.id &&
|
||||
|
||||
@@ -208,7 +208,7 @@ export class DocumentListViewService {
|
||||
this.activeListViewState.sortField = newState.sortField
|
||||
this.activeListViewState.sortReverse = newState.sortReverse
|
||||
this.activeListViewState.currentPage = newState.currentPage
|
||||
this.reload(null, paramsEmpty) // update the params if there arent any
|
||||
this.reload(null, paramsEmpty) // update the params if there aren't any
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export class OpenDocumentsService {
|
||||
openDocument(doc: Document): Observable<boolean> {
|
||||
if (this.openDocuments.find((d) => d.id == doc.id) == null) {
|
||||
if (this.openDocuments.length == this.MAX_OPEN_DOCUMENTS) {
|
||||
// at max, ensure changes arent lost
|
||||
// at max, ensure changes aren't lost
|
||||
const docToRemove = this.openDocuments[this.MAX_OPEN_DOCUMENTS - 1]
|
||||
const closeObservable = this.closeDocument(docToRemove)
|
||||
closeObservable.pipe(first()).subscribe((closed) => {
|
||||
|
||||
@@ -252,10 +252,18 @@ describe('PermissionsService', () => {
|
||||
'view_sharelink',
|
||||
'change_sharelink',
|
||||
'delete_sharelink',
|
||||
'add_consumptiontemplate',
|
||||
'view_consumptiontemplate',
|
||||
'change_consumptiontemplate',
|
||||
'delete_consumptiontemplate',
|
||||
'add_workflow',
|
||||
'view_workflow',
|
||||
'change_workflow',
|
||||
'delete_workflow',
|
||||
'add_workflowtrigger',
|
||||
'view_workflowtrigger',
|
||||
'change_workflowtrigger',
|
||||
'delete_workflowtrigger',
|
||||
'add_workflowaction',
|
||||
'view_workflowaction',
|
||||
'change_workflowaction',
|
||||
'delete_workflowaction',
|
||||
'add_customfield',
|
||||
'view_customfield',
|
||||
'change_customfield',
|
||||
|
||||
@@ -25,8 +25,10 @@ export enum PermissionType {
|
||||
Group = '%s_group',
|
||||
Admin = '%s_logentry',
|
||||
ShareLink = '%s_sharelink',
|
||||
ConsumptionTemplate = '%s_consumptiontemplate',
|
||||
CustomField = '%s_customfield',
|
||||
Workflow = '%s_workflow',
|
||||
WorkflowTrigger = '%s_workflowtrigger',
|
||||
WorkflowAction = '%s_workflowaction',
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { HttpTestingController } from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
|
||||
import { ConsumptionTemplateService } from './consumption-template.service'
|
||||
import {
|
||||
DocumentSource,
|
||||
ConsumptionTemplate,
|
||||
} from 'src/app/data/consumption-template'
|
||||
|
||||
let httpTestingController: HttpTestingController
|
||||
let service: ConsumptionTemplateService
|
||||
const endpoint = 'consumption_templates'
|
||||
const templates: ConsumptionTemplate[] = [
|
||||
{
|
||||
name: 'Template 1',
|
||||
id: 1,
|
||||
order: 1,
|
||||
filter_filename: '*test*',
|
||||
filter_path: null,
|
||||
sources: [DocumentSource.ApiUpload],
|
||||
assign_correspondent: 2,
|
||||
},
|
||||
{
|
||||
name: 'Template 2',
|
||||
id: 2,
|
||||
order: 2,
|
||||
filter_filename: null,
|
||||
filter_path: '/test/',
|
||||
sources: [DocumentSource.ConsumeFolder, DocumentSource.ApiUpload],
|
||||
assign_document_type: 1,
|
||||
},
|
||||
]
|
||||
|
||||
// run common tests
|
||||
commonAbstractPaperlessServiceTests(
|
||||
'consumption_templates',
|
||||
ConsumptionTemplateService
|
||||
)
|
||||
|
||||
describe(`Additional service tests for ConsumptionTemplateService`, () => {
|
||||
it('should reload', () => {
|
||||
service.reload()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
|
||||
)
|
||||
req.flush({
|
||||
results: templates,
|
||||
})
|
||||
expect(service.allTemplates).toEqual(templates)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Dont need to setup again
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
service = TestBed.inject(ConsumptionTemplateService)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
httpTestingController.verify()
|
||||
})
|
||||
})
|
||||
@@ -23,7 +23,7 @@ export class GroupService extends AbstractNameFilterService<Group> {
|
||||
const { typeKey, actionKey } =
|
||||
this.permissionService.getPermissionKeys(perm)
|
||||
if (!typeKey || !actionKey) {
|
||||
// dont lose permissions the UI doesnt use
|
||||
// dont lose permissions the UI doesn't use
|
||||
o.permissions.push(perm)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ export class UserService extends AbstractNameFilterService<User> {
|
||||
const { typeKey, actionKey } =
|
||||
this.permissionService.getPermissionKeys(perm)
|
||||
if (!typeKey || !actionKey) {
|
||||
// dont lose permissions the UI doesnt use
|
||||
// dont lose permissions the UI doesn't use
|
||||
o.user_permissions.push(perm)
|
||||
}
|
||||
})
|
||||
|
||||
85
src-ui/src/app/services/rest/workflow.service.spec.ts
Normal file
85
src-ui/src/app/services/rest/workflow.service.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { HttpTestingController } from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec'
|
||||
import { WorkflowService } from './workflow.service'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import {
|
||||
DocumentSource,
|
||||
WorkflowTriggerType,
|
||||
} from 'src/app/data/workflow-trigger'
|
||||
import { WorkflowActionType } from 'src/app/data/workflow-action'
|
||||
|
||||
let httpTestingController: HttpTestingController
|
||||
let service: WorkflowService
|
||||
const endpoint = 'workflows'
|
||||
const workflows: Workflow[] = [
|
||||
{
|
||||
name: 'Workflow 1',
|
||||
id: 1,
|
||||
order: 1,
|
||||
enabled: true,
|
||||
triggers: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowTriggerType.Consumption,
|
||||
sources: [DocumentSource.ConsumeFolder],
|
||||
filter_filename: '*',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 1,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: 'foo',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Workflow 2',
|
||||
id: 2,
|
||||
order: 2,
|
||||
enabled: true,
|
||||
triggers: [
|
||||
{
|
||||
id: 2,
|
||||
type: WorkflowTriggerType.DocumentAdded,
|
||||
filter_filename: 'foo',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: 2,
|
||||
type: WorkflowActionType.Assignment,
|
||||
assign_title: 'bar',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// run common tests
|
||||
commonAbstractPaperlessServiceTests(endpoint, WorkflowService)
|
||||
|
||||
describe(`Additional service tests for WorkflowService`, () => {
|
||||
it('should reload', () => {
|
||||
service.reload()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
|
||||
)
|
||||
req.flush({
|
||||
results: workflows,
|
||||
})
|
||||
expect(service.allWorkflows).toEqual(workflows)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Dont need to setup again
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
service = TestBed.inject(WorkflowService)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
httpTestingController.verify()
|
||||
})
|
||||
})
|
||||
@@ -1,42 +1,42 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { tap } from 'rxjs'
|
||||
import { ConsumptionTemplate } from 'src/app/data/consumption-template'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { AbstractPaperlessService } from './abstract-paperless-service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConsumptionTemplateService extends AbstractPaperlessService<ConsumptionTemplate> {
|
||||
export class WorkflowService extends AbstractPaperlessService<Workflow> {
|
||||
loading: boolean
|
||||
|
||||
constructor(http: HttpClient) {
|
||||
super(http, 'consumption_templates')
|
||||
super(http, 'workflows')
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.loading = true
|
||||
this.listAll().subscribe((r) => {
|
||||
this.templates = r.results
|
||||
this.workflows = r.results
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
private templates: ConsumptionTemplate[] = []
|
||||
private workflows: Workflow[] = []
|
||||
|
||||
public get allTemplates(): ConsumptionTemplate[] {
|
||||
return this.templates
|
||||
public get allWorkflows(): Workflow[] {
|
||||
return this.workflows
|
||||
}
|
||||
|
||||
create(o: ConsumptionTemplate) {
|
||||
create(o: Workflow) {
|
||||
return super.create(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
|
||||
update(o: ConsumptionTemplate) {
|
||||
update(o: Workflow) {
|
||||
return super.update(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
|
||||
delete(o: ConsumptionTemplate) {
|
||||
delete(o: Workflow) {
|
||||
return super.delete(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export class LocalizedDateParserFormatter extends NgbDateParserFormatter {
|
||||
if (this.separatorRegExp.test(value)) {
|
||||
let segments = value.split(this.separatorRegExp)
|
||||
|
||||
// always accept strict yyyy*mm*dd format even if thats not the input format since we can be certain its not yyyy*dd*mm
|
||||
// always accept strict yyyy*mm*dd format even if that's not the input format since we can be certain its not yyyy*dd*mm
|
||||
if (
|
||||
value.length == 10 &&
|
||||
segments.length == 3 &&
|
||||
|
||||
@@ -3,9 +3,9 @@ const base_url = new URL(document.baseURI)
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiBaseUrl: document.baseURI + 'api/',
|
||||
apiVersion: '3',
|
||||
apiVersion: '4',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: '2.2.0',
|
||||
version: '2.3.3',
|
||||
webSocketHost: window.location.host,
|
||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://localhost:8000/api/',
|
||||
apiVersion: '3',
|
||||
apiVersion: '4',
|
||||
appTitle: 'Paperless-ngx',
|
||||
version: 'DEVELOPMENT',
|
||||
webSocketHost: 'localhost:8000',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user