mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-11 13:23:21 +00:00
Compare commits
239
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c96b38f4e | ||
|
|
21ac856e3f | ||
|
|
37d6b02ebc | ||
|
|
7456b52e84 | ||
|
|
6a392ea099 | ||
|
|
2032ad1341 | ||
|
|
cc8fee91c4 | ||
|
|
a5d46a883e | ||
|
|
b0e1793093 | ||
|
|
7e466d1f71 | ||
|
|
7b69a178c0 | ||
|
|
22cd13a8a9 | ||
|
|
72a4676be0 | ||
|
|
6673144d23 | ||
|
|
994a84cf92 | ||
|
|
654ce5d8f3 | ||
|
|
5d5e9b6db4 | ||
|
|
62089df2d8 | ||
|
|
5e5f6a88a3 | ||
|
|
02e6c49c62 | ||
|
|
3be64da4cb | ||
|
|
c28c532bef | ||
|
|
1d61f7fc62 | ||
|
|
aa67fd3aef | ||
|
|
17dc482872 | ||
|
|
b0e0e8a353 | ||
|
|
fc242bb570 | ||
|
|
b192a419fd | ||
|
|
3986150f95 | ||
|
|
ee5588ade3 | ||
|
|
2635a12281 | ||
|
|
1f396e51f3 | ||
|
|
3eb784b34d | ||
|
|
cb03a0b33e | ||
|
|
a96d0b15b8 | ||
|
|
376b61938f | ||
|
|
d1f5eb0335 | ||
|
|
d283205f57 | ||
|
|
42908ad2b9 | ||
|
|
db6842e710 | ||
|
|
d5f8cd59fb | ||
|
|
91d6741b4f | ||
|
|
972758fc72 | ||
|
|
15d9829b6d | ||
|
|
5ad34dfe03 | ||
|
|
52a0484f74 | ||
|
|
71e2f86f70 | ||
|
|
1824e5fafd | ||
|
|
7b9e56ef22 | ||
|
|
2b9bed749d | ||
|
|
ef49414162 | ||
|
|
a488ba6f90 | ||
|
|
47727de116 | ||
|
|
e86adb6c36 | ||
|
|
74aa10f31f | ||
|
|
2ea4a792fe | ||
|
|
3f5c770af4 | ||
|
|
765313926f | ||
|
|
1f1725ba56 | ||
|
|
c6cecd3c4e | ||
|
|
5e54259db9 | ||
|
|
3a484a3ff2 | ||
|
|
92a1b24583 | ||
|
|
8709dd807b | ||
|
|
5a5a0ffd21 | ||
|
|
d7aa0cb862 | ||
|
|
eafb494066 | ||
|
|
4ca1863dee | ||
|
|
d5d22c2c0f | ||
|
|
991d99bb7c | ||
|
|
30f11814ff | ||
|
|
0ca5c66ca7 | ||
|
|
dd99c2289d | ||
|
|
23c11f49d0 | ||
|
|
cfa1d3b058 | ||
|
|
68bd8f8f63 | ||
|
|
41d5d86637 | ||
|
|
2a50425902 | ||
|
|
b67ac5a7d7 | ||
|
|
b48c434266 | ||
|
|
92baa89638 | ||
|
|
4c7b3dd307 | ||
|
|
fe48752a6f | ||
|
|
4692de5595 | ||
|
|
141d74464b | ||
|
|
c05d069e97 | ||
|
|
f5e47a86c3 | ||
|
|
fe196eaf0e | ||
|
|
7fda0e877b | ||
|
|
5b8f4db1ca | ||
|
|
08ab98f3fc | ||
|
|
8fb73b2709 | ||
|
|
b8ab8327b0 | ||
|
|
3a0ca03544 | ||
|
|
882dfa78d2 | ||
|
|
a5645392a5 | ||
|
|
bbdf2cbd06 | ||
|
|
0bbe180f63 | ||
|
|
e35452beeb | ||
|
|
68a769b40c | ||
|
|
e5b27df99c | ||
|
|
95a48dd685 | ||
|
|
aa938de747 | ||
|
|
486b4babac | ||
|
|
adf1dd4e05 | ||
|
|
3d57f4396f | ||
|
|
752c949009 | ||
|
|
e37ef5aa71 | ||
|
|
919ffdd794 | ||
|
|
30fe172847 | ||
|
|
e79f0d4106 | ||
|
|
c29c9178f0 | ||
|
|
baae99eb49 | ||
|
|
f50a021ef6 | ||
|
|
c8e00dca04 | ||
|
|
0a56018d6b | ||
|
|
350684cd6b | ||
|
|
668fa77428 | ||
|
|
5bd72014a6 | ||
|
|
bbb9c86ba4 | ||
|
|
09c9fa03cf | ||
|
|
04779d72bb | ||
|
|
1b32b9d678 | ||
|
|
12d318deff | ||
|
|
9c98f30d3f | ||
|
|
6a1d7b1bca | ||
|
|
de9f520143 | ||
|
|
a68c487d60 | ||
|
|
217421bceb | ||
|
|
3bc03bbaec | ||
|
|
aaee24ac0b | ||
|
|
674dab29df | ||
|
|
b2b2f7b488 | ||
|
|
0b588c2d31 | ||
|
|
dcff067dc1 | ||
|
|
7eee8538ec | ||
|
|
98b66fdf24 | ||
|
|
693a111c5a | ||
|
|
b19edd0b74 | ||
|
|
0e98a7f1ce | ||
|
|
14517062c4 | ||
|
|
0c0faf7f80 | ||
|
|
eda79603fe | ||
|
|
8f017942d9 | ||
|
|
f4e7a2e9e4 | ||
|
|
317e534aa0 | ||
|
|
3ba6d0325a | ||
|
|
318520a0f1 | ||
|
|
ea1f3653a9 | ||
|
|
423d4b9f2d | ||
|
|
8bc4e5ee94 | ||
|
|
65fe6aacc7 | ||
|
|
b5a23cff2f | ||
|
|
aa90f79c21 | ||
|
|
e704bf88dc | ||
|
|
1646756c6c | ||
|
|
48d6ff13f2 | ||
|
|
bfe2d92619 | ||
|
|
4f0845b094 | ||
|
|
dee568e2a6 | ||
|
|
f60a2af841 | ||
|
|
33cd48a17f | ||
|
|
d76dbf5366 | ||
|
|
b66182cd7b | ||
|
|
3fe6562c57 | ||
|
|
737d568f80 | ||
|
|
b992f9fc05 | ||
|
|
ed714b8a5a | ||
|
|
e0ab6a2e75 | ||
|
|
6d249b4932 | ||
|
|
90ea4f27c4 | ||
|
|
371c4f57d3 | ||
|
|
5569447ea0 | ||
|
|
17796357ee | ||
|
|
b47b9800a1 | ||
|
|
0be884d441 | ||
|
|
fce2fbb8f6 | ||
|
|
fa01396dfd | ||
|
|
c9443e890f | ||
|
|
21e4721a82 | ||
|
|
1985da14f8 | ||
|
|
59a737f669 | ||
|
|
90bee4285f | ||
|
|
7dca0bfa6a | ||
|
|
3a34bdc7b0 | ||
|
|
c7899a9a84 | ||
|
|
82fc1c6cee | ||
|
|
cd3c162e6e | ||
|
|
50c88dab7a | ||
|
|
5744a75e6a | ||
|
|
02c6b5f3e4 | ||
|
|
b566232542 | ||
|
|
be40356d2e | ||
|
|
5ea8cf2335 | ||
|
|
84a04301d1 | ||
|
|
3edda48324 | ||
|
|
f86bc57880 | ||
|
|
97662b6c5c | ||
|
|
6e052d5507 | ||
|
|
afb19fe637 | ||
|
|
1203203e08 | ||
|
|
6cf3c47814 | ||
|
|
24aee98ad5 | ||
|
|
2ab1ac7cb8 | ||
|
|
c2d9c52cde | ||
|
|
cdb11b94b5 | ||
|
|
cfd7a3a7f0 | ||
|
|
c6329f4b76 | ||
|
|
5e06987102 | ||
|
|
3f93557934 | ||
|
|
afe6ad2192 | ||
|
|
a3f9fa0369 | ||
|
|
e106618c39 | ||
|
|
d113826cf8 | ||
|
|
c063b3799f | ||
|
|
81795bc93a | ||
|
|
6664f05543 | ||
|
|
8aab34ea73 | ||
|
|
8404198ec8 | ||
|
|
d8d8872414 | ||
|
|
ff609c2987 | ||
|
|
2b784e709b | ||
|
|
c9716252f0 | ||
|
|
5cf9152a40 | ||
|
|
80210bd3bf | ||
|
|
4e52dd5710 | ||
|
|
d34ef75786 | ||
|
|
b8c424c8e1 | ||
|
|
ec8991c2ec | ||
|
|
4f6d9fa93f | ||
|
|
2180b21c41 | ||
|
|
99bdfdfe7a | ||
|
|
73a0586a75 | ||
|
|
dcba44ad78 | ||
|
|
ed54f1a8b9 | ||
|
|
f8a641b402 | ||
|
|
e85223929c | ||
|
|
5589a584b6 | ||
|
|
fd0168ac6f |
+37
-35
@@ -1,49 +1,51 @@
|
|||||||
categories:
|
categories:
|
||||||
|
- type: 'pre-include'
|
||||||
|
when:
|
||||||
|
- label: 'enhancement'
|
||||||
|
- label: 'bug'
|
||||||
|
- label: 'chore'
|
||||||
|
- label: 'deployment'
|
||||||
|
- label: 'translation'
|
||||||
|
- label: 'dependencies'
|
||||||
|
- label: 'documentation'
|
||||||
|
- label: 'frontend'
|
||||||
|
- label: 'backend'
|
||||||
|
- label: 'ci-cd'
|
||||||
|
- label: 'breaking-change'
|
||||||
|
- label: 'notable'
|
||||||
|
- type: 'pre-exclude'
|
||||||
|
when:
|
||||||
|
- label: 'skip-changelog'
|
||||||
- title: 'Breaking Changes'
|
- title: 'Breaking Changes'
|
||||||
labels:
|
when:
|
||||||
- 'breaking-change'
|
- label: 'breaking-change'
|
||||||
- title: 'Notable Changes'
|
- title: 'Notable Changes'
|
||||||
labels:
|
when:
|
||||||
- 'notable'
|
- label: 'notable'
|
||||||
- title: 'Features / Enhancements'
|
- title: 'Features / Enhancements'
|
||||||
labels:
|
when:
|
||||||
- 'enhancement'
|
- label: 'enhancement'
|
||||||
- title: 'Bug Fixes'
|
- title: 'Bug Fixes'
|
||||||
labels:
|
when:
|
||||||
- 'bug'
|
- label: 'bug'
|
||||||
- title: 'Documentation'
|
- title: 'Documentation'
|
||||||
labels:
|
when:
|
||||||
- 'documentation'
|
- label: 'documentation'
|
||||||
- title: 'Maintenance'
|
- title: 'Maintenance'
|
||||||
labels:
|
when:
|
||||||
- 'chore'
|
- label: 'chore'
|
||||||
- 'deployment'
|
- label: 'deployment'
|
||||||
- 'translation'
|
- label: 'translation'
|
||||||
- 'ci-cd'
|
- label: 'ci-cd'
|
||||||
- title: 'Dependencies'
|
- title: 'Dependencies'
|
||||||
collapse-after: 3
|
collapse-after: 3
|
||||||
labels:
|
when:
|
||||||
- 'dependencies'
|
- label: 'dependencies'
|
||||||
- title: 'All App Changes'
|
- title: 'All App Changes'
|
||||||
labels:
|
|
||||||
- 'frontend'
|
|
||||||
- 'backend'
|
|
||||||
collapse-after: 1
|
collapse-after: 1
|
||||||
include-labels:
|
when:
|
||||||
- 'enhancement'
|
- label: 'frontend'
|
||||||
- 'bug'
|
- label: 'backend'
|
||||||
- 'chore'
|
|
||||||
- 'deployment'
|
|
||||||
- 'translation'
|
|
||||||
- 'dependencies'
|
|
||||||
- 'documentation'
|
|
||||||
- 'frontend'
|
|
||||||
- 'backend'
|
|
||||||
- 'ci-cd'
|
|
||||||
- 'breaking-change'
|
|
||||||
- 'notable'
|
|
||||||
exclude-labels:
|
|
||||||
- 'skip-changelog'
|
|
||||||
filter-by-commitish: true
|
filter-by-commitish: true
|
||||||
category-template: '### $TITLE'
|
category-template: '### $TITLE'
|
||||||
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
backend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.backend == 'true' }}
|
backend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.backend == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
- name: Detect changes
|
- name: Detect changes
|
||||||
id: filter
|
id: filter
|
||||||
if: steps.force.outputs.run_all != 'true'
|
if: steps.force.outputs.run_all != 'true'
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||||
with:
|
with:
|
||||||
base: ${{ steps.range.outputs.base }}
|
base: ${{ steps.range.outputs.base }}
|
||||||
ref: ${{ steps.range.outputs.ref }}
|
ref: ${{ steps.range.outputs.ref }}
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Start containers
|
- name: Start containers
|
||||||
@@ -96,11 +96,11 @@ jobs:
|
|||||||
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
|
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ matrix.python-version }}"
|
python-version: "${{ matrix.python-version }}"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -169,16 +169,16 @@ jobs:
|
|||||||
PAPERLESS_SECRET_KEY: "ci-typing-not-a-real-secret"
|
PAPERLESS_SECRET_KEY: "ci-typing-not-a-real-secret"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "${{ env.DEFAULT_PYTHON }}"
|
python-version: "${{ env.DEFAULT_PYTHON }}"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
ref-name: ${{ steps.ref.outputs.name }}
|
ref-name: ${{ steps.ref.outputs.name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Determine ref name
|
- name: Determine ref name
|
||||||
@@ -106,9 +106,9 @@ jobs:
|
|||||||
echo "repository=${repo_name}"
|
echo "repository=${repo_name}"
|
||||||
echo "name=${repo_name}" >> $GITHUB_OUTPUT
|
echo "name=${repo_name}" >> $GITHUB_OUTPUT
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -121,7 +121,7 @@ jobs:
|
|||||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||||
- name: Docker metadata
|
- name: Docker metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}
|
${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}
|
||||||
@@ -132,7 +132,7 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -182,29 +182,29 @@ jobs:
|
|||||||
echo "Downloaded digests:"
|
echo "Downloaded digests:"
|
||||||
ls -la /tmp/digests/
|
ls -la /tmp/digests/
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: needs.build-arch.outputs.push-external == 'true'
|
if: needs.build-arch.outputs.push-external == 'true'
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to Quay.io
|
- name: Login to Quay.io
|
||||||
if: needs.build-arch.outputs.push-external == 'true'
|
if: needs.build-arch.outputs.push-external == 'true'
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
|
||||||
- name: Docker metadata
|
- name: Docker metadata
|
||||||
id: docker-meta
|
id: docker-meta
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.REGISTRY }}/${{ needs.build-arch.outputs.repository }}
|
${{ env.REGISTRY }}/${{ needs.build-arch.outputs.repository }}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
docs_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.docs == 'true' }}
|
docs_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.docs == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
- name: Detect changes
|
- name: Detect changes
|
||||||
id: filter
|
id: filter
|
||||||
if: steps.force.outputs.run_all != 'true'
|
if: steps.force.outputs.run_all != 'true'
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||||
with:
|
with:
|
||||||
base: ${{ steps.range.outputs.base }}
|
base: ${{ steps.range.outputs.base }}
|
||||||
ref: ${{ steps.range.outputs.ref }}
|
ref: ${{ steps.range.outputs.ref }}
|
||||||
@@ -69,16 +69,16 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
frontend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.frontend == 'true' }}
|
frontend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.frontend == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
- name: Detect changes
|
- name: Detect changes
|
||||||
id: filter
|
id: filter
|
||||||
if: steps.force.outputs.run_all != 'true'
|
if: steps.force.outputs.run_all != 'true'
|
||||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
||||||
with:
|
with:
|
||||||
base: ${{ steps.range.outputs.base }}
|
base: ${{ steps.range.outputs.base }}
|
||||||
ref: ${{ steps.range.outputs.ref }}
|
ref: ${{ steps.range.outputs.ref }}
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -129,8 +129,8 @@ jobs:
|
|||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular CLI
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: cd src-ui && pnpm run lint
|
run: cd src-ui && pnpm run lint
|
||||||
unit-tests:
|
unit-tests:
|
||||||
@@ -148,7 +148,7 @@ jobs:
|
|||||||
shard-count: [4]
|
shard-count: [4]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -168,8 +168,8 @@ jobs:
|
|||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular CLI
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
- name: Run Jest unit tests
|
- name: Run Jest unit tests
|
||||||
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
@@ -191,7 +191,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
container: mcr.microsoft.com/playwright:v1.61.1-noble
|
container: mcr.microsoft.com/playwright:v1.62.0-noble
|
||||||
env:
|
env:
|
||||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
@@ -203,7 +203,7 @@ jobs:
|
|||||||
shard-count: [2]
|
shard-count: [2]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
@@ -211,7 +211,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -223,23 +223,20 @@ jobs:
|
|||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular CLI
|
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm install --no-frozen-lockfile
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
- name: Run Playwright E2E tests
|
- name: Run Playwright E2E tests
|
||||||
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
bundle-analysis:
|
frontend-build:
|
||||||
name: Bundle Analysis
|
name: Frontend Build
|
||||||
needs: [changes, unit-tests, e2e-tests]
|
needs: [changes, unit-tests, e2e-tests]
|
||||||
if: needs.changes.outputs.frontend_changed == 'true'
|
if: needs.changes.outputs.frontend_changed == 'true'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
environment: bundle-analysis
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -248,7 +245,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -260,21 +257,19 @@ jobs:
|
|||||||
~/.pnpm-store
|
~/.pnpm-store
|
||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular CLI
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
- name: Build and analyze
|
- name: Build
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
run: cd src-ui && pnpm run build --configuration=production
|
run: cd src-ui && pnpm run build --configuration=production
|
||||||
gate:
|
gate:
|
||||||
name: Frontend CI Gate
|
name: Frontend CI Gate
|
||||||
needs: [changes, install-dependencies, lint, unit-tests, e2e-tests, bundle-analysis]
|
needs: [changes, install-dependencies, lint, unit-tests, e2e-tests, frontend-build]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-slim
|
||||||
steps:
|
steps:
|
||||||
- name: Check gate
|
- name: Check gate
|
||||||
env:
|
env:
|
||||||
BUNDLE_ANALYSIS_RESULT: ${{ needs['bundle-analysis'].result }}
|
BUILD_RESULT: ${{ needs['frontend-build'].result }}
|
||||||
E2E_RESULT: ${{ needs['e2e-tests'].result }}
|
E2E_RESULT: ${{ needs['e2e-tests'].result }}
|
||||||
FRONTEND_CHANGED: ${{ needs.changes.outputs.frontend_changed }}
|
FRONTEND_CHANGED: ${{ needs.changes.outputs.frontend_changed }}
|
||||||
INSTALL_RESULT: ${{ needs['install-dependencies'].result }}
|
INSTALL_RESULT: ${{ needs['install-dependencies'].result }}
|
||||||
@@ -306,8 +301,8 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${BUNDLE_ANALYSIS_RESULT}" != "success" ]]; then
|
if [[ "${BUILD_RESULT}" != "success" ]]; then
|
||||||
echo "::error::Frontend bundle-analysis job result: ${BUNDLE_ANALYSIS_RESULT}"
|
echo "::error::Frontend build job result: ${BUILD_RESULT}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ jobs:
|
|||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-slim
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
- name: Run prek
|
- name: Run prek
|
||||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
uses: j178/prek-action@5337cb91e0fa35a7ff31b9ca345126d8bbbcdf16 # v2.0.6
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ jobs:
|
|||||||
statuses: read
|
statuses: read
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for Docker build
|
- name: Wait for Docker build
|
||||||
uses: lewagon/wait-on-check-action@96d9100b431964d10e0136aff8b9ccb92470505e # v1.8.0
|
uses: lewagon/wait-on-check-action@2271c86c146b96545b4e871b855e10ffa6f50773 # v1.9.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
check-name: 'Merge and Push Manifest'
|
check-name: 'Merge and Push Manifest'
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
wait-interval: 60
|
wait-interval: 60
|
||||||
|
checks-discovery-timeout: 1800
|
||||||
build-release:
|
build-release:
|
||||||
name: Build Release
|
name: Build Release
|
||||||
needs: wait-for-docker
|
needs: wait-for-docker
|
||||||
@@ -34,7 +35,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# ---- Frontend Build ----
|
# ---- Frontend Build ----
|
||||||
@@ -43,7 +44,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
@@ -54,11 +55,11 @@ jobs:
|
|||||||
# ---- Backend Setup ----
|
# ---- Backend Setup ----
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
@@ -85,7 +86,7 @@ jobs:
|
|||||||
# ---- Prepare Release ----
|
# ---- Prepare Release ----
|
||||||
- name: Generate requirements file
|
- name: Generate requirements file
|
||||||
run: |
|
run: |
|
||||||
uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt
|
uv export --quiet --no-dev --all-extras --emit-index-url --format requirements-txt --output-file requirements.txt
|
||||||
- name: Compile messages
|
- name: Compile messages
|
||||||
env:
|
env:
|
||||||
PAPERLESS_SECRET_KEY: "ci-release-not-a-real-secret"
|
PAPERLESS_SECRET_KEY: "ci-release-not-a-real-secret"
|
||||||
@@ -170,7 +171,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Create release and changelog
|
- name: Create release and changelog
|
||||||
id: create-release
|
id: create-release
|
||||||
uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
|
uses: release-drafter/release-drafter@eada3c96a64734dd381cfbda23511034e328ddb0 # v7.6.0
|
||||||
with:
|
with:
|
||||||
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
|
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
|
||||||
tag: ${{ steps.get-version.outputs.version }}
|
tag: ${{ steps.get-version.outputs.version }}
|
||||||
@@ -201,17 +202,17 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
persist-credentials: true # for pushing changelog branch
|
persist-credentials: true # for pushing changelog branch
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
|
||||||
semgrep:
|
semgrep:
|
||||||
name: Semgrep CE
|
name: Semgrep CE
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -38,13 +38,13 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Run Semgrep
|
- name: Run Semgrep
|
||||||
run: semgrep scan --config auto --sarif-output results.sarif
|
run: semgrep scan --config auto --sarif-output results.sarif
|
||||||
- name: Upload results to GitHub code scanning
|
- name: Upload results to GitHub code scanning
|
||||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ jobs:
|
|||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -47,4 +47,4 @@ jobs:
|
|||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ jobs:
|
|||||||
environment: translation-sync
|
environment: translation-sync
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
|
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
|
||||||
with:
|
with:
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: true
|
download_translations: true
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Label PR by file path or branch name
|
- name: Label PR by file path or branch name
|
||||||
# see .github/labeler.yml for the labeler config
|
# see .github/labeler.yml for the labeler config
|
||||||
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Label by size
|
- name: Label by size
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ jobs:
|
|||||||
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
|
||||||
steps:
|
steps:
|
||||||
- name: Label PR with release-drafter
|
- name: Label PR with release-drafter
|
||||||
uses: release-drafter/release-drafter@4d75298e00d9e34c483e5ff8c68d0ea1c1940c1e # v7.5.1
|
uses: release-drafter/release-drafter@eada3c96a64734dd381cfbda23511034e328ddb0 # v7.6.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 7
|
days-before-stale: 7
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
env:
|
env:
|
||||||
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
|
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
|
||||||
with:
|
with:
|
||||||
@@ -23,13 +23,13 @@ jobs:
|
|||||||
persist-credentials: true # for pushing translation branch
|
persist-credentials: true # for pushing translation branch
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -qq --no-install-recommends gettext
|
sudo apt-get install -qq --no-install-recommends gettext
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 24
|
- name: Use Node.js 24
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: 24.x
|
node-version: 24.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -61,16 +61,13 @@ jobs:
|
|||||||
~/.cache
|
~/.cache
|
||||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
|
run: cd src-ui && pnpm install --frozen-lockfile
|
||||||
run: cd src-ui && pnpm install
|
|
||||||
- name: Re-link Angular cli
|
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
|
||||||
- name: Generate frontend translation strings
|
- name: Generate frontend translation strings
|
||||||
run: |
|
run: |
|
||||||
cd src-ui
|
cd src-ui
|
||||||
pnpm run ng extract-i18n
|
pnpm run ng extract-i18n
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
|
uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
|
||||||
with:
|
with:
|
||||||
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
||||||
commit_message: "Auto translate strings"
|
commit_message: "Auto translate strings"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ repos:
|
|||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.4.2
|
rev: v2.4.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
@@ -38,7 +38,7 @@ repos:
|
|||||||
- json
|
- json
|
||||||
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
||||||
- repo: https://github.com/rbubley/mirrors-prettier
|
- repo: https://github.com/rbubley/mirrors-prettier
|
||||||
rev: 'v3.9.4'
|
rev: 'v3.9.6'
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
types_or:
|
types_or:
|
||||||
@@ -46,16 +46,16 @@ repos:
|
|||||||
- ts
|
- ts
|
||||||
- markdown
|
- markdown
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- prettier@3.9.4
|
- prettier@3.9.6
|
||||||
- 'prettier-plugin-organize-imports@4.3.0'
|
- 'prettier-plugin-organize-imports@4.3.0'
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.15.20
|
rev: v0.16.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: "v2.25.1"
|
rev: "v2.26.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.11.28-python3.12-trixie-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.11.32-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
network_mode: host
|
network_mode: host
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
greenmail:
|
greenmail:
|
||||||
image: docker.io/greenmail/standalone:2.1.9
|
image: docker.io/greenmail/standalone:2.1.11
|
||||||
hostname: greenmail
|
hostname: greenmail
|
||||||
container_name: greenmail
|
container_name: greenmail
|
||||||
environment:
|
environment:
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- "3143:3143" # IMAP
|
- "3143:3143" # IMAP
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
nginx:
|
nginx:
|
||||||
image: docker.io/nginx:1.31.2-alpine
|
image: docker.io/nginx:1.31.3-alpine
|
||||||
hostname: nginx
|
hostname: nginx
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PAPERLESS_REDIS: redis://broker:6379
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
PAPERLESS_DBHOST: db
|
PAPERLESS_DBHOST: db
|
||||||
PAPERLESS_DBENGINE: postgres
|
PAPERLESS_DBENGINE: postgresql
|
||||||
env_file:
|
env_file:
|
||||||
- stack.env
|
- stack.env
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/command/with-contenv /usr/bin/bash
|
||||||
|
# shellcheck shell=bash
|
||||||
|
|
||||||
|
declare -r log_prefix="[init-llmindex-migrate]"
|
||||||
|
|
||||||
|
echo "${log_prefix} Checking for pending LLM index migrations..."
|
||||||
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_llmindex migrate
|
||||||
|
else
|
||||||
|
s6-setuidgid paperless python3 manage.py document_llmindex migrate
|
||||||
|
fi
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
oneshot
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/run
|
||||||
+28
-1
@@ -151,6 +151,23 @@ the background.
|
|||||||
|
|
||||||
### Bare Metal Route {#bare-metal-updating}
|
### Bare Metal Route {#bare-metal-updating}
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Extracting a new release archive on top of an existing installation
|
||||||
|
(e.g. `tar -xf paperless-ngx-vX.Y.Z.tar.xz -C /opt/paperless`) only adds
|
||||||
|
and overwrites files -- it does not remove files that were deleted in
|
||||||
|
the new release. Leftover files from an old version, such as
|
||||||
|
superseded database migrations, can remain on disk and cause errors
|
||||||
|
like `NodeNotFoundError` during `manage.py migrate`.
|
||||||
|
|
||||||
|
Before unpacking a new release over an existing bare-metal
|
||||||
|
installation, remove the previous source tree first (everything
|
||||||
|
except your `media`, `data`, and `consume` directories and your
|
||||||
|
`paperless.conf`/`.env`), or unpack into a fresh directory and move
|
||||||
|
your persistent data and configuration over. Installations updated via
|
||||||
|
`git pull` on a clean checkout are not affected, since Git removes
|
||||||
|
files that no longer exist upstream.
|
||||||
|
|
||||||
After grabbing the new release and unpacking the contents, do the
|
After grabbing the new release and unpacking the contents, do the
|
||||||
following:
|
following:
|
||||||
|
|
||||||
@@ -195,6 +212,16 @@ following:
|
|||||||
This is a no-op if the index is already up to date, so it is safe to
|
This is a no-op if the index is already up to date, so it is safe to
|
||||||
run on every upgrade.
|
run on every upgrade.
|
||||||
|
|
||||||
|
5. Migrate the LLM index if needed.
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
cd src
|
||||||
|
python3 manage.py document_llmindex migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a no-op if the index schema is already current, so it is safe
|
||||||
|
to run on every upgrade.
|
||||||
|
|
||||||
### Database Upgrades
|
### Database Upgrades
|
||||||
|
|
||||||
Paperless-ngx is compatible with Django-supported versions of PostgreSQL and MariaDB and it is generally
|
Paperless-ngx is compatible with Django-supported versions of PostgreSQL and MariaDB and it is generally
|
||||||
@@ -515,7 +542,7 @@ index is updated automatically on the schedule set by
|
|||||||
can manage it manually:
|
can manage it manually:
|
||||||
|
|
||||||
```
|
```
|
||||||
document_llmindex {rebuild,update,compact}
|
document_llmindex {rebuild,update,compact,migrate}
|
||||||
```
|
```
|
||||||
|
|
||||||
Specify `rebuild` to build the index from scratch from all documents in the database. Use
|
Specify `rebuild` to build the index from scratch from all documents in the database. Use
|
||||||
|
|||||||
+2
-1
@@ -301,7 +301,8 @@ The following methods are supported:
|
|||||||
- `delete`
|
- `delete`
|
||||||
- No `parameters` required
|
- No `parameters` required
|
||||||
- `reprocess`
|
- `reprocess`
|
||||||
- No `parameters` required
|
- Optional `parameters`: `{ "remote_ocr": true }` to send the documents to the
|
||||||
|
remote OCR engine, see [Remote OCR](usage.md#remote-ocr). Defaults to false.
|
||||||
- `set_permissions`
|
- `set_permissions`
|
||||||
- Requires `parameters`:
|
- Requires `parameters`:
|
||||||
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
|
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
|
||||||
|
|||||||
+982
-1
@@ -1,5 +1,986 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.5
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: accept Whoosh-era abbreviated relative-date units (yrs, mos, wks, etc) in search queries [@stumpylog](https://github.com/stumpylog) ([#13486](https://github.com/paperless-ngx/paperless-ngx/pull/13486))
|
||||||
|
- Fix: fix edit dialog error change detection [@shamoon](https://github.com/shamoon) ([#13483](https://github.com/paperless-ngx/paperless-ngx/pull/13483))
|
||||||
|
- Fix: key the AI suggestion cache by model and endpoint [@lunetics](https://github.com/lunetics) ([#13449](https://github.com/paperless-ngx/paperless-ngx/pull/13449))
|
||||||
|
- Fix: validate custom field values in bulk operations [@shamoon](https://github.com/shamoon) ([#13457](https://github.com/paperless-ngx/paperless-ngx/pull/13457))
|
||||||
|
- Fixhancement: better handle empty fields from AI suggestions [@shamoon](https://github.com/shamoon) ([#13454](https://github.com/paperless-ngx/paperless-ngx/pull/13454))
|
||||||
|
- Fix: normalize monetary decimal symbol by locale [@shamoon](https://github.com/shamoon) ([#13427](https://github.com/paperless-ngx/paperless-ngx/pull/13427))
|
||||||
|
- Fix: fold overflowing path text in sanity checker [@stumpylog](https://github.com/stumpylog) ([#13426](https://github.com/paperless-ngx/paperless-ngx/pull/13426))
|
||||||
|
- Fix: correct delayed add field button for custom fields [@shamoon](https://github.com/shamoon) ([#13424](https://github.com/paperless-ngx/paperless-ngx/pull/13424))
|
||||||
|
- Fix: hide attributes collapse / show button without UI settings [@shamoon](https://github.com/shamoon) ([#13425](https://github.com/paperless-ngx/paperless-ngx/pull/13425))
|
||||||
|
- Fix: consolidate born-digital PDF detection between archive decision and OCR [@stumpylog](https://github.com/stumpylog) ([#13409](https://github.com/paperless-ngx/paperless-ngx/pull/13409))
|
||||||
|
- Fix: prevent scale vs page loop in pngx PDF viewer [@shamoon](https://github.com/shamoon) ([#13406](https://github.com/paperless-ngx/paperless-ngx/pull/13406))
|
||||||
|
- Fix: handle hidden line breaks in email subjects when sending email [@shamoon](https://github.com/shamoon) ([#13402](https://github.com/paperless-ngx/paperless-ngx/pull/13402))
|
||||||
|
- Fix: avoid NotSupportedError from document\_importer on MariaDB [@stumpylog](https://github.com/stumpylog) ([#13400](https://github.com/paperless-ngx/paperless-ngx/pull/13400))
|
||||||
|
- Fix: exclude next-period start from relative date-range filters [@stumpylog](https://github.com/stumpylog) ([#13381](https://github.com/paperless-ngx/paperless-ngx/pull/13381))
|
||||||
|
- Fix: close non-atomic db connections in before\_task\_publish [@shamoon](https://github.com/shamoon) ([#13366](https://github.com/paperless-ngx/paperless-ngx/pull/13366))
|
||||||
|
- Chore/fix: refactor frontend task service [@shamoon](https://github.com/shamoon) ([#13365](https://github.com/paperless-ngx/paperless-ngx/pull/13365))
|
||||||
|
- Fix: fix Enter selection in search autocomplete [@shamoon](https://github.com/shamoon) ([#13361](https://github.com/paperless-ngx/paperless-ngx/pull/13361))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Documentation: PAPERLESS\_CONSUMER\_IGNORE\_PATTERNS clarifications [@shamoon](https://github.com/shamoon) ([#13489](https://github.com/paperless-ngx/paperless-ngx/pull/13489))
|
||||||
|
- Documentation: Add Password Removal workflow action documentation [@stumpylog](https://github.com/stumpylog) ([#13377](https://github.com/paperless-ngx/paperless-ngx/pull/13377))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore(deps): Bump pymdown-extensions from 10.21.3 to 11.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13378](https://github.com/paperless-ngx/paperless-ngx/pull/13378))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>21 changes</summary>
|
||||||
|
|
||||||
|
- Fix: accept Whoosh-era abbreviated relative-date units (yrs, mos, wks, etc) in search queries [@stumpylog](https://github.com/stumpylog) ([#13486](https://github.com/paperless-ngx/paperless-ngx/pull/13486))
|
||||||
|
- Fix: fix edit dialog error change detection [@shamoon](https://github.com/shamoon) ([#13483](https://github.com/paperless-ngx/paperless-ngx/pull/13483))
|
||||||
|
- Performance: sqlite-vec point-delete for document chunks [@stumpylog](https://github.com/stumpylog) ([#13438](https://github.com/paperless-ngx/paperless-ngx/pull/13438))
|
||||||
|
- Fix: key the AI suggestion cache by model and endpoint [@lunetics](https://github.com/lunetics) ([#13449](https://github.com/paperless-ngx/paperless-ngx/pull/13449))
|
||||||
|
- Fix: validate custom field values in bulk operations [@shamoon](https://github.com/shamoon) ([#13457](https://github.com/paperless-ngx/paperless-ngx/pull/13457))
|
||||||
|
- Fixhancement: better handle empty fields from AI suggestions [@shamoon](https://github.com/shamoon) ([#13454](https://github.com/paperless-ngx/paperless-ngx/pull/13454))
|
||||||
|
- Performance: Use server side iterators during LLM index updating [@stumpylog](https://github.com/stumpylog) ([#13430](https://github.com/paperless-ngx/paperless-ngx/pull/13430))
|
||||||
|
- Fix: normalize monetary decimal symbol by locale [@shamoon](https://github.com/shamoon) ([#13427](https://github.com/paperless-ngx/paperless-ngx/pull/13427))
|
||||||
|
- Fix: fold overflowing path text in sanity checker [@stumpylog](https://github.com/stumpylog) ([#13426](https://github.com/paperless-ngx/paperless-ngx/pull/13426))
|
||||||
|
- Fix: correct delayed add field button for custom fields [@shamoon](https://github.com/shamoon) ([#13424](https://github.com/paperless-ngx/paperless-ngx/pull/13424))
|
||||||
|
- Fix: hide attributes collapse / show button without UI settings [@shamoon](https://github.com/shamoon) ([#13425](https://github.com/paperless-ngx/paperless-ngx/pull/13425))
|
||||||
|
- Fix: consolidate born-digital PDF detection between archive decision and OCR [@stumpylog](https://github.com/stumpylog) ([#13409](https://github.com/paperless-ngx/paperless-ngx/pull/13409))
|
||||||
|
- Fix: prevent scale vs page loop in pngx PDF viewer [@shamoon](https://github.com/shamoon) ([#13406](https://github.com/paperless-ngx/paperless-ngx/pull/13406))
|
||||||
|
- Fix: handle hidden line breaks in email subjects when sending email [@shamoon](https://github.com/shamoon) ([#13402](https://github.com/paperless-ngx/paperless-ngx/pull/13402))
|
||||||
|
- Fix: avoid NotSupportedError from document\_importer on MariaDB [@stumpylog](https://github.com/stumpylog) ([#13400](https://github.com/paperless-ngx/paperless-ngx/pull/13400))
|
||||||
|
- Tweak: adjust doc details button toolbar flow [@shamoon](https://github.com/shamoon) ([#13382](https://github.com/paperless-ngx/paperless-ngx/pull/13382))
|
||||||
|
- Fix: exclude next-period start from relative date-range filters [@stumpylog](https://github.com/stumpylog) ([#13381](https://github.com/paperless-ngx/paperless-ngx/pull/13381))
|
||||||
|
- Chore(deps): Bump pymdown-extensions from 10.21.3 to 11.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13378](https://github.com/paperless-ngx/paperless-ngx/pull/13378))
|
||||||
|
- Fix: close non-atomic db connections in before\_task\_publish [@shamoon](https://github.com/shamoon) ([#13366](https://github.com/paperless-ngx/paperless-ngx/pull/13366))
|
||||||
|
- Chore/fix: refactor frontend task service [@shamoon](https://github.com/shamoon) ([#13365](https://github.com/paperless-ngx/paperless-ngx/pull/13365))
|
||||||
|
- Fix: fix Enter selection in search autocomplete [@shamoon](https://github.com/shamoon) ([#13361](https://github.com/paperless-ngx/paperless-ngx/pull/13361))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.4
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: prevent pdfjs highlight scrolling from affecting the entire page [@shamoon](https://github.com/shamoon) ([#13355](https://github.com/paperless-ngx/paperless-ngx/pull/13355))
|
||||||
|
- Fix: don't skip OCR/archive for tagged PDFs with no actual text [@stumpylog](https://github.com/stumpylog) ([#13351](https://github.com/paperless-ngx/paperless-ngx/pull/13351))
|
||||||
|
- Performance: more efficient diacritic normalization in dropdown filtering [@shamoon](https://github.com/shamoon) ([#13347](https://github.com/paperless-ngx/paperless-ngx/pull/13347))
|
||||||
|
- Fix: dedupe permission-visible documents when combined with multi-tag filters [@stumpylog](https://github.com/stumpylog) ([#13345](https://github.com/paperless-ngx/paperless-ngx/pull/13345))
|
||||||
|
- Fixhancement: pass LLM output language to chat if specified [@shamoon](https://github.com/shamoon) ([#13340](https://github.com/paperless-ngx/paperless-ngx/pull/13340))
|
||||||
|
- Fix: ensure preview reload on live changes [@shamoon](https://github.com/shamoon) ([#13321](https://github.com/paperless-ngx/paperless-ngx/pull/13321))
|
||||||
|
- Fix: clamp all out out-of-range possible fields [@stumpylog](https://github.com/stumpylog) ([#13316](https://github.com/paperless-ngx/paperless-ngx/pull/13316))
|
||||||
|
- Fix: add docstring to DocumentClassifierSchema for cleaner LLM tool description [@stumpylog](https://github.com/stumpylog) ([#13315](https://github.com/paperless-ngx/paperless-ngx/pull/13315))
|
||||||
|
- Fix: guard build\_document\_node against stale FK on deleted correspondent/doc type [@stumpylog](https://github.com/stumpylog) ([#13318](https://github.com/paperless-ngx/paperless-ngx/pull/13318))
|
||||||
|
- Fix: prevent search filter loss when closing document with Escape key [@shamoon](https://github.com/shamoon) ([#13317](https://github.com/paperless-ngx/paperless-ngx/pull/13317))
|
||||||
|
- Fix: fix frontend permissions display for stats/system perms [@shamoon](https://github.com/shamoon) ([#13305](https://github.com/paperless-ngx/paperless-ngx/pull/13305))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Documentation: fix PAPERLESS\_AI\_LLM\_OUTPUT\_LANGUAGE heading level [@SuperSandro2000](https://github.com/SuperSandro2000) ([#13341](https://github.com/paperless-ngx/paperless-ngx/pull/13341))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore: resolve npm provenance issues with chokidar and semver [@shamoon](https://github.com/shamoon) ([#13323](https://github.com/paperless-ngx/paperless-ngx/pull/13323))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>14 changes</summary>
|
||||||
|
|
||||||
|
- Fix: prevent pdfjs highlight scrolling from affecting the entire page [@shamoon](https://github.com/shamoon) ([#13355](https://github.com/paperless-ngx/paperless-ngx/pull/13355))
|
||||||
|
- Fix: don't skip OCR/archive for tagged PDFs with no actual text [@stumpylog](https://github.com/stumpylog) ([#13351](https://github.com/paperless-ngx/paperless-ngx/pull/13351))
|
||||||
|
- Performance: more efficient diacritic normalization in dropdown filtering [@shamoon](https://github.com/shamoon) ([#13347](https://github.com/paperless-ngx/paperless-ngx/pull/13347))
|
||||||
|
- Performance: prefetch notes and custom fields for LLM index text building [@stumpylog](https://github.com/stumpylog) ([#13350](https://github.com/paperless-ngx/paperless-ngx/pull/13350))
|
||||||
|
- Fix: dedupe permission-visible documents when combined with multi-tag filters [@stumpylog](https://github.com/stumpylog) ([#13345](https://github.com/paperless-ngx/paperless-ngx/pull/13345))
|
||||||
|
- Performance: Scope llm index updates to actually modified documents [@stumpylog](https://github.com/stumpylog) ([#13322](https://github.com/paperless-ngx/paperless-ngx/pull/13322))
|
||||||
|
- Fixhancement: pass LLM output language to chat if specified [@shamoon](https://github.com/shamoon) ([#13340](https://github.com/paperless-ngx/paperless-ngx/pull/13340))
|
||||||
|
- Chore: resolve npm provenance issues with chokidar and semver [@shamoon](https://github.com/shamoon) ([#13323](https://github.com/paperless-ngx/paperless-ngx/pull/13323))
|
||||||
|
- Fix: ensure preview reload on live changes [@shamoon](https://github.com/shamoon) ([#13321](https://github.com/paperless-ngx/paperless-ngx/pull/13321))
|
||||||
|
- Fix: clamp all out out-of-range possible fields [@stumpylog](https://github.com/stumpylog) ([#13316](https://github.com/paperless-ngx/paperless-ngx/pull/13316))
|
||||||
|
- Fix: add docstring to DocumentClassifierSchema for cleaner LLM tool description [@stumpylog](https://github.com/stumpylog) ([#13315](https://github.com/paperless-ngx/paperless-ngx/pull/13315))
|
||||||
|
- Fix: guard build\_document\_node against stale FK on deleted correspondent/doc type [@stumpylog](https://github.com/stumpylog) ([#13318](https://github.com/paperless-ngx/paperless-ngx/pull/13318))
|
||||||
|
- Fix: prevent search filter loss when closing document with Escape key [@shamoon](https://github.com/shamoon) ([#13317](https://github.com/paperless-ngx/paperless-ngx/pull/13317))
|
||||||
|
- Fix: fix frontend permissions display for stats/system perms [@shamoon](https://github.com/shamoon) ([#13305](https://github.com/paperless-ngx/paperless-ngx/pull/13305))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.3
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixhancement: PAPERLESS\_ALLAUTH\_TRUSTED\_PROXY\_COUNT [@shamoon](https://github.com/shamoon) ([#13281](https://github.com/paperless-ngx/paperless-ngx/pull/13281))
|
||||||
|
- Fix: preserve document fields during Gotenberg conversion to PDF [@stumpylog](https://github.com/stumpylog) ([#13271](https://github.com/paperless-ngx/paperless-ngx/pull/13271))
|
||||||
|
- Fix: Makes the email date aware as soon as possible during parsing [@stumpylog](https://github.com/stumpylog) ([#13266](https://github.com/paperless-ngx/paperless-ngx/pull/13266))
|
||||||
|
- Fix: Handle a plain string as Celery sometimes provides for the traceback [@stumpylog](https://github.com/stumpylog) ([#13267](https://github.com/paperless-ngx/paperless-ngx/pull/13267))
|
||||||
|
- Fix: Emit the torch index into the requirements.txt for people still using it [@stumpylog](https://github.com/stumpylog) ([#13265](https://github.com/paperless-ngx/paperless-ngx/pull/13265))
|
||||||
|
- Fix: handle notes without a user when building the search index [@matthiasmast](https://github.com/matthiasmast) ([#13260](https://github.com/paperless-ngx/paperless-ngx/pull/13260))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Docs: warn bare-metal users about stale files when upgrading [@stumpylog](https://github.com/stumpylog) ([#13296](https://github.com/paperless-ngx/paperless-ngx/pull/13296))
|
||||||
|
- Documentation: Add the NumPy CPU baseline increase to the migration guide [@stumpylog](https://github.com/stumpylog) ([#13269](https://github.com/paperless-ngx/paperless-ngx/pull/13269))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Fix: Emit the torch index into the requirements.txt for people still using it [@stumpylog](https://github.com/stumpylog) ([#13265](https://github.com/paperless-ngx/paperless-ngx/pull/13265))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>6 changes</summary>
|
||||||
|
|
||||||
|
- Fixhancement: PAPERLESS\_ALLAUTH\_TRUSTED\_PROXY\_COUNT [@shamoon](https://github.com/shamoon) ([#13281](https://github.com/paperless-ngx/paperless-ngx/pull/13281))
|
||||||
|
- Tweak: adjust top navbar wrapping when AI chat button visible [@shamoon](https://github.com/shamoon) ([#13280](https://github.com/paperless-ngx/paperless-ngx/pull/13280))
|
||||||
|
- Fix: preserve document fields during Gotenberg conversion to PDF [@stumpylog](https://github.com/stumpylog) ([#13271](https://github.com/paperless-ngx/paperless-ngx/pull/13271))
|
||||||
|
- Fix: Makes the email date aware as soon as possible during parsing [@stumpylog](https://github.com/stumpylog) ([#13266](https://github.com/paperless-ngx/paperless-ngx/pull/13266))
|
||||||
|
- Fix: Handle a plain string as Celery sometimes provides for the traceback [@stumpylog](https://github.com/stumpylog) ([#13267](https://github.com/paperless-ngx/paperless-ngx/pull/13267))
|
||||||
|
- Fix: handle notes without a user when building the search index [@matthiasmast](https://github.com/matthiasmast) ([#13260](https://github.com/paperless-ngx/paperless-ngx/pull/13260))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.2
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: fix broken migration in 3.0.1 [@shamoon](https://github.com/shamoon) ([#13242](https://github.com/paperless-ngx/paperless-ngx/pull/13242))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
- Fix: fix broken migration in 3.0.1 [@shamoon](https://github.com/shamoon) ([#13242](https://github.com/paperless-ngx/paperless-ngx/pull/13242))
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.1
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: selection\_data re-derives the filtered document set 5 times over [@stumpylog](https://github.com/stumpylog) ([#13229](https://github.com/paperless-ngx/paperless-ngx/pull/13229))
|
||||||
|
- Fix: exclude source document from its own RAG similarity results [@stumpylog](https://github.com/stumpylog) ([#13233](https://github.com/paperless-ngx/paperless-ngx/pull/13233))
|
||||||
|
- Fix: split mailrule maximum\_age clamp into its own migration [@stumpylog](https://github.com/stumpylog) ([#13231](https://github.com/paperless-ngx/paperless-ngx/pull/13231))
|
||||||
|
- Chore: mark yes in confirm button for translation [@shamoon](https://github.com/shamoon) ([#13225](https://github.com/paperless-ngx/paperless-ngx/pull/13225))
|
||||||
|
- Fix: correct URL for W001 check [@shamoon](https://github.com/shamoon) ([#13220](https://github.com/paperless-ngx/paperless-ngx/pull/13220))
|
||||||
|
- Fix: ensure create dialog uses correct attribute type [@shamoon](https://github.com/shamoon) ([#13221](https://github.com/paperless-ngx/paperless-ngx/pull/13221))
|
||||||
|
- Fix: correct database engine from postgres to postgresql [@OberstVonGatow](https://github.com/OberstVonGatow) ([#13213](https://github.com/paperless-ngx/paperless-ngx/pull/13213))
|
||||||
|
- Fix: remove unnecessary .distinct() dominating document list queries [@stumpylog](https://github.com/stumpylog) ([#13205](https://github.com/paperless-ngx/paperless-ngx/pull/13205))
|
||||||
|
- Fix: also wrap non-breaking words in chat messages, handle whitespace [@shamoon](https://github.com/shamoon) ([#13211](https://github.com/paperless-ngx/paperless-ngx/pull/13211))
|
||||||
|
- Fix: tag/custom-field document\_count scales badly with tag count [@stumpylog](https://github.com/stumpylog) ([#13203](https://github.com/paperless-ngx/paperless-ngx/pull/13203))
|
||||||
|
- Fix: batch document user\_can\_change checks to avoid per-row N+1 [@stumpylog](https://github.com/stumpylog) ([#13204](https://github.com/paperless-ngx/paperless-ngx/pull/13204))
|
||||||
|
- Fix: fix app title restoration [@shamoon](https://github.com/shamoon) ([#13208](https://github.com/paperless-ngx/paperless-ngx/pull/13208))
|
||||||
|
- Fix: handle long wrapping titles in AI chat document list [@shamoon](https://github.com/shamoon) ([#13206](https://github.com/paperless-ngx/paperless-ngx/pull/13206))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore(deps-dev): Bump postcss from 8.5.6 to 8.5.22 in /src/paperless\_mail/templates in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13236](https://github.com/paperless-ngx/paperless-ngx/pull/13236))
|
||||||
|
- Chore(deps): Bump pyasn1 from 0.6.3 to 0.6.4 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13237](https://github.com/paperless-ngx/paperless-ngx/pull/13237))
|
||||||
|
- Chore(deps): Bump the uv group across 1 directory with 8 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13189](https://github.com/paperless-ngx/paperless-ngx/pull/13189))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>17 changes</summary>
|
||||||
|
|
||||||
|
- Chore(deps-dev): Bump postcss from 8.5.6 to 8.5.22 in /src/paperless\_mail/templates in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13236](https://github.com/paperless-ngx/paperless-ngx/pull/13236))
|
||||||
|
- Fix: selection\_data re-derives the filtered document set 5 times over [@stumpylog](https://github.com/stumpylog) ([#13229](https://github.com/paperless-ngx/paperless-ngx/pull/13229))
|
||||||
|
- Chore: reword "Settings > Default permissions" description for clarity [@NotaInutilis](https://github.com/NotaInutilis) ([#13232](https://github.com/paperless-ngx/paperless-ngx/pull/13232))
|
||||||
|
- Chore(deps): Bump pyasn1 from 0.6.3 to 0.6.4 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13237](https://github.com/paperless-ngx/paperless-ngx/pull/13237))
|
||||||
|
- Chore(deps): Bump the uv group across 1 directory with 8 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13189](https://github.com/paperless-ngx/paperless-ngx/pull/13189))
|
||||||
|
- Fix: exclude source document from its own RAG similarity results [@stumpylog](https://github.com/stumpylog) ([#13233](https://github.com/paperless-ngx/paperless-ngx/pull/13233))
|
||||||
|
- Fix: split mailrule maximum\_age clamp into its own migration [@stumpylog](https://github.com/stumpylog) ([#13231](https://github.com/paperless-ngx/paperless-ngx/pull/13231))
|
||||||
|
- Chore: mark yes in confirm button for translation [@shamoon](https://github.com/shamoon) ([#13225](https://github.com/paperless-ngx/paperless-ngx/pull/13225))
|
||||||
|
- Fix: correct URL for W001 check [@shamoon](https://github.com/shamoon) ([#13220](https://github.com/paperless-ngx/paperless-ngx/pull/13220))
|
||||||
|
- Fix: ensure create dialog uses correct attribute type [@shamoon](https://github.com/shamoon) ([#13221](https://github.com/paperless-ngx/paperless-ngx/pull/13221))
|
||||||
|
- Fix: correct database engine from postgres to postgresql [@OberstVonGatow](https://github.com/OberstVonGatow) ([#13213](https://github.com/paperless-ngx/paperless-ngx/pull/13213))
|
||||||
|
- Fix: remove unnecessary .distinct() dominating document list queries [@stumpylog](https://github.com/stumpylog) ([#13205](https://github.com/paperless-ngx/paperless-ngx/pull/13205))
|
||||||
|
- Fix: also wrap non-breaking words in chat messages, handle whitespace [@shamoon](https://github.com/shamoon) ([#13211](https://github.com/paperless-ngx/paperless-ngx/pull/13211))
|
||||||
|
- Fix: tag/custom-field document\_count scales badly with tag count [@stumpylog](https://github.com/stumpylog) ([#13203](https://github.com/paperless-ngx/paperless-ngx/pull/13203))
|
||||||
|
- Fix: batch document user\_can\_change checks to avoid per-row N+1 [@stumpylog](https://github.com/stumpylog) ([#13204](https://github.com/paperless-ngx/paperless-ngx/pull/13204))
|
||||||
|
- Fix: fix app title restoration [@shamoon](https://github.com/shamoon) ([#13208](https://github.com/paperless-ngx/paperless-ngx/pull/13208))
|
||||||
|
- Fix: handle long wrapping titles in AI chat document list [@shamoon](https://github.com/shamoon) ([#13206](https://github.com/paperless-ngx/paperless-ngx/pull/13206))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.0
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- [BREAKING] Remove the positional arguments from the pre/post consume scripts [@stumpylog](https://github.com/stumpylog) ([#12573](https://github.com/paperless-ngx/paperless-ngx/pull/12573))
|
||||||
|
- [BREAKING] Decouple OCR control from archive file control [@stumpylog](https://github.com/stumpylog) ([#12448](https://github.com/paperless-ngx/paperless-ngx/pull/12448))
|
||||||
|
- [BREAKING] Chore: drop support for api versions \< 9 [@shamoon](https://github.com/shamoon) ([#12284](https://github.com/paperless-ngx/paperless-ngx/pull/12284))
|
||||||
|
- [BREAKING] Chore: Drop support for Python 3.10 [@stumpylog](https://github.com/stumpylog) ([#12234](https://github.com/paperless-ngx/paperless-ngx/pull/12234))
|
||||||
|
- [BREAKING] Chore: Refactor advanced database settings to allow more user configuration [@stumpylog](https://github.com/stumpylog) ([#12165](https://github.com/paperless-ngx/paperless-ngx/pull/12165))
|
||||||
|
- [BREAKING] Remove API v1 compatibility [@stumpylog](https://github.com/stumpylog) ([#12166](https://github.com/paperless-ngx/paperless-ngx/pull/12166))
|
||||||
|
- [BREAKING] Remove pybzar as a barcode reader [@stumpylog](https://github.com/stumpylog) ([#12065](https://github.com/paperless-ngx/paperless-ngx/pull/12065))
|
||||||
|
- [BREAKING] Remove support for document and thumbnail encryption [@stumpylog](https://github.com/stumpylog) ([#11850](https://github.com/paperless-ngx/paperless-ngx/pull/11850))
|
||||||
|
- [BREAKING] Feature: Simplify and improve the consumer [@stumpylog](https://github.com/stumpylog) ([#11753](https://github.com/paperless-ngx/paperless-ngx/pull/11753))
|
||||||
|
|
||||||
|
### Notable Changes
|
||||||
|
|
||||||
|
- Feature: Replace Whoosh with tantivy search backend [@stumpylog](https://github.com/stumpylog) ([#12471](https://github.com/paperless-ngx/paperless-ngx/pull/12471))
|
||||||
|
- Feature: document parser plugin framework [@stumpylog](https://github.com/stumpylog) ([#12294](https://github.com/paperless-ngx/paperless-ngx/pull/12294))
|
||||||
|
- Feature: document file versions [@shamoon](https://github.com/shamoon) ([#12061](https://github.com/paperless-ngx/paperless-ngx/pull/12061))
|
||||||
|
- Feature: sharelink bundles [@shamoon](https://github.com/shamoon) ([#11682](https://github.com/paperless-ngx/paperless-ngx/pull/11682))
|
||||||
|
- Feature: Paperless AI [@shamoon](https://github.com/shamoon) ([#10319](https://github.com/paperless-ngx/paperless-ngx/pull/10319))
|
||||||
|
- Feature: Remote OCR (Azure AI) [@shamoon](https://github.com/shamoon) ([#10320](https://github.com/paperless-ngx/paperless-ngx/pull/10320))
|
||||||
|
|
||||||
|
### Features / Enhancements
|
||||||
|
|
||||||
|
- [Beta] Paperless-ngx v3.0.0 Beta [@shamoon](https://github.com/shamoon) ([#12713](https://github.com/paperless-ngx/paperless-ngx/pull/12713))
|
||||||
|
- Tweakhancement (beta): allow some bulk operations with all [@shamoon](https://github.com/shamoon) ([#13193](https://github.com/paperless-ngx/paperless-ngx/pull/13193))
|
||||||
|
- Security (beta): enforce current permissions in autocomplete [@shamoon](https://github.com/shamoon) ([#13188](https://github.com/paperless-ngx/paperless-ngx/pull/13188))
|
||||||
|
- Security (beta): bound email linkification [@shamoon](https://github.com/shamoon) ([#13187](https://github.com/paperless-ngx/paperless-ngx/pull/13187))
|
||||||
|
- Change: update root modified timestamp on version changes [@shamoon](https://github.com/shamoon) ([#13170](https://github.com/paperless-ngx/paperless-ngx/pull/13170))
|
||||||
|
- Performance: add DB indexes for common query/sort patterns [@stumpylog](https://github.com/stumpylog) ([#13167](https://github.com/paperless-ngx/paperless-ngx/pull/13167))
|
||||||
|
- Performance: use NgOptimizedImage for thumbnail lazy loading [@shamoon](https://github.com/shamoon) ([#13169](https://github.com/paperless-ngx/paperless-ngx/pull/13169))
|
||||||
|
- Performance: Tantivy indexing optimization [@Merinorus](https://github.com/Merinorus) ([#13053](https://github.com/paperless-ngx/paperless-ngx/pull/13053))
|
||||||
|
- Chorehancement: update to Angular v22, 'zoneless' / 'reactive' [@shamoon](https://github.com/shamoon) ([#13114](https://github.com/paperless-ngx/paperless-ngx/pull/13114))
|
||||||
|
- Enhancement (beta): support LLM timeout config [@shamoon](https://github.com/shamoon) ([#13002](https://github.com/paperless-ngx/paperless-ngx/pull/13002))
|
||||||
|
- Enhancement: ignore diacritics, support multiple substring matching for UI filtering [@shamoon](https://github.com/shamoon) ([#13021](https://github.com/paperless-ngx/paperless-ngx/pull/13021))
|
||||||
|
- Enhancement(beta): replace LanceDB vector store with sqlite-vec [@stumpylog](https://github.com/stumpylog) ([#12990](https://github.com/paperless-ngx/paperless-ngx/pull/12990))
|
||||||
|
- Enhancement (beta): Switch the AI vector store to LanceDB [@stumpylog](https://github.com/stumpylog) ([#12944](https://github.com/paperless-ngx/paperless-ngx/pull/12944))
|
||||||
|
- Fixhancement (beta): tasks dismiss all [@shamoon](https://github.com/shamoon) ([#12949](https://github.com/paperless-ngx/paperless-ngx/pull/12949))
|
||||||
|
- Enhancement (beta): add direct LLM language setting [@shamoon](https://github.com/shamoon) ([#12906](https://github.com/paperless-ngx/paperless-ngx/pull/12906))
|
||||||
|
- Enhancement: try to respect language for AI suggestions [@shamoon](https://github.com/shamoon) ([#12894](https://github.com/paperless-ngx/paperless-ngx/pull/12894))
|
||||||
|
- Enhancement: AI LLM chunk size and context window config [@shamoon](https://github.com/shamoon) ([#12891](https://github.com/paperless-ngx/paperless-ngx/pull/12891))
|
||||||
|
- Enhancement: support ollama embeddings [@shamoon](https://github.com/shamoon) ([#12753](https://github.com/paperless-ngx/paperless-ngx/pull/12753))
|
||||||
|
- Tweakhancment: Include the last applied 'documents' migration in the log [@stumpylog](https://github.com/stumpylog) ([#12757](https://github.com/paperless-ngx/paperless-ngx/pull/12757))
|
||||||
|
- Enhancement: version-aware thumbnail etag [@shamoon](https://github.com/shamoon) ([#12754](https://github.com/paperless-ngx/paperless-ngx/pull/12754))
|
||||||
|
- Feature: Further reduce document importer memory usage [@stumpylog](https://github.com/stumpylog) ([#12707](https://github.com/paperless-ngx/paperless-ngx/pull/12707))
|
||||||
|
- Tweakhancement: use fixed position instead of display none for printing [@shamoon](https://github.com/shamoon) ([#12706](https://github.com/paperless-ngx/paperless-ngx/pull/12706))
|
||||||
|
- Enhancement: Paperless-ngx v3 Logo [@shamoon](https://github.com/shamoon) ([#12673](https://github.com/paperless-ngx/paperless-ngx/pull/12673))
|
||||||
|
- Tweakhancement: localize some more task result messages [@shamoon](https://github.com/shamoon) ([#12672](https://github.com/paperless-ngx/paperless-ngx/pull/12672))
|
||||||
|
- Enhancement: chat message document links [@shamoon](https://github.com/shamoon) ([#12670](https://github.com/paperless-ngx/paperless-ngx/pull/12670))
|
||||||
|
- Tweakhancement: make upload notification open an anchor link [@shamoon](https://github.com/shamoon) ([#12659](https://github.com/paperless-ngx/paperless-ngx/pull/12659))
|
||||||
|
- Tweakhancement: tweak tasks UI, make open doc an anchor [@shamoon](https://github.com/shamoon) ([#12658](https://github.com/paperless-ngx/paperless-ngx/pull/12658))
|
||||||
|
- Enhancement: show small task summary in system status [@shamoon](https://github.com/shamoon) ([#12634](https://github.com/paperless-ngx/paperless-ngx/pull/12634))
|
||||||
|
- Enhancement: new Tasks UI [@shamoon](https://github.com/shamoon) ([#12614](https://github.com/paperless-ngx/paperless-ngx/pull/12614))
|
||||||
|
- Feature: Allow monitoring access to tasks summary [@stumpylog](https://github.com/stumpylog) ([#12624](https://github.com/paperless-ngx/paperless-ngx/pull/12624))
|
||||||
|
- Tweak: remove stale index warning [@shamoon](https://github.com/shamoon) ([#12616](https://github.com/paperless-ngx/paperless-ngx/pull/12616))
|
||||||
|
- Enhancement: add highlighting to title + content searches [@shamoon](https://github.com/shamoon) ([#12593](https://github.com/paperless-ngx/paperless-ngx/pull/12593))
|
||||||
|
- Feature: Redesign the task system [@stumpylog](https://github.com/stumpylog) ([#12584](https://github.com/paperless-ngx/paperless-ngx/pull/12584))
|
||||||
|
- Feature: Tune SQLite default settings for increased speed [@stumpylog](https://github.com/stumpylog) ([#12580](https://github.com/paperless-ngx/paperless-ngx/pull/12580))
|
||||||
|
- Feature: Document fuzzy match improvements [@stumpylog](https://github.com/stumpylog) ([#12579](https://github.com/paperless-ngx/paperless-ngx/pull/12579))
|
||||||
|
- Enhancement: validate and sanitize uploaded logos [@shamoon](https://github.com/shamoon) ([#12551](https://github.com/paperless-ngx/paperless-ngx/pull/12551))
|
||||||
|
- Enhancement: add view\_global\_statistics and view\_system\_status permissions [@shamoon](https://github.com/shamoon) ([#12530](https://github.com/paperless-ngx/paperless-ngx/pull/12530))
|
||||||
|
- Fixhancement: include sharelinks + bundles in export/import [@shamoon](https://github.com/shamoon) ([#12479](https://github.com/paperless-ngx/paperless-ngx/pull/12479))
|
||||||
|
- Enhancement: unify text search to use tantivy [@shamoon](https://github.com/shamoon) ([#12485](https://github.com/paperless-ngx/paperless-ngx/pull/12485))
|
||||||
|
- Feature: Consumer logging correlation [@stumpylog](https://github.com/stumpylog) ([#12510](https://github.com/paperless-ngx/paperless-ngx/pull/12510))
|
||||||
|
- Security enhancement: allow opt-in blocking internal mail hosts [@shamoon](https://github.com/shamoon) ([#12502](https://github.com/paperless-ngx/paperless-ngx/pull/12502))
|
||||||
|
- Fixhancement: include trashed documents in document exporter/importer [@JanKleine](https://github.com/JanKleine) ([#12425](https://github.com/paperless-ngx/paperless-ngx/pull/12425))
|
||||||
|
- Tweakhancement: show file extension in StoragePath test [@shamoon](https://github.com/shamoon) ([#12452](https://github.com/paperless-ngx/paperless-ngx/pull/12452))
|
||||||
|
- Feature: Transition all checksums to use SHA256 [@stumpylog](https://github.com/stumpylog) ([#12432](https://github.com/paperless-ngx/paperless-ngx/pull/12432))
|
||||||
|
- Enhancement: auto-hide the search bar on mobile [@shamoon](https://github.com/shamoon) ([#12404](https://github.com/paperless-ngx/paperless-ngx/pull/12404))
|
||||||
|
- Change: sort custom fields alphabetically by default [@shamoon](https://github.com/shamoon) ([#12358](https://github.com/paperless-ngx/paperless-ngx/pull/12358))
|
||||||
|
- Feature: Add progress information to the classifier training for a better ux [@stumpylog](https://github.com/stumpylog) ([#12331](https://github.com/paperless-ngx/paperless-ngx/pull/12331))
|
||||||
|
- Enhancement: Make the StatusConsumer truly async [@stumpylog](https://github.com/stumpylog) ([#12298](https://github.com/paperless-ngx/paperless-ngx/pull/12298))
|
||||||
|
- Feature: Migrate import/export to rich progress [@stumpylog](https://github.com/stumpylog) ([#12260](https://github.com/paperless-ngx/paperless-ngx/pull/12260))
|
||||||
|
- Enhancement: Show more document details in merge dialog [@svenstaro](https://github.com/svenstaro) ([#12271](https://github.com/paperless-ngx/paperless-ngx/pull/12271))
|
||||||
|
- Enhancement: saved view sharing [@shamoon](https://github.com/shamoon) ([#12142](https://github.com/paperless-ngx/paperless-ngx/pull/12142))
|
||||||
|
- Enhancement: “live” document updates [@shamoon](https://github.com/shamoon) ([#12141](https://github.com/paperless-ngx/paperless-ngx/pull/12141))
|
||||||
|
- Enhancement: Improve the retagger output using rich [@stumpylog](https://github.com/stumpylog) ([#12194](https://github.com/paperless-ngx/paperless-ngx/pull/12194))
|
||||||
|
- Enhancement: Transition sanity check to rich and improve output [@stumpylog](https://github.com/stumpylog) ([#12182](https://github.com/paperless-ngx/paperless-ngx/pull/12182))
|
||||||
|
- Enhancement: Switch all indexing to use rich [@stumpylog](https://github.com/stumpylog) ([#12193](https://github.com/paperless-ngx/paperless-ngx/pull/12193))
|
||||||
|
- Feature: Switch progress bar library to rich [@stumpylog](https://github.com/stumpylog) ([#12169](https://github.com/paperless-ngx/paperless-ngx/pull/12169))
|
||||||
|
- Enhancement: Formatted filename for single document downloads [@JanKleine](https://github.com/JanKleine) ([#12095](https://github.com/paperless-ngx/paperless-ngx/pull/12095))
|
||||||
|
- Enhancement: prevent duplicate mail processing across rules [@shamoon](https://github.com/shamoon) ([#12159](https://github.com/paperless-ngx/paperless-ngx/pull/12159))
|
||||||
|
- Feature: move to trash action for workflows [@JanKleine](https://github.com/JanKleine) ([#11176](https://github.com/paperless-ngx/paperless-ngx/pull/11176))
|
||||||
|
- Tweakhancement: reset to page 1 on reset filters [@shamoon](https://github.com/shamoon) ([#12143](https://github.com/paperless-ngx/paperless-ngx/pull/12143))
|
||||||
|
- Enhancement: consolidate management lists into document attributes section [@shamoon](https://github.com/shamoon) ([#12045](https://github.com/paperless-ngx/paperless-ngx/pull/12045))
|
||||||
|
- Enhancement: option to stop processing further mail rules [@shamoon](https://github.com/shamoon) ([#12053](https://github.com/paperless-ngx/paperless-ngx/pull/12053))
|
||||||
|
- Tweak: improve 2-digit year parsing [@shamoon](https://github.com/shamoon) ([#12044](https://github.com/paperless-ngx/paperless-ngx/pull/12044))
|
||||||
|
- Enhancement: pngx pdf viewer [@shamoon](https://github.com/shamoon) ([#12043](https://github.com/paperless-ngx/paperless-ngx/pull/12043))
|
||||||
|
- Fixhancement: improve ASN handling with PDF operations [@shamoon](https://github.com/shamoon) ([#11689](https://github.com/paperless-ngx/paperless-ngx/pull/11689))
|
||||||
|
- Feature: Enable users to customize date parsing via plugins [@stumpylog](https://github.com/stumpylog) ([#11931](https://github.com/paperless-ngx/paperless-ngx/pull/11931))
|
||||||
|
- Feature: password removal workflow action [@shamoon](https://github.com/shamoon) ([#11665](https://github.com/paperless-ngx/paperless-ngx/pull/11665))
|
||||||
|
- Enhancement: per-type object page sizing [@shamoon](https://github.com/shamoon) ([#11977](https://github.com/paperless-ngx/paperless-ngx/pull/11977))
|
||||||
|
- Enhancement: improve filter drop-down performance with virtual scrolling [@shamoon](https://github.com/shamoon) ([#11973](https://github.com/paperless-ngx/paperless-ngx/pull/11973))
|
||||||
|
- Tweakhancement: tweak bulk delete text [@shamoon](https://github.com/shamoon) ([#11967](https://github.com/paperless-ngx/paperless-ngx/pull/11967))
|
||||||
|
- Feature: support split documents based on tag barcodes @schober-ch ([#11645](https://github.com/paperless-ngx/paperless-ngx/pull/11645))
|
||||||
|
- Enhancement: Add setting for default PDF Editor mode [@JanKleine](https://github.com/JanKleine) ([#11927](https://github.com/paperless-ngx/paperless-ngx/pull/11927))
|
||||||
|
- Fixhancement: auto-queue llm index if needed [@shamoon](https://github.com/shamoon) ([#11891](https://github.com/paperless-ngx/paperless-ngx/pull/11891))
|
||||||
|
- Enhancement: user control of doc details fields [@shamoon](https://github.com/shamoon) ([#11906](https://github.com/paperless-ngx/paperless-ngx/pull/11906))
|
||||||
|
- Enhancement: improve relative dates in date filter [@JanKleine](https://github.com/JanKleine) ([#11899](https://github.com/paperless-ngx/paperless-ngx/pull/11899))
|
||||||
|
- Enhancement: allow duplicates with warnings, UI for discovery [@shamoon](https://github.com/shamoon) ([#11815](https://github.com/paperless-ngx/paperless-ngx/pull/11815))
|
||||||
|
- Enhancement: configurable SSO groups claim [@Gabgobie](https://github.com/Gabgobie) ([#11841](https://github.com/paperless-ngx/paperless-ngx/pull/11841))
|
||||||
|
- Enhancement: support select all for management lists [@shamoon](https://github.com/shamoon) ([#11889](https://github.com/paperless-ngx/paperless-ngx/pull/11889))
|
||||||
|
- Tweakhancement: display document id, with copy [@shamoon](https://github.com/shamoon) ([#11896](https://github.com/paperless-ngx/paperless-ngx/pull/11896))
|
||||||
|
- Enhancement: Add support for app oidc [@paulgessinger](https://github.com/paulgessinger) ([#11756](https://github.com/paperless-ngx/paperless-ngx/pull/11756))
|
||||||
|
- Enhancement: Add 'any of' workflow trigger filters [@shamoon](https://github.com/shamoon) ([#11683](https://github.com/paperless-ngx/paperless-ngx/pull/11683))
|
||||||
|
- Enhancement: support doc\_id placeholder in workflow templates [@shamoon](https://github.com/shamoon) ([#11847](https://github.com/paperless-ngx/paperless-ngx/pull/11847))
|
||||||
|
- Performance: improve treenode inefficiencies [@shamoon](https://github.com/shamoon) ([#11606](https://github.com/paperless-ngx/paperless-ngx/pull/11606))
|
||||||
|
- Feature: password removal action [@shamoon](https://github.com/shamoon) ([#11656](https://github.com/paperless-ngx/paperless-ngx/pull/11656))
|
||||||
|
- Tweakhancement: use anchor element for management list quick filter buttons [@shamoon](https://github.com/shamoon) ([#11692](https://github.com/paperless-ngx/paperless-ngx/pull/11692))
|
||||||
|
- Feature: Indonesian translation [@shamoon](https://github.com/shamoon) ([#11641](https://github.com/paperless-ngx/paperless-ngx/pull/11641))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix (beta): prevent pdfjs offsetParent warning [@shamoon](https://github.com/shamoon) ([#13197](https://github.com/paperless-ngx/paperless-ngx/pull/13197))
|
||||||
|
- Fix (beta): dont include hidden buttons in virtual scroll container height [@shamoon](https://github.com/shamoon) ([#13194](https://github.com/paperless-ngx/paperless-ngx/pull/13194))
|
||||||
|
- Fix (beta): fix custom field bulk editing for 'all' [@shamoon](https://github.com/shamoon) ([#13191](https://github.com/paperless-ngx/paperless-ngx/pull/13191))
|
||||||
|
- Security (beta): two small permission check fixes [@shamoon](https://github.com/shamoon) ([#13186](https://github.com/paperless-ngx/paperless-ngx/pull/13186))
|
||||||
|
- Fix (beta): short-circuit ObjectOwnedOrGrantedPermissionsFilter for superusers [@stumpylog](https://github.com/stumpylog) ([#13183](https://github.com/paperless-ngx/paperless-ngx/pull/13183))
|
||||||
|
- Fix (beta): compute num\_notes via a subquery instead of Count()+distinct [@stumpylog](https://github.com/stumpylog) ([#13182](https://github.com/paperless-ngx/paperless-ngx/pull/13182))
|
||||||
|
- Fix: prevent tag assignment from reverting other pending workflow assignments [@stumpylog](https://github.com/stumpylog) ([#13178](https://github.com/paperless-ngx/paperless-ngx/pull/13178))
|
||||||
|
- Fix(beta): cache per-request effective-document resolution for thumb/metadata/preview [@stumpylog](https://github.com/stumpylog) ([#13166](https://github.com/paperless-ngx/paperless-ngx/pull/13166))
|
||||||
|
- Fix: fallback from archive version for share links [@shamoon](https://github.com/shamoon) ([#13163](https://github.com/paperless-ngx/paperless-ngx/pull/13163))
|
||||||
|
- Fix: better handle saved view errors in dashboard widgets [@shamoon](https://github.com/shamoon) ([#13164](https://github.com/paperless-ngx/paperless-ngx/pull/13164))
|
||||||
|
- Fix (beta): convert chat component to signal-backed Angular [@shamoon](https://github.com/shamoon) ([#13152](https://github.com/paperless-ngx/paperless-ngx/pull/13152))
|
||||||
|
- Fix: HTTP 500 filtering documents by tags + custom field query on MariaDB [@stumpylog](https://github.com/stumpylog) ([#13143](https://github.com/paperless-ngx/paperless-ngx/pull/13143))
|
||||||
|
- Fix: scope ProcessedMail UID deduplication by IMAP UIDVALIDITY [@chouquette](https://github.com/chouquette) ([#12839](https://github.com/paperless-ngx/paperless-ngx/pull/12839))
|
||||||
|
- Fix: allow setting any UID and GID when running rootless [@Tahvok](https://github.com/Tahvok) ([#13090](https://github.com/paperless-ngx/paperless-ngx/pull/13090))
|
||||||
|
- Fix (beta): fix sidebar document close button [@shamoon](https://github.com/shamoon) ([#13136](https://github.com/paperless-ngx/paperless-ngx/pull/13136))
|
||||||
|
- Fix: include wasm assets in pdf.js-based viewer [@shamoon](https://github.com/shamoon) ([#13122](https://github.com/paperless-ngx/paperless-ngx/pull/13122))
|
||||||
|
- Fix: fix ui labels from md5 to sha256 [@shamoon](https://github.com/shamoon) ([#13119](https://github.com/paperless-ngx/paperless-ngx/pull/13119))
|
||||||
|
- Fixhancement: dont assign empty title in workflow with broken template [@shamoon](https://github.com/shamoon) ([#13112](https://github.com/paperless-ngx/paperless-ngx/pull/13112))
|
||||||
|
- Fix: clamp mailrule max age before migration [@shamoon](https://github.com/shamoon) ([#13093](https://github.com/paperless-ngx/paperless-ngx/pull/13093))
|
||||||
|
- Fix: use latest document version for bulk download [@shamoon](https://github.com/shamoon) ([#13103](https://github.com/paperless-ngx/paperless-ngx/pull/13103))
|
||||||
|
- Fix (beta): handle relative date with comma query [@shamoon](https://github.com/shamoon) ([#13087](https://github.com/paperless-ngx/paperless-ngx/pull/13087))
|
||||||
|
- Fix (beta): fix sanity checker affected document counts [@NgoQuocViet2001](https://github.com/NgoQuocViet2001) ([#13052](https://github.com/paperless-ngx/paperless-ngx/pull/13052))
|
||||||
|
- Fix (beta): preserve Unicode in localization prompt [@shamoon](https://github.com/shamoon) ([#13055](https://github.com/paperless-ngx/paperless-ngx/pull/13055))
|
||||||
|
- Fix: change tantivy token limit to title field max length [@shamoon](https://github.com/shamoon) ([#13046](https://github.com/paperless-ngx/paperless-ngx/pull/13046))
|
||||||
|
- Fix: include last-modified in doc etag [@shamoon](https://github.com/shamoon) ([#13044](https://github.com/paperless-ngx/paperless-ngx/pull/13044))
|
||||||
|
- Fix (beta): Stream chunks during compaction to prevent oom on smaller installs [@stumpylog](https://github.com/stumpylog) ([#13014](https://github.com/paperless-ngx/paperless-ngx/pull/13014))
|
||||||
|
- Fixhancement (beta): truncate embedding queries for small chunk size [@shamoon](https://github.com/shamoon) ([#13028](https://github.com/paperless-ngx/paperless-ngx/pull/13028))
|
||||||
|
- Fix(beta): Workaround a Tantivy panic in more like this searching [@stumpylog](https://github.com/stumpylog) ([#13026](https://github.com/paperless-ngx/paperless-ngx/pull/13026))
|
||||||
|
- Fix (beta): Catch consumer files created during watcher re-creations [@stumpylog](https://github.com/stumpylog) ([#13013](https://github.com/paperless-ngx/paperless-ngx/pull/13013))
|
||||||
|
- Fix (beta): restore v2 (Whoosh) advanced-search query compatibility [@stumpylog](https://github.com/stumpylog) ([#13010](https://github.com/paperless-ngx/paperless-ngx/pull/13010))
|
||||||
|
- Fix (beta): retry celery ping and report warning on no response [@shamoon](https://github.com/shamoon) ([#13012](https://github.com/paperless-ngx/paperless-ngx/pull/13012))
|
||||||
|
- Fix (beta): don't send chat message on Enter while composing with IME (CJK) [@mzyy94](https://github.com/mzyy94) ([#12999](https://github.com/paperless-ngx/paperless-ngx/pull/12999))
|
||||||
|
- Fix: Apply unicode normalization to all paths and path components [@stumpylog](https://github.com/stumpylog) ([#12993](https://github.com/paperless-ngx/paperless-ngx/pull/12993))
|
||||||
|
- Fix: Handle the UTF 16 and BOM text files better [@stumpylog](https://github.com/stumpylog) ([#12994](https://github.com/paperless-ngx/paperless-ngx/pull/12994))
|
||||||
|
- Fix (beta): avoid DRF update calling `save` on all fields [@shamoon](https://github.com/shamoon) ([#12992](https://github.com/paperless-ngx/paperless-ngx/pull/12992))
|
||||||
|
- Fix: release pooled DB connection during AI LLM/embedding calls [@stumpylog](https://github.com/stumpylog) ([#12983](https://github.com/paperless-ngx/paperless-ngx/pull/12983))
|
||||||
|
- Fix: health-check pooled DB connections and close the pool on worker shutdown [@stumpylog](https://github.com/stumpylog) ([#12977](https://github.com/paperless-ngx/paperless-ngx/pull/12977))
|
||||||
|
- Fix: wrap long titles in delete confirm dialog [@shamoon](https://github.com/shamoon) ([#12973](https://github.com/paperless-ngx/paperless-ngx/pull/12973))
|
||||||
|
- Fix (beta): fix re-ordering in merge dialog [@shamoon](https://github.com/shamoon) ([#12967](https://github.com/paperless-ngx/paperless-ngx/pull/12967))
|
||||||
|
- Fix (beta): move task filtering to backend fully [@shamoon](https://github.com/shamoon) ([#12956](https://github.com/paperless-ngx/paperless-ngx/pull/12956))
|
||||||
|
- Fix (beta): avoid unnecessary creating new PDF with pw removal workflow [@shamoon](https://github.com/shamoon) ([#12948](https://github.com/paperless-ngx/paperless-ngx/pull/12948))
|
||||||
|
- Fix (beta): correct chat message bg color [@shamoon](https://github.com/shamoon) ([#12955](https://github.com/paperless-ngx/paperless-ngx/pull/12955))
|
||||||
|
- Fix (beta): respect disable state for suggest endpoint, require change perms [@shamoon](https://github.com/shamoon) ([#12942](https://github.com/paperless-ngx/paperless-ngx/pull/12942))
|
||||||
|
- Fix: Ensure checksum comparison is using SHA256 [@stumpylog](https://github.com/stumpylog) ([#12939](https://github.com/paperless-ngx/paperless-ngx/pull/12939))
|
||||||
|
- Fix: Restrict the AI backend at settings configure time [@stumpylog](https://github.com/stumpylog) ([#12903](https://github.com/paperless-ngx/paperless-ngx/pull/12903))
|
||||||
|
- Fix (beta): correctly apply i18n in suggestions dropdown [@shamoon](https://github.com/shamoon) ([#12905](https://github.com/paperless-ngx/paperless-ngx/pull/12905))
|
||||||
|
- Fix: Lock AI index during reading and don't index documents many times during a bulk update [@stumpylog](https://github.com/stumpylog) ([#12899](https://github.com/paperless-ngx/paperless-ngx/pull/12899))
|
||||||
|
- Fix: Minor fixes for the AI indexing [@stumpylog](https://github.com/stumpylog) ([#12893](https://github.com/paperless-ngx/paperless-ngx/pull/12893))
|
||||||
|
- Fix (beta): dont use tool calling with ollama [@shamoon](https://github.com/shamoon) ([#12896](https://github.com/paperless-ngx/paperless-ngx/pull/12896))
|
||||||
|
- Fix: Improvements for security around the AI [@stumpylog](https://github.com/stumpylog) ([#12895](https://github.com/paperless-ngx/paperless-ngx/pull/12895))
|
||||||
|
- Fix: correctly show timestamp tooltip on history item [@nathanaelhoun](https://github.com/nathanaelhoun) ([#12879](https://github.com/paperless-ngx/paperless-ngx/pull/12879))
|
||||||
|
- Fix: Fold query and autocomplete terms with Tantivy's ascii\_fold so special letters match [@stumpylog](https://github.com/stumpylog) ([#12868](https://github.com/paperless-ngx/paperless-ngx/pull/12868))
|
||||||
|
- Fix: Don't store autocomplete\_word, only index it [@stumpylog](https://github.com/stumpylog) ([#12867](https://github.com/paperless-ngx/paperless-ngx/pull/12867))
|
||||||
|
- Fix: Preserve Whoosh date range swapping in Tantviy [@stumpylog](https://github.com/stumpylog) ([#12866](https://github.com/paperless-ngx/paperless-ngx/pull/12866))
|
||||||
|
- Fix: Always release search index writer [@stumpylog](https://github.com/stumpylog) ([#12865](https://github.com/paperless-ngx/paperless-ngx/pull/12865))
|
||||||
|
- Fix: Handle CJK title, content and metadata searching [@stumpylog](https://github.com/stumpylog) ([#12862](https://github.com/paperless-ngx/paperless-ngx/pull/12862))
|
||||||
|
- Fix: Restrict date query rewrites to date or datetime fields only [@stumpylog](https://github.com/stumpylog) ([#12864](https://github.com/paperless-ngx/paperless-ngx/pull/12864))
|
||||||
|
- Fix: Missing call to tanvity wait\_merging\_threads [@stumpylog](https://github.com/stumpylog) ([#12863](https://github.com/paperless-ngx/paperless-ngx/pull/12863))
|
||||||
|
- Fix: Handle tanvity index lock contention [@stumpylog](https://github.com/stumpylog) ([#12856](https://github.com/paperless-ngx/paperless-ngx/pull/12856))
|
||||||
|
- Fix (beta): better catch chat errors [@shamoon](https://github.com/shamoon) ([#12854](https://github.com/paperless-ngx/paperless-ngx/pull/12854))
|
||||||
|
- Security (beta): fixes for v3 beta [@shamoon](https://github.com/shamoon) ([#12838](https://github.com/paperless-ngx/paperless-ngx/pull/12838))
|
||||||
|
- Fix (beta): normalize long punctuation chunks to improve embedding [@shamoon](https://github.com/shamoon) ([#12848](https://github.com/paperless-ngx/paperless-ngx/pull/12848))
|
||||||
|
- Fix: use chord.on\_error before apply\_async [@shamoon](https://github.com/shamoon) ([#12842](https://github.com/paperless-ngx/paperless-ngx/pull/12842))
|
||||||
|
- Fix: Don't use smaller integer fields for some workflow fields [@stumpylog](https://github.com/stumpylog) ([#12834](https://github.com/paperless-ngx/paperless-ngx/pull/12834))
|
||||||
|
- Fix: Password removal source file location [@stumpylog](https://github.com/stumpylog) ([#12830](https://github.com/paperless-ngx/paperless-ngx/pull/12830))
|
||||||
|
- Fix (beta): use correct html button type for custom field buttons [@shamoon](https://github.com/shamoon) ([#12819](https://github.com/paperless-ngx/paperless-ngx/pull/12819))
|
||||||
|
- Fix: Defer password removal workflow action until the file is in place [@stumpylog](https://github.com/stumpylog) ([#12814](https://github.com/paperless-ngx/paperless-ngx/pull/12814))
|
||||||
|
- Fix: Allow setting allauth rate limit configuration settings [@stumpylog](https://github.com/stumpylog) ([#12798](https://github.com/paperless-ngx/paperless-ngx/pull/12798))
|
||||||
|
- Fix: Don't embed the metadata which is already embedded into the context [@stumpylog](https://github.com/stumpylog) ([#12795](https://github.com/paperless-ngx/paperless-ngx/pull/12795))
|
||||||
|
- Fix: Sanitize dash or plus from the text search path [@stumpylog](https://github.com/stumpylog) ([#12789](https://github.com/paperless-ngx/paperless-ngx/pull/12789))
|
||||||
|
- Fix: improve new tasks ui layout across screen sizes [@shamoon](https://github.com/shamoon) ([#12784](https://github.com/paperless-ngx/paperless-ngx/pull/12784))
|
||||||
|
- Fix: Update parser contract to require empty strings, not None [@shamoon](https://github.com/shamoon) ([#12775](https://github.com/paperless-ngx/paperless-ngx/pull/12775))
|
||||||
|
- Fix: Use a persistent, writeable location for hugging face models [@stumpylog](https://github.com/stumpylog) ([#12771](https://github.com/paperless-ngx/paperless-ngx/pull/12771))
|
||||||
|
- Fix: create LLM\_INDEX\_DIR before writing meta.json on first run [@pReya](https://github.com/pReya) ([#12759](https://github.com/paperless-ngx/paperless-ngx/pull/12759))
|
||||||
|
- Fix: use response synthesizer for RAG doc chat [@shamoon](https://github.com/shamoon) ([#12751](https://github.com/paperless-ngx/paperless-ngx/pull/12751))
|
||||||
|
- Fix: only update modified field in notes actions [@shamoon](https://github.com/shamoon) ([#12750](https://github.com/paperless-ngx/paperless-ngx/pull/12750))
|
||||||
|
- Fix: Handle dash or plus operators in search queries [@stumpylog](https://github.com/stumpylog) ([#12734](https://github.com/paperless-ngx/paperless-ngx/pull/12734))
|
||||||
|
- Fixes trash preview when a document has deleted versions [@stumpylog](https://github.com/stumpylog) ([#12742](https://github.com/paperless-ngx/paperless-ngx/pull/12742))
|
||||||
|
- Fix: exclude versions from stats count [@shamoon](https://github.com/shamoon) ([#12738](https://github.com/paperless-ngx/paperless-ngx/pull/12738))
|
||||||
|
- Fixes celery chords by using Redis as our result backend [@stumpylog](https://github.com/stumpylog) ([#12741](https://github.com/paperless-ngx/paperless-ngx/pull/12741))
|
||||||
|
- Fix: bump version.py to 3.0.0 also [@shamoon](https://github.com/shamoon) ([#12736](https://github.com/paperless-ngx/paperless-ngx/pull/12736))
|
||||||
|
- Fix: makes the font cache folder writeable to all users [@stumpylog](https://github.com/stumpylog) ([#12726](https://github.com/paperless-ngx/paperless-ngx/pull/12726))
|
||||||
|
- Fixes Whoosh year only queries to be rewritten to Tantivy date syntax [@stumpylog](https://github.com/stumpylog) ([#12725](https://github.com/paperless-ngx/paperless-ngx/pull/12725))
|
||||||
|
- Fix: pass allow parallel tool calls in LLM client [@shamoon](https://github.com/shamoon) ([#12718](https://github.com/paperless-ngx/paperless-ngx/pull/12718))
|
||||||
|
- Fix: Handle passwords for removal action as lists [@stumpylog](https://github.com/stumpylog) ([#12716](https://github.com/paperless-ngx/paperless-ngx/pull/12716))
|
||||||
|
- Bumps all our versions to 3.0.0 [@stumpylog](https://github.com/stumpylog) ([#12715](https://github.com/paperless-ngx/paperless-ngx/pull/12715))
|
||||||
|
- Fix: avoid unnecessary close\_old\_connections in Celery task dispatch [@stumpylog](https://github.com/stumpylog) ([#12701](https://github.com/paperless-ngx/paperless-ngx/pull/12701))
|
||||||
|
- Fix: apply tag changes directly to document in db [@shamoon](https://github.com/shamoon) ([#12664](https://github.com/paperless-ngx/paperless-ngx/pull/12664))
|
||||||
|
- Fix: Use FileResponse for file API responses [@stumpylog](https://github.com/stumpylog) ([#12638](https://github.com/paperless-ngx/paperless-ngx/pull/12638))
|
||||||
|
- Fix: Changes bare metal webserver to use uvloop [@stumpylog](https://github.com/stumpylog) ([#12626](https://github.com/paperless-ngx/paperless-ngx/pull/12626))
|
||||||
|
- Fixhancement: Exact custom field monetary exact searching [@stumpylog](https://github.com/stumpylog) ([#12592](https://github.com/paperless-ngx/paperless-ngx/pull/12592))
|
||||||
|
- Fix: relative added date check visibility [@shamoon](https://github.com/shamoon) ([#12600](https://github.com/paperless-ngx/paperless-ngx/pull/12600))
|
||||||
|
- Fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type [@ggouzi](https://github.com/ggouzi) ([#12597](https://github.com/paperless-ngx/paperless-ngx/pull/12597))
|
||||||
|
- Fix: reject invalid requests to API notes endpoint [@ggouzi](https://github.com/ggouzi) ([#12582](https://github.com/paperless-ngx/paperless-ngx/pull/12582))
|
||||||
|
- Fix: Respect workflow change\_groups during consumption [@stumpylog](https://github.com/stumpylog) ([#12431](https://github.com/paperless-ngx/paperless-ngx/pull/12431))
|
||||||
|
- Fix: Don't try to update the user when running as non-root [@stumpylog](https://github.com/stumpylog) ([#12365](https://github.com/paperless-ngx/paperless-ngx/pull/12365))
|
||||||
|
- Fix: use maxsplit=1 in Redis URL parsing to handle URLs with multiple colons [@cryptomilk](https://github.com/cryptomilk) ([#12239](https://github.com/paperless-ngx/paperless-ngx/pull/12239))
|
||||||
|
- Fixhancement: show sequential + id version labels, fix padding [@shamoon](https://github.com/shamoon) ([#12196](https://github.com/paperless-ngx/paperless-ngx/pull/12196))
|
||||||
|
- Fix: correct user dropdown button icon styling [@shamoon](https://github.com/shamoon) ([#12092](https://github.com/paperless-ngx/paperless-ngx/pull/12092))
|
||||||
|
- Fix: Handle an OOM kill of celery workers in the task handler [@stumpylog](https://github.com/stumpylog) ([#12040](https://github.com/paperless-ngx/paperless-ngx/pull/12040))
|
||||||
|
- Fix: re-run ASN check after barcode detection [@shamoon](https://github.com/shamoon) ([#11681](https://github.com/paperless-ngx/paperless-ngx/pull/11681))
|
||||||
|
- Fix: prevent infinite loading crash in mail component [@shamoon](https://github.com/shamoon) ([#11978](https://github.com/paperless-ngx/paperless-ngx/pull/11978))
|
||||||
|
- Fix: fix broken docker create\_classifier command in 2.20.6 [@shamoon](https://github.com/shamoon) ([#11965](https://github.com/paperless-ngx/paperless-ngx/pull/11965))
|
||||||
|
- Fix: ensure css color-scheme for dark mode [@shamoon](https://github.com/shamoon) ([#11855](https://github.com/paperless-ngx/paperless-ngx/pull/11855))
|
||||||
|
- Fix: fix tag list horizontal scroll, again [@shamoon](https://github.com/shamoon) ([#11839](https://github.com/paperless-ngx/paperless-ngx/pull/11839))
|
||||||
|
- Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent [@shamoon](https://github.com/shamoon) ([#11811](https://github.com/paperless-ngx/paperless-ngx/pull/11811))
|
||||||
|
- Fix: use explicit order field for workflow actions [@shamoon](https://github.com/shamoon) ([#11781](https://github.com/paperless-ngx/paperless-ngx/pull/11781))
|
||||||
|
- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735))
|
||||||
|
- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731))
|
||||||
|
- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666))
|
||||||
|
- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661))
|
||||||
|
- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659))
|
||||||
|
- Fix: prevent ASN collisions for merge operations [@shamoon](https://github.com/shamoon) ([#11634](https://github.com/paperless-ngx/paperless-ngx/pull/11634))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Documentation (beta): Updates documentation for new v3 features [@stumpylog](https://github.com/stumpylog) ([#13033](https://github.com/paperless-ngx/paperless-ngx/pull/13033))
|
||||||
|
- Documentation: fix PAPERLESS\_DATE\_PARSER\_LANGUAGES formatting [@shamoon](https://github.com/shamoon) ([#12997](https://github.com/paperless-ngx/paperless-ngx/pull/12997))
|
||||||
|
- Documentation: remove duplicate words in three files [@mvanhorn](https://github.com/mvanhorn) ([#12852](https://github.com/paperless-ngx/paperless-ngx/pull/12852))
|
||||||
|
- Documentation: Update v3 migration docs [@shamoon](https://github.com/shamoon) ([#12752](https://github.com/paperless-ngx/paperless-ngx/pull/12752))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore: separate actions from bulk edit endpoint [@shamoon](https://github.com/shamoon) ([#12286](https://github.com/paperless-ngx/paperless-ngx/pull/12286))
|
||||||
|
- Chore: address more zizmor flags [@shamoon](https://github.com/shamoon) ([#12529](https://github.com/paperless-ngx/paperless-ngx/pull/12529))
|
||||||
|
- Chore: Resolves some zizmor reported code scan findings [@stumpylog](https://github.com/stumpylog) ([#12516](https://github.com/paperless-ngx/paperless-ngx/pull/12516))
|
||||||
|
- Chore: Don't upload digets file as a zip [@stumpylog](https://github.com/stumpylog) ([#12264](https://github.com/paperless-ngx/paperless-ngx/pull/12264))
|
||||||
|
- Chore: Switches to use prek in place of pre-commit [@stumpylog](https://github.com/stumpylog) ([#12002](https://github.com/paperless-ngx/paperless-ngx/pull/12002))
|
||||||
|
- Chore: Enable mypy checking in CI [@stumpylog](https://github.com/stumpylog) ([#11991](https://github.com/paperless-ngx/paperless-ngx/pull/11991))
|
||||||
|
- Chore: upgrade to node v24 [@shamoon](https://github.com/shamoon) ([#11747](https://github.com/paperless-ngx/paperless-ngx/pull/11747))
|
||||||
|
- Performance: pre-compress static files on ARM64 [@stumpylog](https://github.com/stumpylog) ([#11721](https://github.com/paperless-ngx/paperless-ngx/pull/11721))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>160 changes</summary>
|
||||||
|
|
||||||
|
- Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13137](https://github.com/paperless-ngx/paperless-ngx/pull/13137))
|
||||||
|
- Chore(deps-dev): Bump the development group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13068](https://github.com/paperless-ngx/paperless-ngx/pull/13068))
|
||||||
|
- docker-compose(deps): bump greenmail/standalone from 2.1.8 to 2.1.9 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#13067](https://github.com/paperless-ngx/paperless-ngx/pull/13067))
|
||||||
|
- docker-compose(deps): bump gotenberg/gotenberg from 8.33 to 8.34 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#13066](https://github.com/paperless-ngx/paperless-ngx/pull/13066))
|
||||||
|
- docker-compose(deps): bump nginx from 1.31.1-alpine to 1.31.2-alpine in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#13065](https://github.com/paperless-ngx/paperless-ngx/pull/13065))
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.11.19-python3.12-trixie-slim to 0.11.28-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#13070](https://github.com/paperless-ngx/paperless-ngx/pull/13070))
|
||||||
|
- Chore(deps): Bump the actions group across 1 directory with 14 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13079](https://github.com/paperless-ngx/paperless-ngx/pull/13079))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 5.7.284 to 6.0.227 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13080](https://github.com/paperless-ngx/paperless-ngx/pull/13080))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.60.0 to 1.61.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13078](https://github.com/paperless-ngx/paperless-ngx/pull/13078))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13076](https://github.com/paperless-ngx/paperless-ngx/pull/13076))
|
||||||
|
- Chore(deps): Bump uuid from 14.0.0 to 14.0.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13077](https://github.com/paperless-ngx/paperless-ngx/pull/13077))
|
||||||
|
- Chore(deps-dev): Bump jest-preset-angular from 16.1.5 to 17.0.0 in /src-ui in the frontend-jest-dependencies group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13075](https://github.com/paperless-ngx/paperless-ngx/pull/13075))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.9.1 to 26.0.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13081](https://github.com/paperless-ngx/paperless-ngx/pull/13081))
|
||||||
|
- Chore(deps): Bump the npm\_and\_yarn group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13016](https://github.com/paperless-ngx/paperless-ngx/pull/13016))
|
||||||
|
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12995](https://github.com/paperless-ngx/paperless-ngx/pull/12995))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12931](https://github.com/paperless-ngx/paperless-ngx/pull/12931))
|
||||||
|
- Chore(deps-dev): Bump types-markdown from 3.10.2.20260211 to 3.10.2.20260518 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12927](https://github.com/paperless-ngx/paperless-ngx/pull/12927))
|
||||||
|
- Chore(deps): Bump aiohttp from 3.13.4 to 3.14.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12930](https://github.com/paperless-ngx/paperless-ngx/pull/12930))
|
||||||
|
- Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12923](https://github.com/paperless-ngx/paperless-ngx/pull/12923))
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.11.6-python3.12-trixie-slim to 0.11.19-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#12920](https://github.com/paperless-ngx/paperless-ngx/pull/12920))
|
||||||
|
- Chore(deps): Bump the actions group across 1 directory with 12 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12909](https://github.com/paperless-ngx/paperless-ngx/pull/12909))
|
||||||
|
- docker-compose(deps): bump apache/tika from 3.2.3.0 to 3.3.1.0 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#12912](https://github.com/paperless-ngx/paperless-ngx/pull/12912))
|
||||||
|
- docker-compose(deps): bump gotenberg/gotenberg from 8.27 to 8.33 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#12910](https://github.com/paperless-ngx/paperless-ngx/pull/12910))
|
||||||
|
- docker-compose(deps): Bump nginx from 1.29.5-alpine to 1.31.1-alpine in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#12911](https://github.com/paperless-ngx/paperless-ngx/pull/12911))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.59.1 to 1.60.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12919](https://github.com/paperless-ngx/paperless-ngx/pull/12919))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.6.0 to 25.9.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12915](https://github.com/paperless-ngx/paperless-ngx/pull/12915))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 5.6.205 to 5.7.284 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12918](https://github.com/paperless-ngx/paperless-ngx/pull/12918))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12913](https://github.com/paperless-ngx/paperless-ngx/pull/12913))
|
||||||
|
- Chore(deps-dev): Bump webpack from 5.106.2 to 5.107.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12917](https://github.com/paperless-ngx/paperless-ngx/pull/12917))
|
||||||
|
- Chore(deps): Bump zone.js from 0.16.1 to 0.16.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12916](https://github.com/paperless-ngx/paperless-ngx/pull/12916))
|
||||||
|
- Chore(deps-dev): Bump the frontend-jest-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12908](https://github.com/paperless-ngx/paperless-ngx/pull/12908))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12907](https://github.com/paperless-ngx/paperless-ngx/pull/12907))
|
||||||
|
- Chore(deps): Bump hono from 4.12.16 to 4.12.18 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12767](https://github.com/paperless-ngx/paperless-ngx/pull/12767))
|
||||||
|
- Chore(deps): Bump @babel/plugin-transform-modules-systemjs from 7.29.0 to 7.29.4 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12764](https://github.com/paperless-ngx/paperless-ngx/pull/12764))
|
||||||
|
- Chore(deps): Bump fast-uri from 3.1.1 to 3.1.2 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12763](https://github.com/paperless-ngx/paperless-ngx/pull/12763))
|
||||||
|
- Chore: Targeted dependency upgrades [@stumpylog](https://github.com/stumpylog) ([#12731](https://github.com/paperless-ngx/paperless-ngx/pull/12731))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.59.0 to 1.59.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12692](https://github.com/paperless-ngx/paperless-ngx/pull/12692))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12689](https://github.com/paperless-ngx/paperless-ngx/pull/12689))
|
||||||
|
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.9.1 to 2.0.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12691](https://github.com/paperless-ngx/paperless-ngx/pull/12691))
|
||||||
|
- Chore(deps-dev): Bump the frontend-jest-dependencies group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12685](https://github.com/paperless-ngx/paperless-ngx/pull/12685))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.5.0 to 25.6.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12690](https://github.com/paperless-ngx/paperless-ngx/pull/12690))
|
||||||
|
- Chore(deps-dev): Bump webpack from 5.105.3 to 5.106.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12693](https://github.com/paperless-ngx/paperless-ngx/pull/12693))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 15 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12684](https://github.com/paperless-ngx/paperless-ngx/pull/12684))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12702](https://github.com/paperless-ngx/paperless-ngx/pull/12702))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 9 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12696](https://github.com/paperless-ngx/paperless-ngx/pull/12696))
|
||||||
|
- Chore(deps): Bump the actions group across 1 directory with 17 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12686](https://github.com/paperless-ngx/paperless-ngx/pull/12686))
|
||||||
|
- Chore(deps): Bump ocrmypdf from 17.4.0 to 17.4.2 in the document-processing group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12687](https://github.com/paperless-ngx/paperless-ngx/pull/12687))
|
||||||
|
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12683](https://github.com/paperless-ngx/paperless-ngx/pull/12683))
|
||||||
|
- Chore(deps): Bump the pre-commit-dependencies group with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12694](https://github.com/paperless-ngx/paperless-ngx/pull/12694))
|
||||||
|
- Chore(deps): Bump uuid from 13.0.0 to 14.0.0 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12627](https://github.com/paperless-ngx/paperless-ngx/pull/12627))
|
||||||
|
- Chore(deps): Bump lxml from 6.0.2 to 6.1.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12619](https://github.com/paperless-ngx/paperless-ngx/pull/12619))
|
||||||
|
- Chore(deps): Bump hono from 4.12.12 to 4.12.14 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12585](https://github.com/paperless-ngx/paperless-ngx/pull/12585))
|
||||||
|
- Chore(deps): Bump follow-redirects from 1.15.11 to 1.16.0 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12575](https://github.com/paperless-ngx/paperless-ngx/pull/12575))
|
||||||
|
- Chore(deps): Bump pillow from 12.1.1 to 12.2.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12574](https://github.com/paperless-ngx/paperless-ngx/pull/12574))
|
||||||
|
- Chore(deps-dev): Bump pytest from 9.0.2 to 9.0.3 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12568](https://github.com/paperless-ngx/paperless-ngx/pull/12568))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 19 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12540](https://github.com/paperless-ngx/paperless-ngx/pull/12540))
|
||||||
|
- Chore(deps): Bump cryptography from 46.0.6 to 46.0.7 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12546](https://github.com/paperless-ngx/paperless-ngx/pull/12546))
|
||||||
|
- Chore(deps-dev): Bump types-python-dateutil from 2.9.0.20260305 to 2.9.0.20260323 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12542](https://github.com/paperless-ngx/paperless-ngx/pull/12542))
|
||||||
|
- Chore(deps-dev): Bump types-pytz from 2025.2.0.20251108 to 2026.1.1.20260304 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12541](https://github.com/paperless-ngx/paperless-ngx/pull/12541))
|
||||||
|
- Chore(deps): Bump django-guardian from 3.3.0 to 3.3.1 in the utilities-patch group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12539](https://github.com/paperless-ngx/paperless-ngx/pull/12539))
|
||||||
|
- Chore(deps-dev): Bump zensical from 0.0.29 to 0.0.31 in the development group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12532](https://github.com/paperless-ngx/paperless-ngx/pull/12532))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 5.4.624 to 5.6.205 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12536](https://github.com/paperless-ngx/paperless-ngx/pull/12536))
|
||||||
|
- Chore(deps): Bump the actions group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12538](https://github.com/paperless-ngx/paperless-ngx/pull/12538))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12535](https://github.com/paperless-ngx/paperless-ngx/pull/12535))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12533](https://github.com/paperless-ngx/paperless-ngx/pull/12533))
|
||||||
|
- Chore(deps-dev): Bump jest-preset-angular from 16.1.1 to 16.1.2 in /src-ui in the frontend-jest-dependencies group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12534](https://github.com/paperless-ngx/paperless-ngx/pull/12534))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.58.2 to 1.59.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12537](https://github.com/paperless-ngx/paperless-ngx/pull/12537))
|
||||||
|
- Chore(deps): Bump the npm\_and\_yarn group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12531](https://github.com/paperless-ngx/paperless-ngx/pull/12531))
|
||||||
|
- Chore(deps): Bump djangorestframework from 3.16.1 to 3.17.1 in the django-ecosystem group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12488](https://github.com/paperless-ngx/paperless-ngx/pull/12488))
|
||||||
|
- Chore(deps): Bump the document-processing group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12489](https://github.com/paperless-ngx/paperless-ngx/pull/12489))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12499](https://github.com/paperless-ngx/paperless-ngx/pull/12499))
|
||||||
|
- Chore(deps): Bump aiohttp from 3.13.3 to 3.13.4 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12486](https://github.com/paperless-ngx/paperless-ngx/pull/12486))
|
||||||
|
- Chore(deps): Bump the pre-commit-dependencies group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12495](https://github.com/paperless-ngx/paperless-ngx/pull/12495))
|
||||||
|
- Chore(deps): Bump lodash from 4.17.23 to 4.18.1 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12498](https://github.com/paperless-ngx/paperless-ngx/pull/12498))
|
||||||
|
- Chore(deps): Bump the actions group across 1 directory with 9 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12490](https://github.com/paperless-ngx/paperless-ngx/pull/12490))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12491](https://github.com/paperless-ngx/paperless-ngx/pull/12491))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.4.0 to 25.5.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12494](https://github.com/paperless-ngx/paperless-ngx/pull/12494))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12493](https://github.com/paperless-ngx/paperless-ngx/pull/12493))
|
||||||
|
- Chore(deps): Bump cryptography from 46.0.5 to 46.0.6 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12458](https://github.com/paperless-ngx/paperless-ngx/pull/12458))
|
||||||
|
- Chore(deps): Bump requests from 2.32.5 to 2.33.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12441](https://github.com/paperless-ngx/paperless-ngx/pull/12441))
|
||||||
|
- Chore(deps): Bump cbor2 from 5.8.0 to 5.9.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12424](https://github.com/paperless-ngx/paperless-ngx/pull/12424))
|
||||||
|
- Chore(deps): Bump tinytag from 2.2.0 to 2.2.1 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12396](https://github.com/paperless-ngx/paperless-ngx/pull/12396))
|
||||||
|
- Chore(deps): Bump ujson from 5.11.0 to 5.12.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12387](https://github.com/paperless-ngx/paperless-ngx/pull/12387))
|
||||||
|
- Chore(deps): Bump pyasn1 from 0.6.2 to 0.6.3 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12370](https://github.com/paperless-ngx/paperless-ngx/pull/12370))
|
||||||
|
- Chore(deps): Bump https://github.com/astral-sh/ruff-pre-commit from v0.15.5 to 0.15.6 in the pre-commit-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12371](https://github.com/paperless-ngx/paperless-ngx/pull/12371))
|
||||||
|
- Chore(deps): Bump the actions group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12377](https://github.com/paperless-ngx/paperless-ngx/pull/12377))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12382](https://github.com/paperless-ngx/paperless-ngx/pull/12382))
|
||||||
|
- Chore(deps): Bump openai from 2.24.0 to 2.26.0 in the utilities-minor group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12379](https://github.com/paperless-ngx/paperless-ngx/pull/12379))
|
||||||
|
- Chore(deps-dev): Bump types-python-dateutil from 2.9.0.20260124 to 2.9.0.20260305 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12380](https://github.com/paperless-ngx/paperless-ngx/pull/12380))
|
||||||
|
- Chore(deps): Update django-allauth[mfa,socialaccount] requirement from ~=65.14.0 to ~=65.15.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12381](https://github.com/paperless-ngx/paperless-ngx/pull/12381))
|
||||||
|
- Chore(deps-dev): Bump the frontend-jest-dependencies group in /src-ui with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12374](https://github.com/paperless-ngx/paperless-ngx/pull/12374))
|
||||||
|
- Chore(deps): Bump @ng-select/ng-select from 21.4.1 to 21.5.2 in /src-ui in the frontend-angular-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12373](https://github.com/paperless-ngx/paperless-ngx/pull/12373))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.3.3 to 25.4.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12376](https://github.com/paperless-ngx/paperless-ngx/pull/12376))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12375](https://github.com/paperless-ngx/paperless-ngx/pull/12375))
|
||||||
|
- Chore(deps): Bump pyopenssl from 25.3.0 to 26.0.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12363](https://github.com/paperless-ngx/paperless-ngx/pull/12363))
|
||||||
|
- Chore(deps): Bump pyjwt from 2.10.1 to 2.12.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12335](https://github.com/paperless-ngx/paperless-ngx/pull/12335))
|
||||||
|
- Chore: bump Angular dependencies to 21.2.x [@shamoon](https://github.com/shamoon) ([#12338](https://github.com/paperless-ngx/paperless-ngx/pull/12338))
|
||||||
|
- Bump ocrmypdf from 16.13.0 to 17.3.0 in the document-processing group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12267](https://github.com/paperless-ngx/paperless-ngx/pull/12267))
|
||||||
|
- Chore(deps): Bump tornado from 6.5.4 to 6.5.5 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12327](https://github.com/paperless-ngx/paperless-ngx/pull/12327))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12324](https://github.com/paperless-ngx/paperless-ngx/pull/12324))
|
||||||
|
- Chore(deps): Bump the pre-commit-dependencies group with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12323](https://github.com/paperless-ngx/paperless-ngx/pull/12323))
|
||||||
|
- Chore(deps): Bump the actions group with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12322](https://github.com/paperless-ngx/paperless-ngx/pull/12322))
|
||||||
|
- docker(deps): bump astral-sh/uv from 0.10.7-python3.12-trixie-slim to 0.10.9-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#12265](https://github.com/paperless-ngx/paperless-ngx/pull/12265))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12291](https://github.com/paperless-ngx/paperless-ngx/pull/12291))
|
||||||
|
- Chore: update ESLint to v10 [@shamoon](https://github.com/shamoon) ([#12256](https://github.com/paperless-ngx/paperless-ngx/pull/12256))
|
||||||
|
- Bump django-allauth from 65.14.0 to 65.14.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12253](https://github.com/paperless-ngx/paperless-ngx/pull/12253))
|
||||||
|
- Bump django from 5.2.11 to 5.2.12 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12249](https://github.com/paperless-ngx/paperless-ngx/pull/12249))
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.10.5-python3.12-trixie-slim to 0.10.7-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#12191](https://github.com/paperless-ngx/paperless-ngx/pull/12191))
|
||||||
|
- Chore(deps): Bump the actions group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12224](https://github.com/paperless-ngx/paperless-ngx/pull/12224))
|
||||||
|
- Chore: Updates actions to the most specific version released [@stumpylog](https://github.com/stumpylog) ([#12222](https://github.com/paperless-ngx/paperless-ngx/pull/12222))
|
||||||
|
- Chore(deps): Bump whitenoise from 6.11.0 to 6.12.0 in the django-ecosystem group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12192](https://github.com/paperless-ngx/paperless-ngx/pull/12192))
|
||||||
|
- docker-compose(deps): Bump gotenberg/gotenberg from 8.26 to 8.27 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#12190](https://github.com/paperless-ngx/paperless-ngx/pull/12190))
|
||||||
|
- Chore: Updates s6-overlay to 3.2.2.0 [@stumpylog](https://github.com/stumpylog) ([#12189](https://github.com/paperless-ngx/paperless-ngx/pull/12189))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 11 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12179](https://github.com/paperless-ngx/paperless-ngx/pull/12179))
|
||||||
|
- Chore(deps): Bump nltk from 3.9.2 to 3.9.3 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12177](https://github.com/paperless-ngx/paperless-ngx/pull/12177))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12174](https://github.com/paperless-ngx/paperless-ngx/pull/12174))
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.10.0-python3.12-trixie-slim to 0.10.4-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#12125](https://github.com/paperless-ngx/paperless-ngx/pull/12125))
|
||||||
|
- Chore(deps): Bump pillow from 12.1.0 to 12.1.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12064](https://github.com/paperless-ngx/paperless-ngx/pull/12064))
|
||||||
|
- Chore(deps): Bump cryptography from 46.0.3 to 46.0.5 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12060](https://github.com/paperless-ngx/paperless-ngx/pull/12060))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12051](https://github.com/paperless-ngx/paperless-ngx/pull/12051))
|
||||||
|
- Chore(deps): Bump j178/prek-action from 1.1.0 to 1.1.1 in the actions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12033](https://github.com/paperless-ngx/paperless-ngx/pull/12033))
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.9.29-python3.12-trixie-slim to 0.10.0-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#12019](https://github.com/paperless-ngx/paperless-ngx/pull/12019))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.58.1 to 1.58.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12032](https://github.com/paperless-ngx/paperless-ngx/pull/12032))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12020](https://github.com/paperless-ngx/paperless-ngx/pull/12020))
|
||||||
|
- docker-compose(deps): Bump nginx from 1.29-alpine to 1.29.5-alpine in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#12018](https://github.com/paperless-ngx/paperless-ngx/pull/12018))
|
||||||
|
- Upgrade: Bump @types/node from 25.2.0 to 25.2.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12008](https://github.com/paperless-ngx/paperless-ngx/pull/12008))
|
||||||
|
- Upgrade: Bump webpack from 5.103.0 to 5.105.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12007](https://github.com/paperless-ngx/paperless-ngx/pull/12007))
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.9.26-python3.12-trixie-slim to 0.9.28-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#11980](https://github.com/paperless-ngx/paperless-ngx/pull/11980))
|
||||||
|
- docker-compose(deps): Bump gotenberg/gotenberg from 8.25 to 8.26 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#11979](https://github.com/paperless-ngx/paperless-ngx/pull/11979))
|
||||||
|
- Chore(deps): Bump the utilities-patch group with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11981](https://github.com/paperless-ngx/paperless-ngx/pull/11981))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11993](https://github.com/paperless-ngx/paperless-ngx/pull/11993))
|
||||||
|
- Chore(deps): Bump drf-spectacular-sidecar from 2025.10.1 to 2026.1.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11985](https://github.com/paperless-ngx/paperless-ngx/pull/11985))
|
||||||
|
- Chore(deps): Update django-allauth[mfa,socialaccount] requirement from ~=65.13.1 to ~=65.14.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11984](https://github.com/paperless-ngx/paperless-ngx/pull/11984))
|
||||||
|
- Chore(deps): Update granian[uvloop] requirement from ~=2.6.0 to ~=2.7.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11983](https://github.com/paperless-ngx/paperless-ngx/pull/11983))
|
||||||
|
- Chore(deps): Bump django from 5.2.10 to 5.2.11 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11988](https://github.com/paperless-ngx/paperless-ngx/pull/11988))
|
||||||
|
- Chore(deps): Bump the actions group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11966](https://github.com/paperless-ngx/paperless-ngx/pull/11966))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 22 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11968](https://github.com/paperless-ngx/paperless-ngx/pull/11968))
|
||||||
|
- Chore(deps): Bump zone.js from 0.15.1 to 0.16.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11970](https://github.com/paperless-ngx/paperless-ngx/pull/11970))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11969](https://github.com/paperless-ngx/paperless-ngx/pull/11969))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 24.10.1 to 25.2.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11972](https://github.com/paperless-ngx/paperless-ngx/pull/11972))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.57.0 to 1.58.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11971](https://github.com/paperless-ngx/paperless-ngx/pull/11971))
|
||||||
|
- Chore: Bulk backend updates [@stumpylog](https://github.com/stumpylog) ([#11543](https://github.com/paperless-ngx/paperless-ngx/pull/11543))
|
||||||
|
- Chore: Manually upgrades allauth to resolve a security issue with it [@stumpylog](https://github.com/stumpylog) ([#11853](https://github.com/paperless-ngx/paperless-ngx/pull/11853))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11793](https://github.com/paperless-ngx/paperless-ngx/pull/11793))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 10 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11799](https://github.com/paperless-ngx/paperless-ngx/pull/11799))
|
||||||
|
- Chore(deps): Bump pyasn1 from 0.6.1 to 0.6.2 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11801](https://github.com/paperless-ngx/paperless-ngx/pull/11801))
|
||||||
|
- Chore(deps): Bump torch from 2.7.1 to 2.8.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11800](https://github.com/paperless-ngx/paperless-ngx/pull/11800))
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.9.15-python3.12-trixie-slim to 0.9.24-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#11762](https://github.com/paperless-ngx/paperless-ngx/pull/11762))
|
||||||
|
- Chore(deps): Bump brotli from 1.1.0 to 1.2.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11796](https://github.com/paperless-ngx/paperless-ngx/pull/11796))
|
||||||
|
- Chore(deps): Bump transformers from 4.51.3 to 4.53.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11797](https://github.com/paperless-ngx/paperless-ngx/pull/11797))
|
||||||
|
- Chore(deps): Bump django from 5.2.7 to 5.2.9 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11794](https://github.com/paperless-ngx/paperless-ngx/pull/11794))
|
||||||
|
- Chore(deps): Bump the llama-index group @[dependabot[bot]](https://github.com/apps/dependabot) ([#11798](https://github.com/paperless-ngx/paperless-ngx/pull/11798))
|
||||||
|
- Chore(deps): Bump marshmallow from 3.26.1 to 3.26.2 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11790](https://github.com/paperless-ngx/paperless-ngx/pull/11790))
|
||||||
|
- Chore(deps): Bump uv from 0.9.3 to 0.9.6 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11795](https://github.com/paperless-ngx/paperless-ngx/pull/11795))
|
||||||
|
- Chore(deps): Bump aiohttp from 3.11.18 to 3.13.3 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11789](https://github.com/paperless-ngx/paperless-ngx/pull/11789))
|
||||||
|
- Chore(deps): Bump urllib3 from 2.5.0 to 2.6.3 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11792](https://github.com/paperless-ngx/paperless-ngx/pull/11792))
|
||||||
|
- Chore(deps): Bump virtualenv from 20.34.0 to 20.36.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11774](https://github.com/paperless-ngx/paperless-ngx/pull/11774))
|
||||||
|
- Chore(deps): Bump azure-core from 1.33.0 to 1.38.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11776](https://github.com/paperless-ngx/paperless-ngx/pull/11776))
|
||||||
|
- Chore: upgrade to Angular v21 [@shamoon](https://github.com/shamoon) ([#11746](https://github.com/paperless-ngx/paperless-ngx/pull/11746))
|
||||||
|
- Chore(deps): Bump the actions group with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11695](https://github.com/paperless-ngx/paperless-ngx/pull/11695))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>360 changes</summary>
|
||||||
|
|
||||||
|
- [Beta] Paperless-ngx v3.0.0 Beta [@shamoon](https://github.com/shamoon) ([#12713](https://github.com/paperless-ngx/paperless-ngx/pull/12713))
|
||||||
|
- Chore (beta): mark first batch of thumbnails as priority for LCP warning [@shamoon](https://github.com/shamoon) ([#13196](https://github.com/paperless-ngx/paperless-ngx/pull/13196))
|
||||||
|
- Fix (beta): prevent pdfjs offsetParent warning [@shamoon](https://github.com/shamoon) ([#13197](https://github.com/paperless-ngx/paperless-ngx/pull/13197))
|
||||||
|
- Fix (beta): dont include hidden buttons in virtual scroll container height [@shamoon](https://github.com/shamoon) ([#13194](https://github.com/paperless-ngx/paperless-ngx/pull/13194))
|
||||||
|
- Tweakhancement (beta): allow some bulk operations with all [@shamoon](https://github.com/shamoon) ([#13193](https://github.com/paperless-ngx/paperless-ngx/pull/13193))
|
||||||
|
- Fix (beta): fix custom field bulk editing for 'all' [@shamoon](https://github.com/shamoon) ([#13191](https://github.com/paperless-ngx/paperless-ngx/pull/13191))
|
||||||
|
- Security (beta): enforce current permissions in autocomplete [@shamoon](https://github.com/shamoon) ([#13188](https://github.com/paperless-ngx/paperless-ngx/pull/13188))
|
||||||
|
- Security (beta): bound email linkification [@shamoon](https://github.com/shamoon) ([#13187](https://github.com/paperless-ngx/paperless-ngx/pull/13187))
|
||||||
|
- Security (beta): two small permission check fixes [@shamoon](https://github.com/shamoon) ([#13186](https://github.com/paperless-ngx/paperless-ngx/pull/13186))
|
||||||
|
- Fix (beta): short-circuit ObjectOwnedOrGrantedPermissionsFilter for superusers [@stumpylog](https://github.com/stumpylog) ([#13183](https://github.com/paperless-ngx/paperless-ngx/pull/13183))
|
||||||
|
- Fix (beta): compute num\_notes via a subquery instead of Count()+distinct [@stumpylog](https://github.com/stumpylog) ([#13182](https://github.com/paperless-ngx/paperless-ngx/pull/13182))
|
||||||
|
- Change: update root modified timestamp on version changes [@shamoon](https://github.com/shamoon) ([#13170](https://github.com/paperless-ngx/paperless-ngx/pull/13170))
|
||||||
|
- Fix: prevent tag assignment from reverting other pending workflow assignments [@stumpylog](https://github.com/stumpylog) ([#13178](https://github.com/paperless-ngx/paperless-ngx/pull/13178))
|
||||||
|
- Performance: add DB indexes for common query/sort patterns [@stumpylog](https://github.com/stumpylog) ([#13167](https://github.com/paperless-ngx/paperless-ngx/pull/13167))
|
||||||
|
- Performance: use NgOptimizedImage for thumbnail lazy loading [@shamoon](https://github.com/shamoon) ([#13169](https://github.com/paperless-ngx/paperless-ngx/pull/13169))
|
||||||
|
- Fix(beta): cache per-request effective-document resolution for thumb/metadata/preview [@stumpylog](https://github.com/stumpylog) ([#13166](https://github.com/paperless-ngx/paperless-ngx/pull/13166))
|
||||||
|
- Fix: fallback from archive version for share links [@shamoon](https://github.com/shamoon) ([#13163](https://github.com/paperless-ngx/paperless-ngx/pull/13163))
|
||||||
|
- Fix: better handle saved view errors in dashboard widgets [@shamoon](https://github.com/shamoon) ([#13164](https://github.com/paperless-ngx/paperless-ngx/pull/13164))
|
||||||
|
- Fix (beta): convert chat component to signal-backed Angular [@shamoon](https://github.com/shamoon) ([#13152](https://github.com/paperless-ngx/paperless-ngx/pull/13152))
|
||||||
|
- Fix: HTTP 500 filtering documents by tags + custom field query on MariaDB [@stumpylog](https://github.com/stumpylog) ([#13143](https://github.com/paperless-ngx/paperless-ngx/pull/13143))
|
||||||
|
- Performance: Tantivy indexing optimization [@Merinorus](https://github.com/Merinorus) ([#13053](https://github.com/paperless-ngx/paperless-ngx/pull/13053))
|
||||||
|
- Chore(deps-dev): Bump the development group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13068](https://github.com/paperless-ngx/paperless-ngx/pull/13068))
|
||||||
|
- Fix: scope ProcessedMail UID deduplication by IMAP UIDVALIDITY [@chouquette](https://github.com/chouquette) ([#12839](https://github.com/paperless-ngx/paperless-ngx/pull/12839))
|
||||||
|
- Fix (beta): fix sidebar document close button [@shamoon](https://github.com/shamoon) ([#13136](https://github.com/paperless-ngx/paperless-ngx/pull/13136))
|
||||||
|
- Fix: include wasm assets in pdf.js-based viewer [@shamoon](https://github.com/shamoon) ([#13122](https://github.com/paperless-ngx/paperless-ngx/pull/13122))
|
||||||
|
- Fix: fix ui labels from md5 to sha256 [@shamoon](https://github.com/shamoon) ([#13119](https://github.com/paperless-ngx/paperless-ngx/pull/13119))
|
||||||
|
- Chorehancement: update to Angular v22, 'zoneless' / 'reactive' [@shamoon](https://github.com/shamoon) ([#13114](https://github.com/paperless-ngx/paperless-ngx/pull/13114))
|
||||||
|
- Fixhancement: dont assign empty title in workflow with broken template [@shamoon](https://github.com/shamoon) ([#13112](https://github.com/paperless-ngx/paperless-ngx/pull/13112))
|
||||||
|
- Fix: clamp mailrule max age before migration [@shamoon](https://github.com/shamoon) ([#13093](https://github.com/paperless-ngx/paperless-ngx/pull/13093))
|
||||||
|
- Fix: use latest document version for bulk download [@shamoon](https://github.com/shamoon) ([#13103](https://github.com/paperless-ngx/paperless-ngx/pull/13103))
|
||||||
|
- Fix (beta): handle relative date with comma query [@shamoon](https://github.com/shamoon) ([#13087](https://github.com/paperless-ngx/paperless-ngx/pull/13087))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 5.7.284 to 6.0.227 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13080](https://github.com/paperless-ngx/paperless-ngx/pull/13080))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.60.0 to 1.61.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13078](https://github.com/paperless-ngx/paperless-ngx/pull/13078))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13076](https://github.com/paperless-ngx/paperless-ngx/pull/13076))
|
||||||
|
- Chore(deps): Bump uuid from 14.0.0 to 14.0.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13077](https://github.com/paperless-ngx/paperless-ngx/pull/13077))
|
||||||
|
- Chore(deps-dev): Bump jest-preset-angular from 16.1.5 to 17.0.0 in /src-ui in the frontend-jest-dependencies group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13075](https://github.com/paperless-ngx/paperless-ngx/pull/13075))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.9.1 to 26.0.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#13081](https://github.com/paperless-ngx/paperless-ngx/pull/13081))
|
||||||
|
- Fix (beta): fix sanity checker affected document counts [@NgoQuocViet2001](https://github.com/NgoQuocViet2001) ([#13052](https://github.com/paperless-ngx/paperless-ngx/pull/13052))
|
||||||
|
- Fix (beta): preserve Unicode in localization prompt [@shamoon](https://github.com/shamoon) ([#13055](https://github.com/paperless-ngx/paperless-ngx/pull/13055))
|
||||||
|
- Fix: change tantivy token limit to title field max length [@shamoon](https://github.com/shamoon) ([#13046](https://github.com/paperless-ngx/paperless-ngx/pull/13046))
|
||||||
|
- Fix: include last-modified in doc etag [@shamoon](https://github.com/shamoon) ([#13044](https://github.com/paperless-ngx/paperless-ngx/pull/13044))
|
||||||
|
- Fix (beta): Stream chunks during compaction to prevent oom on smaller installs [@stumpylog](https://github.com/stumpylog) ([#13014](https://github.com/paperless-ngx/paperless-ngx/pull/13014))
|
||||||
|
- Fixhancement (beta): truncate embedding queries for small chunk size [@shamoon](https://github.com/shamoon) ([#13028](https://github.com/paperless-ngx/paperless-ngx/pull/13028))
|
||||||
|
- Enhancement (beta): support LLM timeout config [@shamoon](https://github.com/shamoon) ([#13002](https://github.com/paperless-ngx/paperless-ngx/pull/13002))
|
||||||
|
- Fix(beta): Workaround a Tantivy panic in more like this searching [@stumpylog](https://github.com/stumpylog) ([#13026](https://github.com/paperless-ngx/paperless-ngx/pull/13026))
|
||||||
|
- Chore (beta): set tool\_required to openai-like llm calls [@shamoon](https://github.com/shamoon) ([#13025](https://github.com/paperless-ngx/paperless-ngx/pull/13025))
|
||||||
|
- Enhancement: ignore diacritics, support multiple substring matching for UI filtering [@shamoon](https://github.com/shamoon) ([#13021](https://github.com/paperless-ngx/paperless-ngx/pull/13021))
|
||||||
|
- Fix (beta): Catch consumer files created during watcher re-creations [@stumpylog](https://github.com/stumpylog) ([#13013](https://github.com/paperless-ngx/paperless-ngx/pull/13013))
|
||||||
|
- Chore(deps): Bump the npm\_and\_yarn group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13016](https://github.com/paperless-ngx/paperless-ngx/pull/13016))
|
||||||
|
- Fix (beta): restore v2 (Whoosh) advanced-search query compatibility [@stumpylog](https://github.com/stumpylog) ([#13010](https://github.com/paperless-ngx/paperless-ngx/pull/13010))
|
||||||
|
- Fix (beta): retry celery ping and report warning on no response [@shamoon](https://github.com/shamoon) ([#13012](https://github.com/paperless-ngx/paperless-ngx/pull/13012))
|
||||||
|
- Enhancement(beta): replace LanceDB vector store with sqlite-vec [@stumpylog](https://github.com/stumpylog) ([#12990](https://github.com/paperless-ngx/paperless-ngx/pull/12990))
|
||||||
|
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12995](https://github.com/paperless-ngx/paperless-ngx/pull/12995))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12931](https://github.com/paperless-ngx/paperless-ngx/pull/12931))
|
||||||
|
- Fix (beta): don't send chat message on Enter while composing with IME (CJK) [@mzyy94](https://github.com/mzyy94) ([#12999](https://github.com/paperless-ngx/paperless-ngx/pull/12999))
|
||||||
|
- Fix: Apply unicode normalization to all paths and path components [@stumpylog](https://github.com/stumpylog) ([#12993](https://github.com/paperless-ngx/paperless-ngx/pull/12993))
|
||||||
|
- Fix: Handle the UTF 16 and BOM text files better [@stumpylog](https://github.com/stumpylog) ([#12994](https://github.com/paperless-ngx/paperless-ngx/pull/12994))
|
||||||
|
- Fix (beta): avoid DRF update calling `save` on all fields [@shamoon](https://github.com/shamoon) ([#12992](https://github.com/paperless-ngx/paperless-ngx/pull/12992))
|
||||||
|
- Fix: release pooled DB connection during AI LLM/embedding calls [@stumpylog](https://github.com/stumpylog) ([#12983](https://github.com/paperless-ngx/paperless-ngx/pull/12983))
|
||||||
|
- Fix: health-check pooled DB connections and close the pool on worker shutdown [@stumpylog](https://github.com/stumpylog) ([#12977](https://github.com/paperless-ngx/paperless-ngx/pull/12977))
|
||||||
|
- Fix: wrap long titles in delete confirm dialog [@shamoon](https://github.com/shamoon) ([#12973](https://github.com/paperless-ngx/paperless-ngx/pull/12973))
|
||||||
|
- Fix (beta): fix re-ordering in merge dialog [@shamoon](https://github.com/shamoon) ([#12967](https://github.com/paperless-ngx/paperless-ngx/pull/12967))
|
||||||
|
- Fix (beta): move task filtering to backend fully [@shamoon](https://github.com/shamoon) ([#12956](https://github.com/paperless-ngx/paperless-ngx/pull/12956))
|
||||||
|
- Chore(deps-dev): Bump types-markdown from 3.10.2.20260211 to 3.10.2.20260518 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12927](https://github.com/paperless-ngx/paperless-ngx/pull/12927))
|
||||||
|
- Chore(deps): Bump aiohttp from 3.13.4 to 3.14.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12930](https://github.com/paperless-ngx/paperless-ngx/pull/12930))
|
||||||
|
- Fix (beta): avoid unnecessary creating new PDF with pw removal workflow [@shamoon](https://github.com/shamoon) ([#12948](https://github.com/paperless-ngx/paperless-ngx/pull/12948))
|
||||||
|
- Enhancement (beta): Switch the AI vector store to LanceDB [@stumpylog](https://github.com/stumpylog) ([#12944](https://github.com/paperless-ngx/paperless-ngx/pull/12944))
|
||||||
|
- Fixhancement (beta): tasks dismiss all [@shamoon](https://github.com/shamoon) ([#12949](https://github.com/paperless-ngx/paperless-ngx/pull/12949))
|
||||||
|
- Fix (beta): correct chat message bg color [@shamoon](https://github.com/shamoon) ([#12955](https://github.com/paperless-ngx/paperless-ngx/pull/12955))
|
||||||
|
- Fix (beta): respect disable state for suggest endpoint, require change perms [@shamoon](https://github.com/shamoon) ([#12942](https://github.com/paperless-ngx/paperless-ngx/pull/12942))
|
||||||
|
- Fix: Ensure checksum comparison is using SHA256 [@stumpylog](https://github.com/stumpylog) ([#12939](https://github.com/paperless-ngx/paperless-ngx/pull/12939))
|
||||||
|
- Fix: Restrict the AI backend at settings configure time [@stumpylog](https://github.com/stumpylog) ([#12903](https://github.com/paperless-ngx/paperless-ngx/pull/12903))
|
||||||
|
- Enhancement (beta): add direct LLM language setting [@shamoon](https://github.com/shamoon) ([#12906](https://github.com/paperless-ngx/paperless-ngx/pull/12906))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.59.1 to 1.60.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12919](https://github.com/paperless-ngx/paperless-ngx/pull/12919))
|
||||||
|
- Fix (beta): correctly apply i18n in suggestions dropdown [@shamoon](https://github.com/shamoon) ([#12905](https://github.com/paperless-ngx/paperless-ngx/pull/12905))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.6.0 to 25.9.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12915](https://github.com/paperless-ngx/paperless-ngx/pull/12915))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 5.6.205 to 5.7.284 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12918](https://github.com/paperless-ngx/paperless-ngx/pull/12918))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12913](https://github.com/paperless-ngx/paperless-ngx/pull/12913))
|
||||||
|
- Chore(deps-dev): Bump webpack from 5.106.2 to 5.107.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12917](https://github.com/paperless-ngx/paperless-ngx/pull/12917))
|
||||||
|
- Chore(deps): Bump zone.js from 0.16.1 to 0.16.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12916](https://github.com/paperless-ngx/paperless-ngx/pull/12916))
|
||||||
|
- Chore(deps-dev): Bump the frontend-jest-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12908](https://github.com/paperless-ngx/paperless-ngx/pull/12908))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12907](https://github.com/paperless-ngx/paperless-ngx/pull/12907))
|
||||||
|
- Fix: Lock AI index during reading and don't index documents many times during a bulk update [@stumpylog](https://github.com/stumpylog) ([#12899](https://github.com/paperless-ngx/paperless-ngx/pull/12899))
|
||||||
|
- Fix: Minor fixes for the AI indexing [@stumpylog](https://github.com/stumpylog) ([#12893](https://github.com/paperless-ngx/paperless-ngx/pull/12893))
|
||||||
|
- Fix (beta): dont use tool calling with ollama [@shamoon](https://github.com/shamoon) ([#12896](https://github.com/paperless-ngx/paperless-ngx/pull/12896))
|
||||||
|
- Enhancement: try to respect language for AI suggestions [@shamoon](https://github.com/shamoon) ([#12894](https://github.com/paperless-ngx/paperless-ngx/pull/12894))
|
||||||
|
- Enhancement: AI LLM chunk size and context window config [@shamoon](https://github.com/shamoon) ([#12891](https://github.com/paperless-ngx/paperless-ngx/pull/12891))
|
||||||
|
- Fix: Improvements for security around the AI [@stumpylog](https://github.com/stumpylog) ([#12895](https://github.com/paperless-ngx/paperless-ngx/pull/12895))
|
||||||
|
- Fix: correctly show timestamp tooltip on history item [@nathanaelhoun](https://github.com/nathanaelhoun) ([#12879](https://github.com/paperless-ngx/paperless-ngx/pull/12879))
|
||||||
|
- Fix: Fold query and autocomplete terms with Tantivy's ascii\_fold so special letters match [@stumpylog](https://github.com/stumpylog) ([#12868](https://github.com/paperless-ngx/paperless-ngx/pull/12868))
|
||||||
|
- Fix: Don't store autocomplete\_word, only index it [@stumpylog](https://github.com/stumpylog) ([#12867](https://github.com/paperless-ngx/paperless-ngx/pull/12867))
|
||||||
|
- Fix: Preserve Whoosh date range swapping in Tantviy [@stumpylog](https://github.com/stumpylog) ([#12866](https://github.com/paperless-ngx/paperless-ngx/pull/12866))
|
||||||
|
- Fix: Always release search index writer [@stumpylog](https://github.com/stumpylog) ([#12865](https://github.com/paperless-ngx/paperless-ngx/pull/12865))
|
||||||
|
- Fix: Handle CJK title, content and metadata searching [@stumpylog](https://github.com/stumpylog) ([#12862](https://github.com/paperless-ngx/paperless-ngx/pull/12862))
|
||||||
|
- Fix: Restrict date query rewrites to date or datetime fields only [@stumpylog](https://github.com/stumpylog) ([#12864](https://github.com/paperless-ngx/paperless-ngx/pull/12864))
|
||||||
|
- Fix: Missing call to tanvity wait\_merging\_threads [@stumpylog](https://github.com/stumpylog) ([#12863](https://github.com/paperless-ngx/paperless-ngx/pull/12863))
|
||||||
|
- Fix: Handle tanvity index lock contention [@stumpylog](https://github.com/stumpylog) ([#12856](https://github.com/paperless-ngx/paperless-ngx/pull/12856))
|
||||||
|
- Fix (beta): better catch chat errors [@shamoon](https://github.com/shamoon) ([#12854](https://github.com/paperless-ngx/paperless-ngx/pull/12854))
|
||||||
|
- Performance (beta): dont re-build vector index with each chat [@shamoon](https://github.com/shamoon) ([#12847](https://github.com/paperless-ngx/paperless-ngx/pull/12847))
|
||||||
|
- Security (beta): fixes for v3 beta [@shamoon](https://github.com/shamoon) ([#12838](https://github.com/paperless-ngx/paperless-ngx/pull/12838))
|
||||||
|
- Fix (beta): normalize long punctuation chunks to improve embedding [@shamoon](https://github.com/shamoon) ([#12848](https://github.com/paperless-ngx/paperless-ngx/pull/12848))
|
||||||
|
- Fix: use chord.on\_error before apply\_async [@shamoon](https://github.com/shamoon) ([#12842](https://github.com/paperless-ngx/paperless-ngx/pull/12842))
|
||||||
|
- Fix: Don't use smaller integer fields for some workflow fields [@stumpylog](https://github.com/stumpylog) ([#12834](https://github.com/paperless-ngx/paperless-ngx/pull/12834))
|
||||||
|
- Fix: Password removal source file location [@stumpylog](https://github.com/stumpylog) ([#12830](https://github.com/paperless-ngx/paperless-ngx/pull/12830))
|
||||||
|
- Fix (beta): use correct html button type for custom field buttons [@shamoon](https://github.com/shamoon) ([#12819](https://github.com/paperless-ngx/paperless-ngx/pull/12819))
|
||||||
|
- Fix: Defer password removal workflow action until the file is in place [@stumpylog](https://github.com/stumpylog) ([#12814](https://github.com/paperless-ngx/paperless-ngx/pull/12814))
|
||||||
|
- Fix: Allow setting allauth rate limit configuration settings [@stumpylog](https://github.com/stumpylog) ([#12798](https://github.com/paperless-ngx/paperless-ngx/pull/12798))
|
||||||
|
- Fix: Don't embed the metadata which is already embedded into the context [@stumpylog](https://github.com/stumpylog) ([#12795](https://github.com/paperless-ngx/paperless-ngx/pull/12795))
|
||||||
|
- Fix: Sanitize dash or plus from the text search path [@stumpylog](https://github.com/stumpylog) ([#12789](https://github.com/paperless-ngx/paperless-ngx/pull/12789))
|
||||||
|
- Fix: improve new tasks ui layout across screen sizes [@shamoon](https://github.com/shamoon) ([#12784](https://github.com/paperless-ngx/paperless-ngx/pull/12784))
|
||||||
|
- Fix: Update parser contract to require empty strings, not None [@shamoon](https://github.com/shamoon) ([#12775](https://github.com/paperless-ngx/paperless-ngx/pull/12775))
|
||||||
|
- Chore: Further dependency security updates [@stumpylog](https://github.com/stumpylog) ([#12780](https://github.com/paperless-ngx/paperless-ngx/pull/12780))
|
||||||
|
- Fix: Use a persistent, writeable location for hugging face models [@stumpylog](https://github.com/stumpylog) ([#12771](https://github.com/paperless-ngx/paperless-ngx/pull/12771))
|
||||||
|
- Chore(deps): Bump hono from 4.12.16 to 4.12.18 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12767](https://github.com/paperless-ngx/paperless-ngx/pull/12767))
|
||||||
|
- Enhancement: support ollama embeddings [@shamoon](https://github.com/shamoon) ([#12753](https://github.com/paperless-ngx/paperless-ngx/pull/12753))
|
||||||
|
- Fix: create LLM\_INDEX\_DIR before writing meta.json on first run [@pReya](https://github.com/pReya) ([#12759](https://github.com/paperless-ngx/paperless-ngx/pull/12759))
|
||||||
|
- Chore(deps): Bump @babel/plugin-transform-modules-systemjs from 7.29.0 to 7.29.4 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12764](https://github.com/paperless-ngx/paperless-ngx/pull/12764))
|
||||||
|
- Chore(deps): Bump fast-uri from 3.1.1 to 3.1.2 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12763](https://github.com/paperless-ngx/paperless-ngx/pull/12763))
|
||||||
|
- Tweakhancment: Include the last applied 'documents' migration in the log [@stumpylog](https://github.com/stumpylog) ([#12757](https://github.com/paperless-ngx/paperless-ngx/pull/12757))
|
||||||
|
- Fix: use response synthesizer for RAG doc chat [@shamoon](https://github.com/shamoon) ([#12751](https://github.com/paperless-ngx/paperless-ngx/pull/12751))
|
||||||
|
- Fix: only update modified field in notes actions [@shamoon](https://github.com/shamoon) ([#12750](https://github.com/paperless-ngx/paperless-ngx/pull/12750))
|
||||||
|
- Enhancement: version-aware thumbnail etag [@shamoon](https://github.com/shamoon) ([#12754](https://github.com/paperless-ngx/paperless-ngx/pull/12754))
|
||||||
|
- Documentation: Update v3 migration docs [@shamoon](https://github.com/shamoon) ([#12752](https://github.com/paperless-ngx/paperless-ngx/pull/12752))
|
||||||
|
- Fix: Handle dash or plus operators in search queries [@stumpylog](https://github.com/stumpylog) ([#12734](https://github.com/paperless-ngx/paperless-ngx/pull/12734))
|
||||||
|
- Fixes trash preview when a document has deleted versions [@stumpylog](https://github.com/stumpylog) ([#12742](https://github.com/paperless-ngx/paperless-ngx/pull/12742))
|
||||||
|
- Fix: exclude versions from stats count [@shamoon](https://github.com/shamoon) ([#12738](https://github.com/paperless-ngx/paperless-ngx/pull/12738))
|
||||||
|
- Fixes celery chords by using Redis as our result backend [@stumpylog](https://github.com/stumpylog) ([#12741](https://github.com/paperless-ngx/paperless-ngx/pull/12741))
|
||||||
|
- Fix: bump version.py to 3.0.0 also [@shamoon](https://github.com/shamoon) ([#12736](https://github.com/paperless-ngx/paperless-ngx/pull/12736))
|
||||||
|
- Chore: Targeted dependency upgrades [@stumpylog](https://github.com/stumpylog) ([#12731](https://github.com/paperless-ngx/paperless-ngx/pull/12731))
|
||||||
|
- Fixes Whoosh year only queries to be rewritten to Tantivy date syntax [@stumpylog](https://github.com/stumpylog) ([#12725](https://github.com/paperless-ngx/paperless-ngx/pull/12725))
|
||||||
|
- Fix: pass allow parallel tool calls in LLM client [@shamoon](https://github.com/shamoon) ([#12718](https://github.com/paperless-ngx/paperless-ngx/pull/12718))
|
||||||
|
- Fix: Handle passwords for removal action as lists [@stumpylog](https://github.com/stumpylog) ([#12716](https://github.com/paperless-ngx/paperless-ngx/pull/12716))
|
||||||
|
- Bumps all our versions to 3.0.0 [@stumpylog](https://github.com/stumpylog) ([#12715](https://github.com/paperless-ngx/paperless-ngx/pull/12715))
|
||||||
|
- Feature: Further reduce document importer memory usage [@stumpylog](https://github.com/stumpylog) ([#12707](https://github.com/paperless-ngx/paperless-ngx/pull/12707))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.59.0 to 1.59.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12692](https://github.com/paperless-ngx/paperless-ngx/pull/12692))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12689](https://github.com/paperless-ngx/paperless-ngx/pull/12689))
|
||||||
|
- Chore(deps-dev): Bump @codecov/webpack-plugin from 1.9.1 to 2.0.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12691](https://github.com/paperless-ngx/paperless-ngx/pull/12691))
|
||||||
|
- Chore(deps-dev): Bump the frontend-jest-dependencies group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12685](https://github.com/paperless-ngx/paperless-ngx/pull/12685))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.5.0 to 25.6.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12690](https://github.com/paperless-ngx/paperless-ngx/pull/12690))
|
||||||
|
- Chore(deps-dev): Bump webpack from 5.105.3 to 5.106.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12693](https://github.com/paperless-ngx/paperless-ngx/pull/12693))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 15 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12684](https://github.com/paperless-ngx/paperless-ngx/pull/12684))
|
||||||
|
- Tweakhancement: use fixed position instead of display none for printing [@shamoon](https://github.com/shamoon) ([#12706](https://github.com/paperless-ngx/paperless-ngx/pull/12706))
|
||||||
|
- Fix: avoid unnecessary close\_old\_connections in Celery task dispatch [@stumpylog](https://github.com/stumpylog) ([#12701](https://github.com/paperless-ngx/paperless-ngx/pull/12701))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12702](https://github.com/paperless-ngx/paperless-ngx/pull/12702))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 9 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12696](https://github.com/paperless-ngx/paperless-ngx/pull/12696))
|
||||||
|
- Chore(deps): Bump ocrmypdf from 17.4.0 to 17.4.2 in the document-processing group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12687](https://github.com/paperless-ngx/paperless-ngx/pull/12687))
|
||||||
|
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12683](https://github.com/paperless-ngx/paperless-ngx/pull/12683))
|
||||||
|
- Enhancement: Paperless-ngx v3 Logo [@shamoon](https://github.com/shamoon) ([#12673](https://github.com/paperless-ngx/paperless-ngx/pull/12673))
|
||||||
|
- Tweakhancement: localize some more task result messages [@shamoon](https://github.com/shamoon) ([#12672](https://github.com/paperless-ngx/paperless-ngx/pull/12672))
|
||||||
|
- Enhancement: chat message document links [@shamoon](https://github.com/shamoon) ([#12670](https://github.com/paperless-ngx/paperless-ngx/pull/12670))
|
||||||
|
- Fix: apply tag changes directly to document in db [@shamoon](https://github.com/shamoon) ([#12664](https://github.com/paperless-ngx/paperless-ngx/pull/12664))
|
||||||
|
- Tweakhancement: make upload notification open an anchor link [@shamoon](https://github.com/shamoon) ([#12659](https://github.com/paperless-ngx/paperless-ngx/pull/12659))
|
||||||
|
- Tweakhancement: tweak tasks UI, make open doc an anchor [@shamoon](https://github.com/shamoon) ([#12658](https://github.com/paperless-ngx/paperless-ngx/pull/12658))
|
||||||
|
- Fix: Use FileResponse for file API responses [@stumpylog](https://github.com/stumpylog) ([#12638](https://github.com/paperless-ngx/paperless-ngx/pull/12638))
|
||||||
|
- Chore: Paginate the task listing [@stumpylog](https://github.com/stumpylog) ([#12633](https://github.com/paperless-ngx/paperless-ngx/pull/12633))
|
||||||
|
- Enhancement: show small task summary in system status [@shamoon](https://github.com/shamoon) ([#12634](https://github.com/paperless-ngx/paperless-ngx/pull/12634))
|
||||||
|
- Security: Don't allow the example secret key as a secret key [@stumpylog](https://github.com/stumpylog) ([#12630](https://github.com/paperless-ngx/paperless-ngx/pull/12630))
|
||||||
|
- Chore(deps): Bump uuid from 13.0.0 to 14.0.0 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12627](https://github.com/paperless-ngx/paperless-ngx/pull/12627))
|
||||||
|
- Enhancement: new Tasks UI [@shamoon](https://github.com/shamoon) ([#12614](https://github.com/paperless-ngx/paperless-ngx/pull/12614))
|
||||||
|
- Feature: Allow monitoring access to tasks summary [@stumpylog](https://github.com/stumpylog) ([#12624](https://github.com/paperless-ngx/paperless-ngx/pull/12624))
|
||||||
|
- Chore(deps): Bump lxml from 6.0.2 to 6.1.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12619](https://github.com/paperless-ngx/paperless-ngx/pull/12619))
|
||||||
|
- Performance: Increases workflow related M2M prefetching [@stumpylog](https://github.com/stumpylog) ([#12618](https://github.com/paperless-ngx/paperless-ngx/pull/12618))
|
||||||
|
- Tweak: remove stale index warning [@shamoon](https://github.com/shamoon) ([#12616](https://github.com/paperless-ngx/paperless-ngx/pull/12616))
|
||||||
|
- Fixhancement: Exact custom field monetary exact searching [@stumpylog](https://github.com/stumpylog) ([#12592](https://github.com/paperless-ngx/paperless-ngx/pull/12592))
|
||||||
|
- Performance: Resolves another N+1 query related to custom fields [@stumpylog](https://github.com/stumpylog) ([#12617](https://github.com/paperless-ngx/paperless-ngx/pull/12617))
|
||||||
|
- Enhancement: add highlighting to title + content searches [@shamoon](https://github.com/shamoon) ([#12593](https://github.com/paperless-ngx/paperless-ngx/pull/12593))
|
||||||
|
- Feature: Redesign the task system [@stumpylog](https://github.com/stumpylog) ([#12584](https://github.com/paperless-ngx/paperless-ngx/pull/12584))
|
||||||
|
- Fix: relative added date check visibility [@shamoon](https://github.com/shamoon) ([#12600](https://github.com/paperless-ngx/paperless-ngx/pull/12600))
|
||||||
|
- Fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type [@ggouzi](https://github.com/ggouzi) ([#12597](https://github.com/paperless-ngx/paperless-ngx/pull/12597))
|
||||||
|
- Chore(deps): Bump hono from 4.12.12 to 4.12.14 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12585](https://github.com/paperless-ngx/paperless-ngx/pull/12585))
|
||||||
|
- Feature: Tune SQLite default settings for increased speed [@stumpylog](https://github.com/stumpylog) ([#12580](https://github.com/paperless-ngx/paperless-ngx/pull/12580))
|
||||||
|
- Feature: Document fuzzy match improvements [@stumpylog](https://github.com/stumpylog) ([#12579](https://github.com/paperless-ngx/paperless-ngx/pull/12579))
|
||||||
|
- Fix: reject invalid requests to API notes endpoint [@ggouzi](https://github.com/ggouzi) ([#12582](https://github.com/paperless-ngx/paperless-ngx/pull/12582))
|
||||||
|
- [BREAKING] Remove the positional arguments from the pre/post consume scripts [@stumpylog](https://github.com/stumpylog) ([#12573](https://github.com/paperless-ngx/paperless-ngx/pull/12573))
|
||||||
|
- Chore(deps): Bump follow-redirects from 1.15.11 to 1.16.0 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12575](https://github.com/paperless-ngx/paperless-ngx/pull/12575))
|
||||||
|
- Chore(deps): Bump pillow from 12.1.1 to 12.2.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12574](https://github.com/paperless-ngx/paperless-ngx/pull/12574))
|
||||||
|
- Chore(deps-dev): Bump pytest from 9.0.2 to 9.0.3 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12568](https://github.com/paperless-ngx/paperless-ngx/pull/12568))
|
||||||
|
- Enhancement: validate and sanitize uploaded logos [@shamoon](https://github.com/shamoon) ([#12551](https://github.com/paperless-ngx/paperless-ngx/pull/12551))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 19 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12540](https://github.com/paperless-ngx/paperless-ngx/pull/12540))
|
||||||
|
- Chore(deps): Bump cryptography from 46.0.6 to 46.0.7 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12546](https://github.com/paperless-ngx/paperless-ngx/pull/12546))
|
||||||
|
- Chore(deps-dev): Bump types-python-dateutil from 2.9.0.20260305 to 2.9.0.20260323 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12542](https://github.com/paperless-ngx/paperless-ngx/pull/12542))
|
||||||
|
- Chore(deps-dev): Bump types-pytz from 2025.2.0.20251108 to 2026.1.1.20260304 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12541](https://github.com/paperless-ngx/paperless-ngx/pull/12541))
|
||||||
|
- Chore(deps): Bump django-guardian from 3.3.0 to 3.3.1 in the utilities-patch group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12539](https://github.com/paperless-ngx/paperless-ngx/pull/12539))
|
||||||
|
- Chore(deps-dev): Bump zensical from 0.0.29 to 0.0.31 in the development group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12532](https://github.com/paperless-ngx/paperless-ngx/pull/12532))
|
||||||
|
- Chore(deps): Bump pdfjs-dist from 5.4.624 to 5.6.205 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12536](https://github.com/paperless-ngx/paperless-ngx/pull/12536))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12535](https://github.com/paperless-ngx/paperless-ngx/pull/12535))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12533](https://github.com/paperless-ngx/paperless-ngx/pull/12533))
|
||||||
|
- Chore(deps-dev): Bump jest-preset-angular from 16.1.1 to 16.1.2 in /src-ui in the frontend-jest-dependencies group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12534](https://github.com/paperless-ngx/paperless-ngx/pull/12534))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.58.2 to 1.59.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12537](https://github.com/paperless-ngx/paperless-ngx/pull/12537))
|
||||||
|
- Enhancement: add view\_global\_statistics and view\_system\_status permissions [@shamoon](https://github.com/shamoon) ([#12530](https://github.com/paperless-ngx/paperless-ngx/pull/12530))
|
||||||
|
- Chore(deps): Bump the npm\_and\_yarn group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12531](https://github.com/paperless-ngx/paperless-ngx/pull/12531))
|
||||||
|
- [BREAKING] Decouple OCR control from archive file control [@stumpylog](https://github.com/stumpylog) ([#12448](https://github.com/paperless-ngx/paperless-ngx/pull/12448))
|
||||||
|
- Fixhancement: include sharelinks + bundles in export/import [@shamoon](https://github.com/shamoon) ([#12479](https://github.com/paperless-ngx/paperless-ngx/pull/12479))
|
||||||
|
- Chore: Better typed status manager messages [@stumpylog](https://github.com/stumpylog) ([#12509](https://github.com/paperless-ngx/paperless-ngx/pull/12509))
|
||||||
|
- Enhancement: unify text search to use tantivy [@shamoon](https://github.com/shamoon) ([#12485](https://github.com/paperless-ngx/paperless-ngx/pull/12485))
|
||||||
|
- Feature: Consumer logging correlation [@stumpylog](https://github.com/stumpylog) ([#12510](https://github.com/paperless-ngx/paperless-ngx/pull/12510))
|
||||||
|
- Chore(deps): Bump djangorestframework from 3.16.1 to 3.17.1 in the django-ecosystem group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12488](https://github.com/paperless-ngx/paperless-ngx/pull/12488))
|
||||||
|
- Chore(deps): Bump the document-processing group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12489](https://github.com/paperless-ngx/paperless-ngx/pull/12489))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12499](https://github.com/paperless-ngx/paperless-ngx/pull/12499))
|
||||||
|
- Security: Sign the Celery pickled tasks [@stumpylog](https://github.com/stumpylog) ([#12504](https://github.com/paperless-ngx/paperless-ngx/pull/12504))
|
||||||
|
- Security enhancement: allow opt-in blocking internal mail hosts [@shamoon](https://github.com/shamoon) ([#12502](https://github.com/paperless-ngx/paperless-ngx/pull/12502))
|
||||||
|
- Feature: Replace Whoosh with tantivy search backend [@stumpylog](https://github.com/stumpylog) ([#12471](https://github.com/paperless-ngx/paperless-ngx/pull/12471))
|
||||||
|
- Chore(deps): Bump aiohttp from 3.13.3 to 3.13.4 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12486](https://github.com/paperless-ngx/paperless-ngx/pull/12486))
|
||||||
|
- Chore(deps): Bump lodash from 4.17.23 to 4.18.1 in /src-ui in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12498](https://github.com/paperless-ngx/paperless-ngx/pull/12498))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 20 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12491](https://github.com/paperless-ngx/paperless-ngx/pull/12491))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.4.0 to 25.5.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12494](https://github.com/paperless-ngx/paperless-ngx/pull/12494))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12493](https://github.com/paperless-ngx/paperless-ngx/pull/12493))
|
||||||
|
- Performance: support bulk edit without id lists [@shamoon](https://github.com/shamoon) ([#12355](https://github.com/paperless-ngx/paperless-ngx/pull/12355))
|
||||||
|
- Performance: deprecate and remove usage of `all` in API results [@shamoon](https://github.com/shamoon) ([#12309](https://github.com/paperless-ngx/paperless-ngx/pull/12309))
|
||||||
|
- Performance: support passing selection data with filtered document requests [@shamoon](https://github.com/shamoon) ([#12300](https://github.com/paperless-ngx/paperless-ngx/pull/12300))
|
||||||
|
- Chore(deps): Bump cryptography from 46.0.5 to 46.0.6 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12458](https://github.com/paperless-ngx/paperless-ngx/pull/12458))
|
||||||
|
- Tweakhancement: show file extension in StoragePath test [@shamoon](https://github.com/shamoon) ([#12452](https://github.com/paperless-ngx/paperless-ngx/pull/12452))
|
||||||
|
- Feature: Transition all checksums to use SHA256 [@stumpylog](https://github.com/stumpylog) ([#12432](https://github.com/paperless-ngx/paperless-ngx/pull/12432))
|
||||||
|
- Chore(deps): Bump requests from 2.32.5 to 2.33.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12441](https://github.com/paperless-ngx/paperless-ngx/pull/12441))
|
||||||
|
- Enhancement: auto-hide the search bar on mobile [@shamoon](https://github.com/shamoon) ([#12404](https://github.com/paperless-ngx/paperless-ngx/pull/12404))
|
||||||
|
- Chore: logger, response and template sanitization cleanup [@shamoon](https://github.com/shamoon) ([#12439](https://github.com/paperless-ngx/paperless-ngx/pull/12439))
|
||||||
|
- Fix: Respect workflow change\_groups during consumption [@stumpylog](https://github.com/stumpylog) ([#12431](https://github.com/paperless-ngx/paperless-ngx/pull/12431))
|
||||||
|
- Security: prevent prototype pollution in frontend settings and list view [@shamoon](https://github.com/shamoon) ([#12438](https://github.com/paperless-ngx/paperless-ngx/pull/12438))
|
||||||
|
- Chore(deps): Bump cbor2 from 5.8.0 to 5.9.0 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12424](https://github.com/paperless-ngx/paperless-ngx/pull/12424))
|
||||||
|
- Chore(deps): Bump tinytag from 2.2.0 to 2.2.1 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#12396](https://github.com/paperless-ngx/paperless-ngx/pull/12396))
|
||||||
|
- Chore(deps): Bump ujson from 5.11.0 to 5.12.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12387](https://github.com/paperless-ngx/paperless-ngx/pull/12387))
|
||||||
|
- Chore(deps): Bump pyasn1 from 0.6.2 to 0.6.3 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12370](https://github.com/paperless-ngx/paperless-ngx/pull/12370))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12382](https://github.com/paperless-ngx/paperless-ngx/pull/12382))
|
||||||
|
- Chore(deps): Bump openai from 2.24.0 to 2.26.0 in the utilities-minor group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12379](https://github.com/paperless-ngx/paperless-ngx/pull/12379))
|
||||||
|
- Chore(deps-dev): Bump types-python-dateutil from 2.9.0.20260124 to 2.9.0.20260305 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12380](https://github.com/paperless-ngx/paperless-ngx/pull/12380))
|
||||||
|
- Chore(deps): Update django-allauth[mfa,socialaccount] requirement from ~=65.14.0 to ~=65.15.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12381](https://github.com/paperless-ngx/paperless-ngx/pull/12381))
|
||||||
|
- Chore(deps-dev): Bump the frontend-jest-dependencies group in /src-ui with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12374](https://github.com/paperless-ngx/paperless-ngx/pull/12374))
|
||||||
|
- Chore(deps): Bump @ng-select/ng-select from 21.4.1 to 21.5.2 in /src-ui in the frontend-angular-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12373](https://github.com/paperless-ngx/paperless-ngx/pull/12373))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 25.3.3 to 25.4.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12376](https://github.com/paperless-ngx/paperless-ngx/pull/12376))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12375](https://github.com/paperless-ngx/paperless-ngx/pull/12375))
|
||||||
|
- Chore: Reduce system status test times [@stumpylog](https://github.com/stumpylog) ([#12354](https://github.com/paperless-ngx/paperless-ngx/pull/12354))
|
||||||
|
- Chore(deps): Bump pyopenssl from 25.3.0 to 26.0.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12363](https://github.com/paperless-ngx/paperless-ngx/pull/12363))
|
||||||
|
- Chore(deps): Bump pyjwt from 2.10.1 to 2.12.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12335](https://github.com/paperless-ngx/paperless-ngx/pull/12335))
|
||||||
|
- Change: sort custom fields alphabetically by default [@shamoon](https://github.com/shamoon) ([#12358](https://github.com/paperless-ngx/paperless-ngx/pull/12358))
|
||||||
|
- Feature: Add progress information to the classifier training for a better ux [@stumpylog](https://github.com/stumpylog) ([#12331](https://github.com/paperless-ngx/paperless-ngx/pull/12331))
|
||||||
|
- Chore: bump Angular dependencies to 21.2.x [@shamoon](https://github.com/shamoon) ([#12338](https://github.com/paperless-ngx/paperless-ngx/pull/12338))
|
||||||
|
- Bump ocrmypdf from 16.13.0 to 17.3.0 in the document-processing group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12267](https://github.com/paperless-ngx/paperless-ngx/pull/12267))
|
||||||
|
- Feature: document parser plugin framework [@stumpylog](https://github.com/stumpylog) ([#12294](https://github.com/paperless-ngx/paperless-ngx/pull/12294))
|
||||||
|
- Chore(deps): Bump tornado from 6.5.4 to 6.5.5 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12327](https://github.com/paperless-ngx/paperless-ngx/pull/12327))
|
||||||
|
- Enhancement: Make the StatusConsumer truly async [@stumpylog](https://github.com/stumpylog) ([#12298](https://github.com/paperless-ngx/paperless-ngx/pull/12298))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 5 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12324](https://github.com/paperless-ngx/paperless-ngx/pull/12324))
|
||||||
|
- Chore: mark document detail email action as deprecated [@shamoon](https://github.com/shamoon) ([#12308](https://github.com/paperless-ngx/paperless-ngx/pull/12308))
|
||||||
|
- Chore: separate actions from bulk edit endpoint [@shamoon](https://github.com/shamoon) ([#12286](https://github.com/paperless-ngx/paperless-ngx/pull/12286))
|
||||||
|
- [BREAKING] Chore: drop support for api versions \< 9 [@shamoon](https://github.com/shamoon) ([#12284](https://github.com/paperless-ngx/paperless-ngx/pull/12284))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12291](https://github.com/paperless-ngx/paperless-ngx/pull/12291))
|
||||||
|
- Performance: Stream JSON during import for memory improvements [@stumpylog](https://github.com/stumpylog) ([#12276](https://github.com/paperless-ngx/paperless-ngx/pull/12276))
|
||||||
|
- Feature: Migrate import/export to rich progress [@stumpylog](https://github.com/stumpylog) ([#12260](https://github.com/paperless-ngx/paperless-ngx/pull/12260))
|
||||||
|
- Enhancement: Show more document details in merge dialog [@svenstaro](https://github.com/svenstaro) ([#12271](https://github.com/paperless-ngx/paperless-ngx/pull/12271))
|
||||||
|
- Performance: Further export memory improvements [@stumpylog](https://github.com/stumpylog) ([#12273](https://github.com/paperless-ngx/paperless-ngx/pull/12273))
|
||||||
|
- Chore: pytest style paperless tests [@stumpylog](https://github.com/stumpylog) ([#12254](https://github.com/paperless-ngx/paperless-ngx/pull/12254))
|
||||||
|
- Chore: update ESLint to v10 [@shamoon](https://github.com/shamoon) ([#12256](https://github.com/paperless-ngx/paperless-ngx/pull/12256))
|
||||||
|
- Bump django-allauth from 65.14.0 to 65.14.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12253](https://github.com/paperless-ngx/paperless-ngx/pull/12253))
|
||||||
|
- Bump django from 5.2.11 to 5.2.12 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12249](https://github.com/paperless-ngx/paperless-ngx/pull/12249))
|
||||||
|
- [BREAKING] Chore: Drop support for Python 3.10 [@stumpylog](https://github.com/stumpylog) ([#12234](https://github.com/paperless-ngx/paperless-ngx/pull/12234))
|
||||||
|
- Performance: Improve exporter memory efficiency [@stumpylog](https://github.com/stumpylog) ([#12236](https://github.com/paperless-ngx/paperless-ngx/pull/12236))
|
||||||
|
- Enhancement: saved view sharing [@shamoon](https://github.com/shamoon) ([#12142](https://github.com/paperless-ngx/paperless-ngx/pull/12142))
|
||||||
|
- Fix: use maxsplit=1 in Redis URL parsing to handle URLs with multiple colons [@cryptomilk](https://github.com/cryptomilk) ([#12239](https://github.com/paperless-ngx/paperless-ngx/pull/12239))
|
||||||
|
- Enhancement: “live” document updates [@shamoon](https://github.com/shamoon) ([#12141](https://github.com/paperless-ngx/paperless-ngx/pull/12141))
|
||||||
|
- Enhancement: Improve the retagger output using rich [@stumpylog](https://github.com/stumpylog) ([#12194](https://github.com/paperless-ngx/paperless-ngx/pull/12194))
|
||||||
|
- Enhancement: Transition sanity check to rich and improve output [@stumpylog](https://github.com/stumpylog) ([#12182](https://github.com/paperless-ngx/paperless-ngx/pull/12182))
|
||||||
|
- Enhancement: Switch all indexing to use rich [@stumpylog](https://github.com/stumpylog) ([#12193](https://github.com/paperless-ngx/paperless-ngx/pull/12193))
|
||||||
|
- Chore(deps): Bump whitenoise from 6.11.0 to 6.12.0 in the django-ecosystem group @[dependabot[bot]](https://github.com/apps/dependabot) ([#12192](https://github.com/paperless-ngx/paperless-ngx/pull/12192))
|
||||||
|
- Fixhancement: show sequential + id version labels, fix padding [@shamoon](https://github.com/shamoon) ([#12196](https://github.com/paperless-ngx/paperless-ngx/pull/12196))
|
||||||
|
- [BREAKING] Chore: Refactor advanced database settings to allow more user configuration [@stumpylog](https://github.com/stumpylog) ([#12165](https://github.com/paperless-ngx/paperless-ngx/pull/12165))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 11 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12179](https://github.com/paperless-ngx/paperless-ngx/pull/12179))
|
||||||
|
- Feature: Switch progress bar library to rich [@stumpylog](https://github.com/stumpylog) ([#12169](https://github.com/paperless-ngx/paperless-ngx/pull/12169))
|
||||||
|
- Chore(deps): Bump nltk from 3.9.2 to 3.9.3 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12177](https://github.com/paperless-ngx/paperless-ngx/pull/12177))
|
||||||
|
- Enhancement: Formatted filename for single document downloads [@JanKleine](https://github.com/JanKleine) ([#12095](https://github.com/paperless-ngx/paperless-ngx/pull/12095))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12174](https://github.com/paperless-ngx/paperless-ngx/pull/12174))
|
||||||
|
- Feature: document file versions [@shamoon](https://github.com/shamoon) ([#12061](https://github.com/paperless-ngx/paperless-ngx/pull/12061))
|
||||||
|
- [BREAKING] Remove API v1 compatibility [@stumpylog](https://github.com/stumpylog) ([#12166](https://github.com/paperless-ngx/paperless-ngx/pull/12166))
|
||||||
|
- Enhancement: prevent duplicate mail processing across rules [@shamoon](https://github.com/shamoon) ([#12159](https://github.com/paperless-ngx/paperless-ngx/pull/12159))
|
||||||
|
- Feature: move to trash action for workflows [@JanKleine](https://github.com/JanKleine) ([#11176](https://github.com/paperless-ngx/paperless-ngx/pull/11176))
|
||||||
|
- Tweakhancement: reset to page 1 on reset filters [@shamoon](https://github.com/shamoon) ([#12143](https://github.com/paperless-ngx/paperless-ngx/pull/12143))
|
||||||
|
- Fix: correct user dropdown button icon styling [@shamoon](https://github.com/shamoon) ([#12092](https://github.com/paperless-ngx/paperless-ngx/pull/12092))
|
||||||
|
- Enhancement: consolidate management lists into document attributes section [@shamoon](https://github.com/shamoon) ([#12045](https://github.com/paperless-ngx/paperless-ngx/pull/12045))
|
||||||
|
- Enhancement: option to stop processing further mail rules [@shamoon](https://github.com/shamoon) ([#12053](https://github.com/paperless-ngx/paperless-ngx/pull/12053))
|
||||||
|
- [BREAKING] Remove pybzar as a barcode reader [@stumpylog](https://github.com/stumpylog) ([#12065](https://github.com/paperless-ngx/paperless-ngx/pull/12065))
|
||||||
|
- Chore(deps): Bump pillow from 12.1.0 to 12.1.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12064](https://github.com/paperless-ngx/paperless-ngx/pull/12064))
|
||||||
|
- Chore(deps): Bump cryptography from 46.0.3 to 46.0.5 @[dependabot[bot]](https://github.com/apps/dependabot) ([#12060](https://github.com/paperless-ngx/paperless-ngx/pull/12060))
|
||||||
|
- Chore: Optimizes the integer fields for choice types [@stumpylog](https://github.com/stumpylog) ([#12057](https://github.com/paperless-ngx/paperless-ngx/pull/12057))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12051](https://github.com/paperless-ngx/paperless-ngx/pull/12051))
|
||||||
|
- Tweak: improve 2-digit year parsing [@shamoon](https://github.com/shamoon) ([#12044](https://github.com/paperless-ngx/paperless-ngx/pull/12044))
|
||||||
|
- Enhancement: pngx pdf viewer [@shamoon](https://github.com/shamoon) ([#12043](https://github.com/paperless-ngx/paperless-ngx/pull/12043))
|
||||||
|
- Fix: Handle an OOM kill of celery workers in the task handler [@stumpylog](https://github.com/stumpylog) ([#12040](https://github.com/paperless-ngx/paperless-ngx/pull/12040))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.58.1 to 1.58.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12032](https://github.com/paperless-ngx/paperless-ngx/pull/12032))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#12020](https://github.com/paperless-ngx/paperless-ngx/pull/12020))
|
||||||
|
- Fixhancement: improve ASN handling with PDF operations [@shamoon](https://github.com/shamoon) ([#11689](https://github.com/paperless-ngx/paperless-ngx/pull/11689))
|
||||||
|
- Upgrade: Bump @types/node from 25.2.0 to 25.2.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12008](https://github.com/paperless-ngx/paperless-ngx/pull/12008))
|
||||||
|
- Upgrade: Bump webpack from 5.103.0 to 5.105.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#12007](https://github.com/paperless-ngx/paperless-ngx/pull/12007))
|
||||||
|
- Chore: bump Angular to 21.1.3, ngx-ui-tour-ng-bootstrap to v18 [@shamoon](https://github.com/shamoon) ([#12015](https://github.com/paperless-ngx/paperless-ngx/pull/12015))
|
||||||
|
- Chore: Switches to use prek in place of pre-commit [@stumpylog](https://github.com/stumpylog) ([#12002](https://github.com/paperless-ngx/paperless-ngx/pull/12002))
|
||||||
|
- Chore(deps): Bump the utilities-patch group with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11981](https://github.com/paperless-ngx/paperless-ngx/pull/11981))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11993](https://github.com/paperless-ngx/paperless-ngx/pull/11993))
|
||||||
|
- Chore(deps): Bump drf-spectacular-sidecar from 2025.10.1 to 2026.1.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11985](https://github.com/paperless-ngx/paperless-ngx/pull/11985))
|
||||||
|
- Chore(deps): Update django-allauth[mfa,socialaccount] requirement from ~=65.13.1 to ~=65.14.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11984](https://github.com/paperless-ngx/paperless-ngx/pull/11984))
|
||||||
|
- Chore(deps): Update granian[uvloop] requirement from ~=2.6.0 to ~=2.7.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11983](https://github.com/paperless-ngx/paperless-ngx/pull/11983))
|
||||||
|
- Chore(deps): Bump django from 5.2.10 to 5.2.11 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11988](https://github.com/paperless-ngx/paperless-ngx/pull/11988))
|
||||||
|
- Feature: Enable users to customize date parsing via plugins [@stumpylog](https://github.com/stumpylog) ([#11931](https://github.com/paperless-ngx/paperless-ngx/pull/11931))
|
||||||
|
- Chore: Fix further test flakiness [@stumpylog](https://github.com/stumpylog) ([#11987](https://github.com/paperless-ngx/paperless-ngx/pull/11987))
|
||||||
|
- Feature: password removal workflow action [@shamoon](https://github.com/shamoon) ([#11665](https://github.com/paperless-ngx/paperless-ngx/pull/11665))
|
||||||
|
- Fix: re-run ASN check after barcode detection [@shamoon](https://github.com/shamoon) ([#11681](https://github.com/paperless-ngx/paperless-ngx/pull/11681))
|
||||||
|
- Fix: prevent infinite loading crash in mail component [@shamoon](https://github.com/shamoon) ([#11978](https://github.com/paperless-ngx/paperless-ngx/pull/11978))
|
||||||
|
- Enhancement: per-type object page sizing [@shamoon](https://github.com/shamoon) ([#11977](https://github.com/paperless-ngx/paperless-ngx/pull/11977))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 22 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11968](https://github.com/paperless-ngx/paperless-ngx/pull/11968))
|
||||||
|
- Chore(deps): Bump zone.js from 0.15.1 to 0.16.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11970](https://github.com/paperless-ngx/paperless-ngx/pull/11970))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11969](https://github.com/paperless-ngx/paperless-ngx/pull/11969))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 24.10.1 to 25.2.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11972](https://github.com/paperless-ngx/paperless-ngx/pull/11972))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.57.0 to 1.58.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11971](https://github.com/paperless-ngx/paperless-ngx/pull/11971))
|
||||||
|
- Enhancement: improve filter drop-down performance with virtual scrolling [@shamoon](https://github.com/shamoon) ([#11973](https://github.com/paperless-ngx/paperless-ngx/pull/11973))
|
||||||
|
- Tweakhancement: tweak bulk delete text [@shamoon](https://github.com/shamoon) ([#11967](https://github.com/paperless-ngx/paperless-ngx/pull/11967))
|
||||||
|
- Feature: support split documents based on tag barcodes @schober-ch ([#11645](https://github.com/paperless-ngx/paperless-ngx/pull/11645))
|
||||||
|
- Chore: Bulk backend updates [@stumpylog](https://github.com/stumpylog) ([#11543](https://github.com/paperless-ngx/paperless-ngx/pull/11543))
|
||||||
|
- Tweak: increase minimum screen width before inserting padding [@JanKleine](https://github.com/JanKleine) ([#11926](https://github.com/paperless-ngx/paperless-ngx/pull/11926))
|
||||||
|
- Enhancement: Add setting for default PDF Editor mode [@JanKleine](https://github.com/JanKleine) ([#11927](https://github.com/paperless-ngx/paperless-ngx/pull/11927))
|
||||||
|
- Fixhancement: auto-queue llm index if needed [@shamoon](https://github.com/shamoon) ([#11891](https://github.com/paperless-ngx/paperless-ngx/pull/11891))
|
||||||
|
- Feature: sharelink bundles [@shamoon](https://github.com/shamoon) ([#11682](https://github.com/paperless-ngx/paperless-ngx/pull/11682))
|
||||||
|
- Enhancement: user control of doc details fields [@shamoon](https://github.com/shamoon) ([#11906](https://github.com/paperless-ngx/paperless-ngx/pull/11906))
|
||||||
|
- Enhancement: improve relative dates in date filter [@JanKleine](https://github.com/JanKleine) ([#11899](https://github.com/paperless-ngx/paperless-ngx/pull/11899))
|
||||||
|
- Performance: faster statistics panel on dashboard [@Merinorus](https://github.com/Merinorus) ([#11760](https://github.com/paperless-ngx/paperless-ngx/pull/11760))
|
||||||
|
- Enhancement: allow duplicates with warnings, UI for discovery [@shamoon](https://github.com/shamoon) ([#11815](https://github.com/paperless-ngx/paperless-ngx/pull/11815))
|
||||||
|
- Enhancement: configurable SSO groups claim [@Gabgobie](https://github.com/Gabgobie) ([#11841](https://github.com/paperless-ngx/paperless-ngx/pull/11841))
|
||||||
|
- Enhancement: support select all for management lists [@shamoon](https://github.com/shamoon) ([#11889](https://github.com/paperless-ngx/paperless-ngx/pull/11889))
|
||||||
|
- Tweakhancement: display document id, with copy [@shamoon](https://github.com/shamoon) ([#11896](https://github.com/paperless-ngx/paperless-ngx/pull/11896))
|
||||||
|
- Enhancement: Add support for app oidc [@paulgessinger](https://github.com/paulgessinger) ([#11756](https://github.com/paperless-ngx/paperless-ngx/pull/11756))
|
||||||
|
- Enhancement: Add 'any of' workflow trigger filters [@shamoon](https://github.com/shamoon) ([#11683](https://github.com/paperless-ngx/paperless-ngx/pull/11683))
|
||||||
|
- [BREAKING] Remove support for document and thumbnail encryption [@stumpylog](https://github.com/stumpylog) ([#11850](https://github.com/paperless-ngx/paperless-ngx/pull/11850))
|
||||||
|
- Fix: ensure css color-scheme for dark mode [@shamoon](https://github.com/shamoon) ([#11855](https://github.com/paperless-ngx/paperless-ngx/pull/11855))
|
||||||
|
- Enhancement: support doc\_id placeholder in workflow templates [@shamoon](https://github.com/shamoon) ([#11847](https://github.com/paperless-ngx/paperless-ngx/pull/11847))
|
||||||
|
- [BREAKING] Feature: Simplify and improve the consumer [@stumpylog](https://github.com/stumpylog) ([#11753](https://github.com/paperless-ngx/paperless-ngx/pull/11753))
|
||||||
|
- Fix: fix tag list horizontal scroll, again [@shamoon](https://github.com/shamoon) ([#11839](https://github.com/paperless-ngx/paperless-ngx/pull/11839))
|
||||||
|
- Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent [@shamoon](https://github.com/shamoon) ([#11811](https://github.com/paperless-ngx/paperless-ngx/pull/11811))
|
||||||
|
- Chore(deps): Bump the utilities-patch group across 1 directory with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11793](https://github.com/paperless-ngx/paperless-ngx/pull/11793))
|
||||||
|
- Chore(deps): Bump the utilities-minor group across 1 directory with 10 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11799](https://github.com/paperless-ngx/paperless-ngx/pull/11799))
|
||||||
|
- Chore(deps): Bump pyasn1 from 0.6.1 to 0.6.2 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11801](https://github.com/paperless-ngx/paperless-ngx/pull/11801))
|
||||||
|
- Chore(deps): Bump torch from 2.7.1 to 2.8.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11800](https://github.com/paperless-ngx/paperless-ngx/pull/11800))
|
||||||
|
- Chore(deps): Bump brotli from 1.1.0 to 1.2.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11796](https://github.com/paperless-ngx/paperless-ngx/pull/11796))
|
||||||
|
- Chore(deps): Bump transformers from 4.51.3 to 4.53.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11797](https://github.com/paperless-ngx/paperless-ngx/pull/11797))
|
||||||
|
- Chore(deps): Bump django from 5.2.7 to 5.2.9 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11794](https://github.com/paperless-ngx/paperless-ngx/pull/11794))
|
||||||
|
- Chore(deps): Bump the llama-index group @[dependabot[bot]](https://github.com/apps/dependabot) ([#11798](https://github.com/paperless-ngx/paperless-ngx/pull/11798))
|
||||||
|
- Chore(deps): Bump marshmallow from 3.26.1 to 3.26.2 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11790](https://github.com/paperless-ngx/paperless-ngx/pull/11790))
|
||||||
|
- Chore(deps): Bump uv from 0.9.3 to 0.9.6 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11795](https://github.com/paperless-ngx/paperless-ngx/pull/11795))
|
||||||
|
- Chore(deps): Bump aiohttp from 3.11.18 to 3.13.3 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11789](https://github.com/paperless-ngx/paperless-ngx/pull/11789))
|
||||||
|
- Chore(deps): Bump urllib3 from 2.5.0 to 2.6.3 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11792](https://github.com/paperless-ngx/paperless-ngx/pull/11792))
|
||||||
|
- Chore(deps): Bump virtualenv from 20.34.0 to 20.36.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11774](https://github.com/paperless-ngx/paperless-ngx/pull/11774))
|
||||||
|
- Fix: use explicit order field for workflow actions [@shamoon](https://github.com/shamoon) ([#11781](https://github.com/paperless-ngx/paperless-ngx/pull/11781))
|
||||||
|
- Chore(deps): Bump azure-core from 1.33.0 to 1.38.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11776](https://github.com/paperless-ngx/paperless-ngx/pull/11776))
|
||||||
|
- Feature: Paperless AI [@shamoon](https://github.com/shamoon) ([#10319](https://github.com/paperless-ngx/paperless-ngx/pull/10319))
|
||||||
|
- Performance: improve treenode inefficiencies [@shamoon](https://github.com/shamoon) ([#11606](https://github.com/paperless-ngx/paperless-ngx/pull/11606))
|
||||||
|
- Chore: upgrade to node v24 [@shamoon](https://github.com/shamoon) ([#11747](https://github.com/paperless-ngx/paperless-ngx/pull/11747))
|
||||||
|
- Chore: upgrade to Angular v21 [@shamoon](https://github.com/shamoon) ([#11746](https://github.com/paperless-ngx/paperless-ngx/pull/11746))
|
||||||
|
- Feature: Remote OCR (Azure AI) [@shamoon](https://github.com/shamoon) ([#10320](https://github.com/paperless-ngx/paperless-ngx/pull/10320))
|
||||||
|
- Feature: password removal action [@shamoon](https://github.com/shamoon) ([#11656](https://github.com/paperless-ngx/paperless-ngx/pull/11656))
|
||||||
|
- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735))
|
||||||
|
- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731))
|
||||||
|
- Tweakhancement: use anchor element for management list quick filter buttons [@shamoon](https://github.com/shamoon) ([#11692](https://github.com/paperless-ngx/paperless-ngx/pull/11692))
|
||||||
|
- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666))
|
||||||
|
- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661))
|
||||||
|
- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659))
|
||||||
|
- Fix: prevent ASN collisions for merge operations [@shamoon](https://github.com/shamoon) ([#11634](https://github.com/paperless-ngx/paperless-ngx/pull/11634))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 2.20.15
|
## paperless-ngx 2.20.15
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
@@ -7045,7 +8026,7 @@ primarily.
|
|||||||
|
|
||||||
### paperless-ng 0.9.0
|
### paperless-ng 0.9.0
|
||||||
|
|
||||||
- **Deprecated:** GnuPG. [See this note on the state of GnuPG in paperless-ng.](administration.md#encryption)
|
- **Deprecated:** GnuPG. [See this note on the state of GnuPG in paperless-ng.](#paperless-ng-093)
|
||||||
This features will most likely be removed in future versions.
|
This features will most likely be removed in future versions.
|
||||||
- **Added:** New frontend. Features:
|
- **Added:** New frontend. Features:
|
||||||
- Single page application: It's much more responsive than the
|
- Single page application: It's much more responsive than the
|
||||||
|
|||||||
+56
-21
@@ -522,22 +522,36 @@ do CORS calls. Set this to your public domain name.
|
|||||||
fail2ban with log entries for failed authorization attempts. Value should be
|
fail2ban with log entries for failed authorization attempts. Value should be
|
||||||
IP address(es).
|
IP address(es).
|
||||||
|
|
||||||
This setting also controls allauth's
|
By default, this setting also controls allauth's trusted proxy count,
|
||||||
[`ALLAUTH_TRUSTED_PROXY_COUNT`](https://docs.allauth.org/en/latest/account/configuration.html),
|
which is set to the number of proxies listed here. Override that default
|
||||||
which is set to the number of proxies listed here. Without this,
|
with [`PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT`](#PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT)
|
||||||
allauth cannot determine the client IP address for rate limiting when
|
when the list length does not match the number of proxy hops.
|
||||||
running behind a reverse proxy, resulting in a `403 Forbidden` on login.
|
|
||||||
|
|
||||||
Defaults to empty string.
|
Defaults to empty string.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT=<integer>`](#PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT) {#PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT}
|
||||||
|
|
||||||
|
: Sets allauth's
|
||||||
|
[`ALLAUTH_TRUSTED_PROXY_COUNT`](https://docs.allauth.org/en/latest/common/rate_limits.html#configuration).
|
||||||
|
This is the number of trusted proxy **hops** represented in each
|
||||||
|
`X-Forwarded-For` header, not the number of IP addresses through which those
|
||||||
|
proxies may be reached. For example, a single dual-stack proxy is one hop even
|
||||||
|
when its IPv4 and IPv6 addresses are both listed in
|
||||||
|
[`PAPERLESS_TRUSTED_PROXIES`](#PAPERLESS_TRUSTED_PROXIES).
|
||||||
|
|
||||||
|
Only trust `X-Forwarded-For` when untrusted clients cannot connect directly
|
||||||
|
to Paperless-ngx.
|
||||||
|
|
||||||
|
Defaults to the number of entries in `PAPERLESS_TRUSTED_PROXIES`.
|
||||||
|
|
||||||
#### [`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER=<header-name>`](#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER) {#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER}
|
#### [`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER=<header-name>`](#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER) {#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER}
|
||||||
|
|
||||||
: Sets allauth's
|
: Sets allauth's
|
||||||
[`ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](https://docs.allauth.org/en/latest/account/configuration.html).
|
[`ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](https://docs.allauth.org/en/latest/common/rate_limits.html#configuration).
|
||||||
Use this when your reverse proxy sets a dedicated header for the real
|
Use this when your reverse proxy sets a dedicated header for the real
|
||||||
client IP instead of `X-Forwarded-For`, for example `X-Real-IP` (nginx)
|
client IP instead of `X-Forwarded-For`, for example `X-Real-IP` (nginx)
|
||||||
or `CF-Connecting-IP` (Cloudflare). When set, this takes precedence over
|
or `CF-Connecting-IP` (Cloudflare). When set, this takes precedence over
|
||||||
[`PAPERLESS_TRUSTED_PROXIES`](#PAPERLESS_TRUSTED_PROXIES).
|
`PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT`.
|
||||||
|
|
||||||
Defaults to none.
|
Defaults to none.
|
||||||
|
|
||||||
@@ -918,8 +932,8 @@ for display in the web interface.
|
|||||||
| Document type | `never` | `auto` (default) | `always` |
|
| Document type | `never` | `auto` (default) | `always` |
|
||||||
| -------------------------- | ------- | -------------------------- | -------- |
|
| -------------------------- | ------- | -------------------------- | -------- |
|
||||||
| Scanned image (TIFF, JPEG) | No | **Yes** | Yes |
|
| Scanned image (TIFF, JPEG) | No | **Yes** | Yes |
|
||||||
| Image-based PDF | No | **Yes** (short/no text, untagged) | Yes |
|
| Image-based PDF | No | **Yes** (no embedded text) | Yes |
|
||||||
| Born-digital PDF | No | No (tagged or has embedded text) | Yes |
|
| Born-digital PDF | No | No (has embedded text, optionally confirmed by tag) | Yes |
|
||||||
| Plain text, email, HTML | No | No | No |
|
| Plain text, email, HTML | No | No | No |
|
||||||
| DOCX / ODT (via Tika) | Yes\* | Yes\* | Yes\* |
|
| DOCX / ODT (via Tika) | Yes\* | Yes\* | Yes\* |
|
||||||
|
|
||||||
@@ -1159,19 +1173,21 @@ still perform some basic text pre-processing before matching.
|
|||||||
|
|
||||||
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
|
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
|
||||||
|
|
||||||
Specifies which language Paperless should use when parsing dates from documents.
|
: Specifies which language Paperless should use when parsing dates from documents.
|
||||||
|
|
||||||
This should be a language code supported by the dateparser library,
|
: This should be a language code supported by the dateparser library,
|
||||||
for example: "en", or a combination such as "en+de".
|
for example: "en", or a combination such as "en+de".
|
||||||
Locales are also supported (e.g., "en-AU").
|
Locales are also supported (e.g., "en-AU").
|
||||||
Multiple languages can be combined using "+", for example: "en+de" or "en-AU+de".
|
Multiple languages can be combined using "+", for example: "en+de" or "en-AU+de".
|
||||||
For valid values, refer to the list of supported languages and locales in the [dateparser documentation](https://dateparser.readthedocs.io/en/latest/supported_locales.html).
|
For valid values, refer to the list of supported languages and locales in the [dateparser documentation](https://dateparser.readthedocs.io/en/latest/supported_locales.html).
|
||||||
|
|
||||||
|
: Set this to match the languages in which most of your documents are written.
|
||||||
|
|
||||||
Set this to match the languages in which most of your documents are written.
|
|
||||||
If not set, Paperless will attempt to infer the language(s) from the OCR configuration (`PAPERLESS_OCR_LANGUAGE`).
|
If not set, Paperless will attempt to infer the language(s) from the OCR configuration (`PAPERLESS_OCR_LANGUAGE`).
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
This format differs from the `PAPERLESS_OCR_LANGUAGE` setting, which uses ISO 639-2 codes (3 letters, e.g., "eng+deu" for Tesseract OCR).
|
|
||||||
|
This format differs from the `PAPERLESS_OCR_LANGUAGE` setting, which uses ISO 639-2 codes (3 letters, e.g., "eng+deu" for Tesseract OCR).
|
||||||
|
|
||||||
#### [`PAPERLESS_EMAIL_TASK_CRON=<cron expression>`](#PAPERLESS_EMAIL_TASK_CRON) {#PAPERLESS_EMAIL_TASK_CRON}
|
#### [`PAPERLESS_EMAIL_TASK_CRON=<cron expression>`](#PAPERLESS_EMAIL_TASK_CRON) {#PAPERLESS_EMAIL_TASK_CRON}
|
||||||
|
|
||||||
@@ -1344,13 +1360,16 @@ don't exist yet.
|
|||||||
#### [`PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>`](#PAPERLESS_CONSUMER_IGNORE_PATTERNS) {#PAPERLESS_CONSUMER_IGNORE_PATTERNS}
|
#### [`PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>`](#PAPERLESS_CONSUMER_IGNORE_PATTERNS) {#PAPERLESS_CONSUMER_IGNORE_PATTERNS}
|
||||||
|
|
||||||
: Additional regex patterns for files to ignore in the consumption directory. Patterns are matched against filenames only (not full paths)
|
: Additional regex patterns for files to ignore in the consumption directory. Patterns are matched against filenames only (not full paths)
|
||||||
using Python's `re.match()`, which anchors at the start of the filename.
|
using Python's `re.search()`. Use `^` to anchor a pattern to the start of the filename and `$` to anchor it to the end.
|
||||||
|
|
||||||
See the [watchfiles documentation](https://watchfiles.helpmanual.io/api/filters/#watchfiles.BaseFilter.ignore_entity_patterns)
|
See the [watchfiles documentation](https://watchfiles.helpmanual.io/api/filters/#watchfiles.BaseFilter.ignore_entity_patterns)
|
||||||
|
|
||||||
This setting is for additional patterns beyond the built-in defaults. Common system files and directories are already ignored automatically.
|
This setting is for additional patterns beyond the built-in defaults. Common system files and directories are already ignored automatically.
|
||||||
The patterns will be compiled via Python's standard `re` module.
|
The patterns will be compiled via Python's standard `re` module.
|
||||||
|
|
||||||
|
These are regular expressions, not glob patterns. For example, the glob pattern `._*` does not mean "starts with `._`" when used as a
|
||||||
|
regular expression; it matches nearly any non-empty filename. Use `^\._.*` for that behavior instead.
|
||||||
|
|
||||||
Example custom patterns:
|
Example custom patterns:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -1365,7 +1384,11 @@ using Python's `re.match()`, which anchors at the start of the filename.
|
|||||||
|
|
||||||
Defaults to `[]` (empty list, uses only built-in defaults).
|
Defaults to `[]` (empty list, uses only built-in defaults).
|
||||||
|
|
||||||
The default ignores are `[.DS_Store, .DS_STORE, ._*, desktop.ini, Thumbs.db]` and cannot be overridden.
|
The built-in file patterns are equivalent to the following regular expressions and cannot be overridden:
|
||||||
|
|
||||||
|
```json
|
||||||
|
["^\\.DS_Store$", "^\\.DS_STORE$", "^\\._.*", "^desktop\\.ini$", "^Thumbs\\.db$"]
|
||||||
|
```
|
||||||
|
|
||||||
#### [`PAPERLESS_CONSUMER_IGNORE_DIRS=<json>`](#PAPERLESS_CONSUMER_IGNORE_DIRS) {#PAPERLESS_CONSUMER_IGNORE_DIRS}
|
#### [`PAPERLESS_CONSUMER_IGNORE_DIRS=<json>`](#PAPERLESS_CONSUMER_IGNORE_DIRS) {#PAPERLESS_CONSUMER_IGNORE_DIRS}
|
||||||
|
|
||||||
@@ -2024,6 +2047,18 @@ password. All of these options come from their similarly-named [Django settings]
|
|||||||
|
|
||||||
Defaults to None.
|
Defaults to None.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_REMOTE_OCR_MODE=<str>`](#PAPERLESS_REMOTE_OCR_MODE) {#PAPERLESS_REMOTE_OCR_MODE}
|
||||||
|
|
||||||
|
: Which documents are sent to the remote OCR engine.
|
||||||
|
|
||||||
|
- `always`: every document of a supported file type is sent to the remote
|
||||||
|
engine, bypassing the local OCR engine.
|
||||||
|
- `workflow_only`: documents are processed locally unless a workflow
|
||||||
|
explicitly enables remote OCR for them, letting you use the remote engine
|
||||||
|
selectively.
|
||||||
|
|
||||||
|
Defaults to "always".
|
||||||
|
|
||||||
## AI {#ai}
|
## AI {#ai}
|
||||||
|
|
||||||
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
|
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
|
||||||
@@ -2119,7 +2154,7 @@ used with the OpenAI-compatible backend to target a custom provider or local gat
|
|||||||
|
|
||||||
Defaults to None.
|
Defaults to None.
|
||||||
|
|
||||||
### [`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE=<str>`](#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE) {#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE}
|
#### [`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE=<str>`](#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE) {#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE}
|
||||||
|
|
||||||
: The language to use for AI suggestions (results may vary by LLM model). If not supplied, defaults to the user's UI language setting or None.
|
: The language to use for AI suggestions (results may vary by LLM model). If not supplied, defaults to the user's UI language setting or None.
|
||||||
|
|
||||||
|
|||||||
@@ -456,6 +456,20 @@ def score(
|
|||||||
return 10
|
return 10
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Remote services**
|
||||||
|
|
||||||
|
If your parser sends document content to a remote service, declare it:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyCustomParser:
|
||||||
|
uses_remote_service = True
|
||||||
|
```
|
||||||
|
|
||||||
|
Paperless-ngx excludes such parsers when the document being consumed has not
|
||||||
|
been marked for remote processing, so users can keep remote OCR off by default
|
||||||
|
and enable it selectively with a workflow. Parsers that do not declare the
|
||||||
|
attribute are treated as fully local and are always considered.
|
||||||
|
|
||||||
**Archive and rendition flags**
|
**Archive and rendition flags**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
+66
-5
@@ -133,7 +133,7 @@ PAPERLESS_DB_OPTIONS="sslmode=require,sslrootcert=/certs/ca.pem,pool.max_size=10
|
|||||||
|
|
||||||
## OCR and Archive File Generation Settings
|
## OCR and Archive File Generation Settings
|
||||||
|
|
||||||
The settings that control OCR behaviour and archive file generation have been redesigned. The old settings that coupled these two concerns together are **removed** — old values are not silently honoured; a startup warning is logged if any removed variable is still set in your environment.
|
The settings that control OCR behaviour and archive file generation have been redesigned. The old settings that coupled these two concerns together are **removed** - old values are not silently honoured; a startup warning is logged if any removed variable is still set in your environment.
|
||||||
|
|
||||||
### Removed settings
|
### Removed settings
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ Remove any `PAPERLESS_OCR_SKIP_ARCHIVE_FILE` variable from your environment. If
|
|||||||
# v2: skip OCR when text present, always archive
|
# v2: skip OCR when text present, always archive
|
||||||
PAPERLESS_OCR_MODE=skip
|
PAPERLESS_OCR_MODE=skip
|
||||||
# v3: equivalent (auto is the new default)
|
# v3: equivalent (auto is the new default)
|
||||||
# No change needed — auto is the default
|
# No change needed - auto is the default
|
||||||
|
|
||||||
# v2: skip OCR when text present, skip archive too
|
# v2: skip OCR when text present, skip archive too
|
||||||
PAPERLESS_OCR_MODE=skip_noarchive
|
PAPERLESS_OCR_MODE=skip_noarchive
|
||||||
@@ -189,10 +189,10 @@ PAPERLESS_ARCHIVE_FILE_GENERATION=auto
|
|||||||
|
|
||||||
If you use the **remote OCR parser** (Azure AI), note that it always produces a
|
If you use the **remote OCR parser** (Azure AI), note that it always produces a
|
||||||
searchable PDF and stores it as the archive copy. `ARCHIVE_FILE_GENERATION=never`
|
searchable PDF and stores it as the archive copy. `ARCHIVE_FILE_GENERATION=never`
|
||||||
has no effect for documents handled by the remote parser — the archive is produced
|
has no effect for documents handled by the remote parser - the archive is produced
|
||||||
unconditionally by the remote engine.
|
unconditionally by the remote engine.
|
||||||
|
|
||||||
# Search Index (Whoosh -> Tantivy)
|
## Search Index (Whoosh -> Tantivy)
|
||||||
|
|
||||||
The full-text search backend has been replaced with [Tantivy](https://github.com/quickwit-oss/tantivy).
|
The full-text search backend has been replaced with [Tantivy](https://github.com/quickwit-oss/tantivy).
|
||||||
The index format is incompatible with Whoosh, so **the search index is automatically rebuilt from
|
The index format is incompatible with Whoosh, so **the search index is automatically rebuilt from
|
||||||
@@ -324,8 +324,69 @@ option since v1.8.0.
|
|||||||
Allauth changed how it determines the client IP address for login rate limiting. Users running
|
Allauth changed how it determines the client IP address for login rate limiting. Users running
|
||||||
behind a reverse proxy may need to set
|
behind a reverse proxy may need to set
|
||||||
[`PAPERLESS_TRUSTED_PROXIES`](configuration.md#PAPERLESS_TRUSTED_PROXIES),
|
[`PAPERLESS_TRUSTED_PROXIES`](configuration.md#PAPERLESS_TRUSTED_PROXIES),
|
||||||
|
[`PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_PROXY_COUNT),
|
||||||
[`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER),
|
[`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER),
|
||||||
or both, to avoid `403 Forbidden` errors on login.
|
or a combination of these settings to avoid `403 Forbidden` errors on login.
|
||||||
|
The proxy count is the number of proxy hops in `X-Forwarded-For`, which may
|
||||||
|
differ from the number of configured proxy IP addresses.
|
||||||
|
|
||||||
|
## Minimum CPU Requirements (NumPy Baseline)
|
||||||
|
|
||||||
|
Starting with NumPy 2.4.0, official `manylinux` x86_64 wheels are compiled with a minimum
|
||||||
|
CPU baseline of `x86-64-v2`, which requires SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, and
|
||||||
|
CMPXCHG16B. This is a [deliberate upstream change](https://github.com/numpy/numpy/issues/27851)
|
||||||
|
citing that these instructions have been present in over 99.7% of CPUs since 2008
|
||||||
|
([Intel](<https://en.wikipedia.org/wiki/Nehalem_(microarchitecture)>)) or 2011
|
||||||
|
([AMD](<https://en.wikipedia.org/wiki/Bulldozer_(microarchitecture)>)).
|
||||||
|
|
||||||
|
NumPy is a dependency of the document classifier (via scikit-learn), so any CPU that
|
||||||
|
predates SSE4.2 support will crash with `SIGILL` (illegal instruction) when the classifier
|
||||||
|
is loaded or trained, regardless of whether AI features are enabled.
|
||||||
|
|
||||||
|
This differs from NumPy's optional SIMD dispatch (e.g. AVX2, AVX512), which is detected and
|
||||||
|
selected safely at runtime - the `x86-64-v2` requirement above is a hard floor baked into the
|
||||||
|
wheel, with no runtime fallback.
|
||||||
|
|
||||||
|
### Affected hardware
|
||||||
|
|
||||||
|
CPUs older than roughly 2008 (Intel) or 2011 (AMD) that lack SSE4.2 support. This is more
|
||||||
|
likely to affect low-power or embedded hardware - e.g. early Atom, Celeron, or pre-Bulldozer
|
||||||
|
AMD chips - than typical desktop or server hardware from the last decade.
|
||||||
|
|
||||||
|
Check for SSE4.2 support with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -o -m1 sse4_2 /proc/cpuinfo
|
||||||
|
```
|
||||||
|
|
||||||
|
If this prints nothing, your CPU is affected.
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
|
||||||
|
The Celery worker (and potentially the web server) repeatedly crashes and restarts with a
|
||||||
|
`SIGILL` error, typically visible in `dmesg`/`journalctl` as a `trap invalid opcode` inside
|
||||||
|
`_multiarray_umath...so`. Because the classifier is trained on a periodic schedule
|
||||||
|
(hourly, by default), affected instances see intermittent, hard-to-reproduce document
|
||||||
|
consumption failures whenever that scheduled task runs and takes down the worker process
|
||||||
|
mid-task.
|
||||||
|
|
||||||
|
### Action Required (for affected hardware only)
|
||||||
|
|
||||||
|
There is no way to make the classifier itself work on such CPUs - it requires an unofficial
|
||||||
|
NumPy build with `cpu-baseline=none`, which is not something we can ship. The practical
|
||||||
|
path forward is to stop the classifier from ever loading or training, which avoids
|
||||||
|
importing NumPy at all:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PAPERLESS_TRAIN_TASK_CRON=disable
|
||||||
|
```
|
||||||
|
|
||||||
|
This disables the periodic classifier training task (see
|
||||||
|
[`PAPERLESS_TRAIN_TASK_CRON`](configuration.md#PAPERLESS_TRAIN_TASK_CRON)). Automatic
|
||||||
|
matching based on the classifier (suggested correspondents, document types, tags, and
|
||||||
|
storage paths from trained rules) will no longer be available, but rule-based matching is
|
||||||
|
unaffected, and document consumption itself will no longer be at risk of crashing the
|
||||||
|
worker.
|
||||||
|
|
||||||
## Database Migrations
|
## Database Migrations
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -157,8 +157,8 @@ Do not combine this with `USERMAP_UID` or `USERMAP_GID`, which are intended for
|
|||||||
Some file systems, such as NFS network shares, don't support file system
|
Some file systems, such as NFS network shares, don't support file system
|
||||||
notifications with `inotify`. When the consumption directory is on such a
|
notifications with `inotify`. When the consumption directory is on such a
|
||||||
file system, Paperless-ngx will not pick up new files with the default
|
file system, Paperless-ngx will not pick up new files with the default
|
||||||
configuration. Use [`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING)
|
configuration. Set [`PAPERLESS_CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL)
|
||||||
to enable polling and disable inotify. See [here](configuration.md#polling).
|
to a positive number to enable polling and disable native filesystem notifications.
|
||||||
|
|
||||||
## Bare Metal Install {#bare_metal}
|
## Bare Metal Install {#bare_metal}
|
||||||
|
|
||||||
@@ -262,6 +262,14 @@ to enable polling and disable inotify. See [here](configuration.md#polling).
|
|||||||
and copy the contents to the home directory of the user you created
|
and copy the contents to the home directory of the user you created
|
||||||
earlier (`/opt/paperless`).
|
earlier (`/opt/paperless`).
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
If you are updating an existing bare-metal installation rather than
|
||||||
|
installing for the first time, see the
|
||||||
|
[bare metal update instructions](administration.md#bare-metal-updating)
|
||||||
|
instead -- extracting a new release on top of an old one can leave
|
||||||
|
behind stale files from the previous version.
|
||||||
|
|
||||||
Optional: If you cloned the Git repository, you will need to
|
Optional: If you cloned the Git repository, you will need to
|
||||||
compile the frontend yourself. See [here](development.md#front-end-development)
|
compile the frontend yourself. See [here](development.md#front-end-development)
|
||||||
and use the `build` step, not `serve`.
|
and use the `build` step, not `serve`.
|
||||||
|
|||||||
+48
-12
@@ -458,8 +458,8 @@ For related metadata such as tags, correspondents, document types, and storage p
|
|||||||
### Password reset
|
### Password reset
|
||||||
|
|
||||||
In order to enable the password reset feature you will need to setup an SMTP backend, see
|
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). If your installation does not have
|
[`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST). You should also set
|
||||||
[`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) set, the reset link included in emails will use the server host.
|
[`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) and / or its corresponding configuration settings.
|
||||||
|
|
||||||
### Two-factor authentication
|
### Two-factor authentication
|
||||||
|
|
||||||
@@ -620,6 +620,34 @@ no other workflow will be executed on the document.
|
|||||||
If a "Move to Trash" action is executed in a consume pipeline, the consumption
|
If a "Move to Trash" action is executed in a consume pipeline, the consumption
|
||||||
will be aborted and the file will be deleted.
|
will be aborted and the file will be deleted.
|
||||||
|
|
||||||
|
##### Password Removal {#workflow-action-password-removal}
|
||||||
|
|
||||||
|
"Password Removal" actions attempt to remove password protection from encrypted PDF documents. You can specify:
|
||||||
|
|
||||||
|
- One or more passwords to try, separated by commas or new lines
|
||||||
|
- Each password is tried in order until one successfully unlocks the document
|
||||||
|
|
||||||
|
Password removal never modifies a file in place. Instead, once a working password is found, the
|
||||||
|
decrypted content is consumed as a new [document version](#document-file-versions), leaving the
|
||||||
|
original (still encrypted) version in the document's version history.
|
||||||
|
|
||||||
|
**Consumption Started**: because this trigger fires before the document exists yet, the password
|
||||||
|
removal itself is deferred until after the initial consumption of the encrypted file has completed.
|
||||||
|
OCR engines cannot process an encrypted PDF, so this first version is typically stored with no
|
||||||
|
extracted text (unless the file already contained extractable text outside of OCR). Immediately
|
||||||
|
afterwards, the password is removed and the decrypted file is automatically re-consumed as a second,
|
||||||
|
new version of the same document, this time with normal OCR/text extraction applied. In other words,
|
||||||
|
a password-protected file added with this trigger will briefly exist as an un-OCR'd version before
|
||||||
|
the properly processed version is created.
|
||||||
|
|
||||||
|
**Document Added**, **Document Updated**, **Scheduled**: these triggers run against a document that
|
||||||
|
already exists, so password removal happens immediately: the decrypted content is queued for
|
||||||
|
consumption as a new version right away. Note that if the document's initial consumption also
|
||||||
|
happened while it was still encrypted, that original version will likewise be missing OCR text.
|
||||||
|
|
||||||
|
**Current limitation**: Passwords are stored as a simple list without descriptions. To handle
|
||||||
|
multiple PDF types with different passwords, create separate workflows for each use case.
|
||||||
|
|
||||||
#### Workflow placeholders
|
#### Workflow placeholders
|
||||||
|
|
||||||
Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
||||||
@@ -833,10 +861,10 @@ contract you signed 8 years ago).
|
|||||||
|
|
||||||
When you search paperless for a document, it tries to match this query
|
When you search paperless for a document, it tries to match this query
|
||||||
against your documents. Paperless will look for matching documents by
|
against your documents. Paperless will look for matching documents by
|
||||||
inspecting their content, title, correspondent, type, tags, notes, and
|
inspecting their content, title, correspondent, type, and tags. Paperless
|
||||||
custom field values. Paperless returns a scored list of results, so that
|
returns a scored list of results, so that documents matching your query
|
||||||
documents matching your query better will appear further up in the search
|
better will appear further up in the search results. Notes and custom field
|
||||||
results.
|
values can be searched using the advanced search syntax described below.
|
||||||
|
|
||||||
By default, paperless returns only documents which contain all words
|
By default, paperless returns only documents which contain all words
|
||||||
typed in the search bar. A few things to know about how matching works:
|
typed in the search bar. A few things to know about how matching works:
|
||||||
@@ -891,9 +919,9 @@ Supported date keywords: `today`, `yesterday`, `previous week`,
|
|||||||
|
|
||||||
#### Searching custom fields
|
#### Searching custom fields
|
||||||
|
|
||||||
Custom field values are included in the full-text index, so a plain search
|
Custom field names and values are included in the full-text index, but they
|
||||||
already matches documents whose custom field values contain your search terms.
|
are not searched by a plain, unqualified query. Use the advanced search syntax
|
||||||
To narrow by field name or value specifically:
|
to search by field name or value:
|
||||||
|
|
||||||
```
|
```
|
||||||
custom_fields.value:policy
|
custom_fields.value:policy
|
||||||
@@ -921,8 +949,9 @@ custom_fields.name:"Contract Number" custom_fields.value:1312
|
|||||||
|
|
||||||
#### Searching notes
|
#### Searching notes
|
||||||
|
|
||||||
Notes content is included in full-text search automatically. To search
|
Notes are included in the full-text index, but they are not searched by a
|
||||||
by note author or content specifically:
|
plain, unqualified query. Use the advanced search syntax to search by note
|
||||||
|
author or content:
|
||||||
|
|
||||||
```
|
```
|
||||||
notes.user:alice
|
notes.user:alice
|
||||||
@@ -1055,11 +1084,18 @@ Paperless-ngx supports performing OCR on documents using remote services. At the
|
|||||||
[Microsoft's Azure "Document Intelligence" service](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence).
|
[Microsoft's Azure "Document Intelligence" service](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence).
|
||||||
This is of course a paid service (with a free tier) which requires an Azure account and subscription. Azure AI is not affiliated with
|
This is of course a paid service (with a free tier) which requires an Azure account and subscription. Azure AI is not affiliated with
|
||||||
Paperless-ngx in any way. When enabled, Paperless-ngx will automatically send appropriate documents to Azure for OCR processing, bypassing
|
Paperless-ngx in any way. When enabled, Paperless-ngx will automatically send appropriate documents to Azure for OCR processing, bypassing
|
||||||
the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details.
|
the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details. These
|
||||||
|
settings can be supplied as environment variables or via **Application Configuration**.
|
||||||
|
|
||||||
Additionally, when using a commercial service with this feature, consider both potential costs as well as any associated file size
|
Additionally, when using a commercial service with this feature, consider both potential costs as well as any associated file size
|
||||||
or page limitations (e.g. with a free tier).
|
or page limitations (e.g. with a free tier).
|
||||||
|
|
||||||
|
By default, every document of a supported file type is sent to the remote engine. To use it more selectively, set the
|
||||||
|
[remote OCR mode](configuration.md#PAPERLESS_REMOTE_OCR_MODE) to `workflow_only`. Documents are then processed locally
|
||||||
|
unless a workflow explicitly enables remote OCR for them, so you can limit the remote engine to particular documents.
|
||||||
|
|
||||||
|
Setting the mode to `workflow_only` also allows the **Reprocess** actions to selectively use remote OCR for individual documents.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Paperless-ngx consists of the following components:
|
Paperless-ngx consists of the following components:
|
||||||
|
|||||||
+26
-26
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "3.0.0"
|
version = "3.0.5"
|
||||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
@@ -21,7 +21,7 @@ dependencies = [
|
|||||||
"channels~=4.2",
|
"channels~=4.2",
|
||||||
"channels-redis~=4.2",
|
"channels-redis~=4.2",
|
||||||
"concurrent-log-handler~=0.9.25",
|
"concurrent-log-handler~=0.9.25",
|
||||||
"dateparser~=1.2",
|
"dateparser~=1.4",
|
||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
"django~=5.2.13",
|
"django~=5.2.13",
|
||||||
@@ -32,33 +32,32 @@ dependencies = [
|
|||||||
"django-cors-headers~=4.9.0",
|
"django-cors-headers~=4.9.0",
|
||||||
"django-extensions~=4.1",
|
"django-extensions~=4.1",
|
||||||
"django-filter~=25.1",
|
"django-filter~=25.1",
|
||||||
"django-guardian~=3.3.0",
|
"django-guardian~=3.3.3",
|
||||||
"django-multiselectfield~=1.0.1",
|
"django-multiselectfield~=1.0.1",
|
||||||
"django-rich~=2.2.0",
|
"django-rich~=2.2.0",
|
||||||
"django-soft-delete~=1.0.18",
|
"django-soft-delete~=1.0.18",
|
||||||
"django-treenode>=0.24",
|
"django-treenode>=0.24",
|
||||||
"djangorestframework~=3.16",
|
"djangorestframework~=3.16",
|
||||||
"djangorestframework-guardian~=0.4.0",
|
"drf-spectacular~=0.30",
|
||||||
"drf-spectacular~=0.28",
|
"drf-spectacular-sidecar~=2026.7.1",
|
||||||
"drf-spectacular-sidecar~=2026.5.1",
|
|
||||||
"drf-writable-nested~=0.7.1",
|
"drf-writable-nested~=0.7.1",
|
||||||
"filelock~=3.29.0",
|
"filelock~=3.32.0",
|
||||||
"flower~=2.0.1",
|
"flower~=2.0.1",
|
||||||
"gotenberg-client~=0.14.0",
|
"gotenberg-client~=0.14.0",
|
||||||
"httpx-oauth~=0.16",
|
"httpx-oauth~=0.17",
|
||||||
"ijson>=3.2",
|
"ijson>=3.5.1",
|
||||||
"imap-tools~=1.13.0",
|
"imap-tools~=1.14.0",
|
||||||
"jinja2~=3.1.5",
|
"jinja2~=3.1.5",
|
||||||
"langdetect~=1.0.9",
|
"langdetect~=1.0.9",
|
||||||
"llama-index-core>=0.14.22",
|
"llama-index-core>=0.14.23",
|
||||||
"llama-index-embeddings-huggingface>=0.6.1",
|
"llama-index-embeddings-huggingface>=0.6.1",
|
||||||
"llama-index-embeddings-ollama>=0.9",
|
"llama-index-embeddings-ollama>=0.9",
|
||||||
"llama-index-embeddings-openai-like>=0.2.2",
|
"llama-index-embeddings-openai-like>=0.2.2",
|
||||||
"llama-index-llms-ollama>=0.9.1",
|
"llama-index-llms-ollama>=0.9.1",
|
||||||
"llama-index-llms-openai-like>=0.7.1",
|
"llama-index-llms-openai-like>=0.7.1",
|
||||||
"nltk~=3.10.0",
|
"nltk~=3.10.0",
|
||||||
"ocrmypdf~=17.4.2",
|
"ocrmypdf~=17.7.0",
|
||||||
"openai>=2.32",
|
"openai>=2.48",
|
||||||
"pathvalidate~=3.3.1",
|
"pathvalidate~=3.3.1",
|
||||||
"pdf2image~=1.17.0",
|
"pdf2image~=1.17.0",
|
||||||
"python-dateutil~=2.9.0",
|
"python-dateutil~=2.9.0",
|
||||||
@@ -68,17 +67,17 @@ dependencies = [
|
|||||||
"python-magic~=0.4.27",
|
"python-magic~=0.4.27",
|
||||||
"rapidfuzz~=3.14.5",
|
"rapidfuzz~=3.14.5",
|
||||||
"redis[hiredis]~=5.2.1",
|
"redis[hiredis]~=5.2.1",
|
||||||
"regex>=2026.4.4",
|
"regex>=2026.7.19",
|
||||||
"scikit-learn~=1.8.0",
|
"scikit-learn~=1.9.0",
|
||||||
"sentence-transformers>=5.4.1",
|
"sentence-transformers>=5.6.1",
|
||||||
"setproctitle~=1.3.4",
|
"setproctitle~=1.3.4",
|
||||||
"sqlite-vec==0.1.9",
|
"sqlite-vec==0.1.9",
|
||||||
"tantivy~=0.26.0",
|
"tantivy~=0.26.0",
|
||||||
"tika-client~=0.11.0",
|
"tika-client~=0.11.0",
|
||||||
"torch~=2.12.0",
|
"torch~=2.13.0",
|
||||||
"watchfiles>=1.1.1",
|
"watchfiles>=1.2",
|
||||||
"whitenoise~=6.11",
|
"whitenoise~=6.11",
|
||||||
"zxing-cpp~=3.0.0",
|
"zxing-cpp~=3.1.0",
|
||||||
]
|
]
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
mariadb = [
|
mariadb = [
|
||||||
@@ -101,25 +100,25 @@ dev = [
|
|||||||
{ include-group = "testing" },
|
{ include-group = "testing" },
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
"zensical>=0.0.47",
|
"zensical>=0.0.51",
|
||||||
]
|
]
|
||||||
lint = [
|
lint = [
|
||||||
"prek~=0.3.10",
|
"prek~=0.4.11",
|
||||||
"ruff~=0.15.20",
|
"ruff~=0.16.1",
|
||||||
]
|
]
|
||||||
testing = [
|
testing = [
|
||||||
"daphne",
|
"daphne",
|
||||||
"factory-boy~=3.3.1",
|
"factory-boy~=3.3.1",
|
||||||
"faker~=40.15.0",
|
"faker~=40.36.0",
|
||||||
"imagehash",
|
"imagehash",
|
||||||
"pytest~=9.0.3",
|
"pytest~=9.1.1",
|
||||||
"pytest-cov~=7.1.0",
|
"pytest-cov~=7.1.0",
|
||||||
"pytest-django~=4.12.0",
|
"pytest-django~=4.12.0",
|
||||||
"pytest-env~=1.6.0",
|
"pytest-env~=1.7.0",
|
||||||
"pytest-httpx",
|
"pytest-httpx",
|
||||||
"pytest-mock~=3.15.1",
|
"pytest-mock~=3.15.1",
|
||||||
# "pytest-randomly~=4.0.1",
|
# "pytest-randomly~=4.0.1",
|
||||||
"pytest-rerunfailures~=16.1",
|
"pytest-rerunfailures~=16.4",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist~=3.8.0",
|
"pytest-xdist~=3.8.0",
|
||||||
"time-machine>=2.13",
|
"time-machine>=2.13",
|
||||||
@@ -183,6 +182,7 @@ output-format = "grouped"
|
|||||||
line-ending = "lf"
|
line-ending = "lf"
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
# https://docs.astral.sh/ruff/rules/
|
# https://docs.astral.sh/ruff/rules/
|
||||||
|
select = [ "E4", "E7", "E9", "F" ]
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
"COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com
|
||||||
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
"DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj
|
||||||
|
|||||||
+12
-9
@@ -56,13 +56,13 @@
|
|||||||
},
|
},
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-builders/custom-webpack:browser",
|
"builder": "@angular/build:application",
|
||||||
"options": {
|
"options": {
|
||||||
"customWebpackConfig": {
|
"outputPath": {
|
||||||
"path": "./extra-webpack.config.ts"
|
"base": "dist/paperless-ui",
|
||||||
|
"browser": ""
|
||||||
},
|
},
|
||||||
"outputPath": "dist/paperless-ui",
|
"browser": "src/main.ts",
|
||||||
"main": "src/main.ts",
|
|
||||||
"outputHashing": "none",
|
"outputHashing": "none",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
@@ -97,6 +97,7 @@
|
|||||||
"scripts": [],
|
"scripts": [],
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
"file-saver",
|
"file-saver",
|
||||||
|
"mime-names",
|
||||||
"utif"
|
"utif"
|
||||||
],
|
],
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
@@ -117,11 +118,13 @@
|
|||||||
"with": "src/environments/environment.prod.ts"
|
"with": "src/environments/environment.prod.ts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputPath": "../src/documents/static/frontend/",
|
"outputPath": {
|
||||||
|
"base": "../src/documents/static/frontend/",
|
||||||
|
"browser": ""
|
||||||
|
},
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "none",
|
"outputHashing": "none",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"namedChunks": false,
|
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
@@ -145,7 +148,7 @@
|
|||||||
"defaultConfiguration": ""
|
"defaultConfiguration": ""
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "paperless-ui:build:en-US"
|
"buildTarget": "paperless-ui:build:en-US"
|
||||||
},
|
},
|
||||||
@@ -156,7 +159,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-builders/custom-webpack:extract-i18n",
|
"builder": "@angular/build:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "paperless-ui:build"
|
"buildTarget": "paperless-ui:build"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test } from '@playwright/test'
|
import { expect, test, type WebSocketRoute } from '@playwright/test'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
const REQUESTS_HAR = path.join(__dirname, 'requests/api-document-detail.har')
|
const REQUESTS_HAR = path.join(__dirname, 'requests/api-document-detail.har')
|
||||||
@@ -95,3 +95,60 @@ test('should support quick filters', async ({ page }) => {
|
|||||||
.click()
|
.click()
|
||||||
await expect(page).toHaveURL(/tags__id__all=4&sort=created&reverse=1&page=1/)
|
await expect(page).toHaveURL(/tags__id__all=4&sort=created&reverse=1&page=1/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should finish reloading the preview after a remote document update', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
let resolveStatusSocket: (socket: WebSocketRoute) => void
|
||||||
|
const statusSocketReady = new Promise<WebSocketRoute>((resolve) => {
|
||||||
|
resolveStatusSocket = resolve
|
||||||
|
})
|
||||||
|
await page.routeWebSocket(/\/ws\/status\/$/, (socket) => {
|
||||||
|
resolveStatusSocket(socket)
|
||||||
|
})
|
||||||
|
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||||
|
let previewRequestCount = 0
|
||||||
|
page.on('request', (request) => {
|
||||||
|
if (request.url().includes('/api/documents/175/preview/')) {
|
||||||
|
previewRequestCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await page.goto('/documents/175/details')
|
||||||
|
|
||||||
|
await page.locator('pngx-document-detail').waitFor()
|
||||||
|
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
||||||
|
/\w+/
|
||||||
|
)
|
||||||
|
const previewWasLoaded = await page.evaluate(() => {
|
||||||
|
const detail = document.querySelector('pngx-document-detail')
|
||||||
|
const component = (window as any).ng.getComponent(detail)
|
||||||
|
component.pdfPreviewLoaded({ numPages: 1 })
|
||||||
|
return component.previewLoaded()
|
||||||
|
})
|
||||||
|
expect(previewWasLoaded).toBe(true)
|
||||||
|
const previewRequestsBeforeReload = previewRequestCount
|
||||||
|
|
||||||
|
const statusSocket = await statusSocketReady
|
||||||
|
const documentReloaded = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/api/documents/175/?full_perms=true') &&
|
||||||
|
response.request().method() === 'GET'
|
||||||
|
)
|
||||||
|
statusSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'document_updated',
|
||||||
|
data: {
|
||||||
|
document_id: 175,
|
||||||
|
modified: '2026-07-26T20:00:00Z',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
await documentReloaded
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText('Document reloaded with latest changes.').first()
|
||||||
|
).toBeVisible()
|
||||||
|
await expect
|
||||||
|
.poll(() => previewRequestCount)
|
||||||
|
.toBeGreaterThan(previewRequestsBeforeReload + 1)
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import {
|
|
||||||
CustomWebpackBrowserSchema,
|
|
||||||
TargetOptions,
|
|
||||||
} from '@angular-builders/custom-webpack'
|
|
||||||
import * as webpack from 'webpack'
|
|
||||||
const { codecovWebpackPlugin } = require('@codecov/webpack-plugin')
|
|
||||||
|
|
||||||
export default (
|
|
||||||
config: webpack.Configuration,
|
|
||||||
options: CustomWebpackBrowserSchema,
|
|
||||||
targetOptions: TargetOptions
|
|
||||||
) => {
|
|
||||||
if (config.plugins) {
|
|
||||||
config.plugins.push(
|
|
||||||
codecovWebpackPlugin({
|
|
||||||
enableBundleAnalysis: process.env.CODECOV_TOKEN !== undefined,
|
|
||||||
bundleName: 'paperless-ngx',
|
|
||||||
uploadToken: process.env.CODECOV_TOKEN,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
+980
-947
File diff suppressed because it is too large
Load Diff
+29
-32
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "3.0.0",
|
"version": "3.0.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
@@ -11,16 +11,16 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^22.0.3",
|
"@angular/cdk": "^22.0.6",
|
||||||
"@angular/common": "~22.0.5",
|
"@angular/common": "~22.1.0",
|
||||||
"@angular/compiler": "~22.0.5",
|
"@angular/compiler": "~22.1.0",
|
||||||
"@angular/core": "~22.0.5",
|
"@angular/core": "~22.1.0",
|
||||||
"@angular/forms": "~22.0.5",
|
"@angular/forms": "~22.1.0",
|
||||||
"@angular/localize": "~22.0.5",
|
"@angular/localize": "~22.1.0",
|
||||||
"@angular/platform-browser": "~22.0.5",
|
"@angular/platform-browser": "~22.1.0",
|
||||||
"@angular/router": "~22.0.5",
|
"@angular/router": "~22.1.0",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
|
||||||
"@ng-select/ng-select": "^23.2.0",
|
"@ng-select/ng-select": "^23.5.0",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
@@ -32,33 +32,31 @@
|
|||||||
"ngx-device-detector": "^12.0.0",
|
"ngx-device-detector": "^12.0.0",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^19.0.0",
|
"ngx-ui-tour-ng-bootstrap": "^19.0.0",
|
||||||
"normalize-diacritics": "^5.0.0",
|
"normalize-diacritics": "^5.0.0",
|
||||||
"pdfjs-dist": "^6.0.227",
|
"pdfjs-dist": "^6.2.108",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"uuid": "^14.0.1"
|
"uuid": "^14.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^22.0.1",
|
|
||||||
"@angular-builders/jest": "^22.0.1",
|
"@angular-builders/jest": "^22.0.1",
|
||||||
"@angular-devkit/core": "^22.0.5",
|
"@angular-devkit/core": "^22.1.2",
|
||||||
"@angular-devkit/schematics": "^22.0.5",
|
"@angular-devkit/schematics": "^22.1.2",
|
||||||
"@angular-eslint/builder": "22.0.0",
|
"@angular-eslint/builder": "22.1.0",
|
||||||
"@angular-eslint/eslint-plugin": "22.0.0",
|
"@angular-eslint/eslint-plugin": "22.1.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "22.0.0",
|
"@angular-eslint/eslint-plugin-template": "22.1.0",
|
||||||
"@angular-eslint/schematics": "22.0.0",
|
"@angular-eslint/schematics": "22.1.0",
|
||||||
"@angular-eslint/template-parser": "22.0.0",
|
"@angular-eslint/template-parser": "22.1.0",
|
||||||
"@angular/build": "^22.0.5",
|
"@angular/build": "22.1.2",
|
||||||
"@angular/cli": "~22.0.5",
|
"@angular/cli": "22.1.2",
|
||||||
"@angular/compiler-cli": "~22.0.5",
|
"@angular/compiler-cli": "~22.1.0",
|
||||||
"@codecov/webpack-plugin": "^2.0.1",
|
"@playwright/test": "^1.62.0",
|
||||||
"@playwright/test": "^1.61.1",
|
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^26.0.0",
|
"@types/node": "^26.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||||
"@typescript-eslint/parser": "^8.62.0",
|
"@typescript-eslint/parser": "^8.65.0",
|
||||||
"@typescript-eslint/utils": "^8.62.0",
|
"@typescript-eslint/utils": "^8.65.0",
|
||||||
"eslint": "^10.5.0",
|
"eslint": "^10.8.0",
|
||||||
"jest": "30.4.2",
|
"jest": "30.4.2",
|
||||||
"jest-environment-jsdom": "^30.4.1",
|
"jest-environment-jsdom": "^30.4.1",
|
||||||
"jest-junit": "^17.0.0",
|
"jest-junit": "^17.0.0",
|
||||||
@@ -66,8 +64,7 @@
|
|||||||
"jest-websocket-mock": "^2.5.0",
|
"jest-websocket-mock": "^2.5.0",
|
||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3"
|
||||||
"webpack": "^5.107.2"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.26.0"
|
"packageManager": "pnpm@11.15.1"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2177
-1981
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,10 @@ packages:
|
|||||||
- "."
|
- "."
|
||||||
minimumReleaseAge: 10080
|
minimumReleaseAge: 10080
|
||||||
trustPolicy: no-downgrade
|
trustPolicy: no-downgrade
|
||||||
|
trustPolicyExclude:
|
||||||
|
- "chokidar@4.0.3"
|
||||||
|
- "semver@6.3.1 || 5.7.2"
|
||||||
|
blockExoticSubdeps: true
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
"@parcel/watcher": true
|
"@parcel/watcher": true
|
||||||
canvas: true
|
canvas: true
|
||||||
|
|||||||
@@ -14,43 +14,48 @@
|
|||||||
<a ngbNavLink>{{category}}</a>
|
<a ngbNavLink>{{category}}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
|
@for (section of getCategorySections(category); track section) {
|
||||||
@for (option of getCategoryOptions(category); track option.key) {
|
@if (section) {
|
||||||
<div class="col">
|
<h5 class="mt-4 mb-3">{{section}}</h5>
|
||||||
<div class="card bg-light">
|
}
|
||||||
<div class="card-body">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
|
||||||
<div class="card-title d-flex align-items-center">
|
@for (option of getCategoryOptions(category, section); track option.key) {
|
||||||
<h6 class="mb-0">
|
<div class="col">
|
||||||
{{option.title}}
|
<div class="card bg-light">
|
||||||
</h6>
|
<div class="card-body">
|
||||||
<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">
|
<div class="card-title d-flex align-items-center">
|
||||||
<i-bs name="info-circle"></i-bs>
|
<h6 class="mb-0">
|
||||||
</a>
|
{{option.title}}
|
||||||
@if (isSet(option.key)) {
|
</h6>
|
||||||
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
|
<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">
|
||||||
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
<i-bs name="info-circle"></i-bs>
|
||||||
</button>
|
</a>
|
||||||
|
@if (isSet(option.key)) {
|
||||||
|
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
|
||||||
|
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</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> }
|
||||||
|
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
|
||||||
|
@case (ConfigOptionType.Password) { <pngx-input-password [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-password> }
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (option.note) {
|
||||||
|
<div class="form-text fst-italic">{{option.note}}</div>
|
||||||
}
|
}
|
||||||
</div>
|
</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> }
|
|
||||||
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
|
|
||||||
@case (ConfigOptionType.Password) { <pngx-input-password [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-password> }
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@if (option.note) {
|
|
||||||
<div class="form-text fst-italic">{{option.note}}</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { OutputTypeConfig } from 'src/app/data/paperless-config'
|
import {
|
||||||
|
ConfigCategory,
|
||||||
|
ConfigSection,
|
||||||
|
OutputTypeConfig,
|
||||||
|
} from 'src/app/data/paperless-config'
|
||||||
import { ConfigService } from 'src/app/services/config.service'
|
import { ConfigService } from 'src/app/services/config.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
@@ -158,4 +162,24 @@ describe('ConfigComponent', () => {
|
|||||||
component.resetOption('barcodes_enabled')
|
component.resetOption('barcodes_enabled')
|
||||||
expect(component.configForm.get('barcodes_enabled').value).toBeNull()
|
expect(component.configForm.get('barcodes_enabled').value).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should group options into sections within a category, or not', () => {
|
||||||
|
const sections = component.getCategorySections(ConfigCategory.OCR)
|
||||||
|
expect(sections).toEqual([null, ConfigSection.RemoteOCR])
|
||||||
|
expect(
|
||||||
|
component
|
||||||
|
.getCategoryOptions(ConfigCategory.OCR)
|
||||||
|
.map((option) => option.key)
|
||||||
|
).toContain('output_type')
|
||||||
|
expect(
|
||||||
|
component
|
||||||
|
.getCategoryOptions(ConfigCategory.OCR, ConfigSection.RemoteOCR)
|
||||||
|
.map((option) => option.key)
|
||||||
|
).toEqual([
|
||||||
|
'remote_ocr_engine',
|
||||||
|
'remote_ocr_api_key',
|
||||||
|
'remote_ocr_endpoint',
|
||||||
|
'remote_ocr_mode',
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -74,8 +74,20 @@ export class ConfigComponent
|
|||||||
return Object.values(ConfigCategory)
|
return Object.values(ConfigCategory)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategoryOptions(category: string): ConfigOption[] {
|
getCategorySections(category: string): string[] {
|
||||||
return PaperlessConfigOptions.filter((o) => o.category === category)
|
return [
|
||||||
|
...new Set(
|
||||||
|
PaperlessConfigOptions.filter((o) => o.category === category).map(
|
||||||
|
(o) => o.section ?? null // null means no section
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategoryOptions(category: string, section: string = null): ConfigOption[] {
|
||||||
|
return PaperlessConfigOptions.filter(
|
||||||
|
(o) => o.category === category && (o.section ?? null) === section
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
initialConfig: PaperlessConfig
|
initialConfig: PaperlessConfig
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
|
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-tabs">
|
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [activeId]="activeNavID()" (activeIdChange)="activeNavID.set($event)" class="nav-tabs">
|
||||||
<li [ngbNavItem]="SettingsNavIDs.General">
|
<li [ngbNavItem]="SettingsNavIDs.General">
|
||||||
<a ngbNavLink i18n>General</a>
|
<a ngbNavLink i18n>General</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<p i18n>
|
<p i18n>
|
||||||
Settings apply to this user account for objects (Tags, Mail Rules, etc. but not documents) created via the web UI.
|
Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI. These settings do not apply to documents.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ describe('SettingsComponent', () => {
|
|||||||
activatedRoute.snapshot.fragment = '#notifications'
|
activatedRoute.snapshot.fragment = '#notifications'
|
||||||
const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor')
|
const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor')
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
expect(component.activeNavID).toEqual(4) // Notifications
|
expect(component.activeNavID()).toEqual(4) // Notifications
|
||||||
component.ngAfterViewInit()
|
component.ngAfterViewInit()
|
||||||
expect(scrollSpy).toHaveBeenCalledWith('#notifications')
|
expect(scrollSpy).toHaveBeenCalledWith('#notifications')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class SettingsComponent
|
|||||||
private systemStatusService = inject(SystemStatusService)
|
private systemStatusService = inject(SystemStatusService)
|
||||||
private savedViewsService = inject(SavedViewService)
|
private savedViewsService = inject(SavedViewService)
|
||||||
|
|
||||||
activeNavID: number
|
readonly activeNavID = signal<number>(undefined)
|
||||||
|
|
||||||
settingsForm = new FormGroup({
|
settingsForm = new FormGroup({
|
||||||
bulkEditConfirmationDialogs: new FormControl(null),
|
bulkEditConfirmationDialogs: new FormControl(null),
|
||||||
@@ -283,7 +283,7 @@ export class SettingsComponent
|
|||||||
(navID) => navID.toLowerCase() == section
|
(navID) => navID.toLowerCase() == section
|
||||||
)
|
)
|
||||||
if (navIDKey) {
|
if (navIDKey) {
|
||||||
this.activeNavID = SettingsNavIDs[navIDKey]
|
this.activeNavID.set(SettingsNavIDs[navIDKey])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -386,7 +386,7 @@ export class SettingsComponent
|
|||||||
.navigate(['settings', foundNavIDkey.toLowerCase()])
|
.navigate(['settings', foundNavIDkey.toLowerCase()])
|
||||||
.then((navigated) => {
|
.then((navigated) => {
|
||||||
if (!navigated && this.isDirty) {
|
if (!navigated && this.isDirty) {
|
||||||
this.activeNavID = navChangeEvent.activeId
|
this.activeNavID.set(navChangeEvent.activeId)
|
||||||
} else if (navigated && this.isDirty) {
|
} else if (navigated && this.isDirty) {
|
||||||
this.initialize()
|
this.initialize()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
@if (!tasksService.completedFileTasks && tasksService.loading) {
|
@if (loading() && pagedTasks().length === 0) {
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||||
@for (view of savedViewService.sidebarViews; track view.id) {
|
@for (view of savedViewService.sidebarViews; track view.id) {
|
||||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews()"
|
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews() || !canSaveSettings"
|
||||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||||
(cdkDragEnded)="onDragEnd($event)">
|
(cdkDragEnded)="onDragEnd($event)">
|
||||||
<a class="nav-link" routerLink="view/{{view.id}}"
|
<a class="nav-link" routerLink="view/{{view.id}}"
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<span class="badge bg-info text-dark position-absolute top-0 end-0 d-none d-md-block">{{ savedViewService.getDocumentCount(view) }}</span>
|
<span class="badge bg-info text-dark position-absolute top-0 end-0 d-none d-md-block">{{ savedViewService.getDocumentCount(view) }}</span>
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarSavedViews()) {
|
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
|
||||||
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
||||||
<i-bs name="grip-vertical"></i-bs>
|
<i-bs name="grip-vertical"></i-bs>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||||
<i-bs class="me-2" name="stack"></i-bs><span><ng-container i18n>Attributes</ng-container></span>
|
<i-bs class="me-2" name="stack"></i-bs><span><ng-container i18n>Attributes</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
@if (!slimSidebarEnabled) {
|
@if (!slimSidebarEnabled && canSaveSettings) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
|
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
|
||||||
|
|||||||
@@ -280,10 +280,52 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 376px) and (max-width: 768px) {
|
@media screen and (max-width: 575.98px) {
|
||||||
|
.navbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"toggler brand actions"
|
||||||
|
"search search search";
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-toggler {
|
.navbar-toggler {
|
||||||
// compensate for 2 buttons on the right
|
grid-area: toggler;
|
||||||
margin-right: 45px;
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
grid-area: brand;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
justify-self: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> div,
|
||||||
|
.custom-title {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title,
|
||||||
|
.byline {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
grid-area: search;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar > ul {
|
||||||
|
grid-area: actions;
|
||||||
|
justify-self: end;
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,13 +335,6 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 345px) {
|
|
||||||
.custom-title {
|
|
||||||
max-width: 110px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host ::ng-deep .dropdown.show .dropdown-toggle,
|
:host ::ng-deep .dropdown.show .dropdown-toggle,
|
||||||
:host ::ng-deep .dropdown-toggle:hover {
|
:host ::ng-deep .dropdown-toggle:hover {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
<div ngbDropdownMenu class="dropdown-menu-end shadow p-3" aria-labelledby="chatDropdown">
|
<div ngbDropdownMenu class="dropdown-menu-end shadow p-3" aria-labelledby="chatDropdown">
|
||||||
<div class="chat-container bg-light p-2">
|
<div class="chat-container bg-light p-2">
|
||||||
<div class="chat-messages font-monospace small">
|
<div class="chat-messages font-monospace small">
|
||||||
@for (message of messages; track message) {
|
@for (message of messages(); track message) {
|
||||||
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
|
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
|
||||||
<div class="p-2 m-2" [class.bg-body]="message.role === 'user'">
|
<div class="p-2 m-2" [class.bg-body]="message.role === 'user'">
|
||||||
<span>
|
<span class="text-break">
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
|
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
|
||||||
</span>
|
</span>
|
||||||
@if (message.role === 'assistant' && message.references?.length) {
|
@if (message.role === 'assistant' && message.references?.length) {
|
||||||
<div class="chat-references list-group mt-3">
|
<div class="chat-references list-group mt-3">
|
||||||
@for (reference of message.references; track reference.id) {
|
@for (reference of message.references; track reference.id) {
|
||||||
<a class="list-group-item list-group-item-action text-primary" [routerLink]="['/documents', reference.id]">
|
<a class="list-group-item list-group-item-action d-flex text-primary text-break" [routerLink]="['/documents', reference.id]">
|
||||||
<i-bs width="0.9em" height="0.9em" name="file-text" class="me-1"></i-bs><span>{{ reference.title }}</span>
|
<i-bs width="0.9em" height="0.9em" name="file-text" class="me-1"></i-bs><span>{{ reference.title }}</span>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
@@ -36,11 +36,12 @@
|
|||||||
id="chatInput"
|
id="chatInput"
|
||||||
class="form-control form-control-sm" name="chatInput" type="text"
|
class="form-control form-control-sm" name="chatInput" type="text"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
[disabled]="loading"
|
[disabled]="loading()"
|
||||||
[(ngModel)]="input"
|
[ngModel]="input()"
|
||||||
|
(ngModelChange)="input.set($event)"
|
||||||
(keydown)="searchInputKeyDown($event)"
|
(keydown)="searchInputKeyDown($event)"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-sm btn-secondary" type="button" (click)="sendMessage()" [disabled]="loading">Send</button>
|
<button class="btn btn-sm btn-secondary" type="button" (click)="sendMessage()" [disabled]="loading()">Send</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
.chat-messages {
|
.chat-messages {
|
||||||
max-height: 350px;
|
max-height: 350px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-references {
|
.chat-references {
|
||||||
|
|||||||
@@ -56,44 +56,53 @@ describe('ChatComponent', () => {
|
|||||||
it('should update documentId on initialization', () => {
|
it('should update documentId on initialization', () => {
|
||||||
jest.spyOn(router, 'url', 'get').mockReturnValue('/documents/123')
|
jest.spyOn(router, 'url', 'get').mockReturnValue('/documents/123')
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
expect(component.documentId).toBe(123)
|
expect(component.documentId()).toBe(123)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update documentId on navigation', () => {
|
it('should update documentId on navigation', () => {
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
routerEvents$.next(new NavigationEnd(1, '/documents/456', '/documents/456'))
|
routerEvents$.next(new NavigationEnd(1, '/documents/456', '/documents/456'))
|
||||||
expect(component.documentId).toBe(456)
|
expect(component.documentId()).toBe(456)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return correct placeholder based on documentId', () => {
|
it('should return correct placeholder based on documentId', () => {
|
||||||
component.documentId = 123
|
component.documentId.set(123)
|
||||||
expect(component.placeholder).toBe('Ask a question about this document...')
|
expect(component.placeholder).toBe('Ask a question about this document...')
|
||||||
component.documentId = undefined
|
component.documentId.set(undefined)
|
||||||
expect(component.placeholder).toBe('Ask a question about a document...')
|
expect(component.placeholder).toBe('Ask a question about a document...')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should send a message and handle streaming response', () => {
|
it('should send a message and render the streaming response', async () => {
|
||||||
component.input = 'Hello'
|
component.input.set('Hello')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
|
|
||||||
expect(component.messages).toHaveLength(2)
|
expect(component.messages()).toHaveLength(2)
|
||||||
expect(component.messages[0].content).toBe('Hello')
|
expect(component.messages()[0].content).toBe('Hello')
|
||||||
expect(component.loading).toBe(true)
|
expect(component.loading()).toBe(true)
|
||||||
|
|
||||||
mockStream$.next('Hi')
|
mockStream$.next('Hi')
|
||||||
expect(component.messages[1].content).toBe('H')
|
expect(component.messages()[1].content).toBe('H')
|
||||||
mockStream$.next('Hi there')
|
mockStream$.next('Hi there')
|
||||||
// advance time to process the typewriter effect
|
// advance time to process the typewriter effect
|
||||||
jest.advanceTimersByTime(1000)
|
await jest.runAllTimersAsync()
|
||||||
expect(component.messages[1].content).toBe('Hi there')
|
await fixture.whenStable()
|
||||||
|
expect(component.messages()[1].content).toBe('Hi there')
|
||||||
|
expect(
|
||||||
|
fixture.nativeElement.querySelector('.chat-messages').textContent
|
||||||
|
).toContain('Hi there')
|
||||||
|
|
||||||
mockStream$.complete()
|
mockStream$.complete()
|
||||||
expect(component.loading).toBe(false)
|
await jest.runAllTimersAsync()
|
||||||
expect(component.messages[1].isStreaming).toBe(false)
|
await fixture.whenStable()
|
||||||
|
expect(component.loading()).toBe(false)
|
||||||
|
expect(component.messages()[1].isStreaming).toBe(false)
|
||||||
|
expect(fixture.nativeElement.querySelector('#chatInput').disabled).toBe(
|
||||||
|
false
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse references from the metadata trailer without showing it', () => {
|
it('should parse references from the metadata trailer without showing it', () => {
|
||||||
component.input = 'Hello'
|
component.input.set('Hello')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
|
|
||||||
mockStream$.next(
|
mockStream$.next(
|
||||||
@@ -101,14 +110,14 @@ describe('ChatComponent', () => {
|
|||||||
)
|
)
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
expect(component.messages[1].content).toBe('Hi there')
|
expect(component.messages()[1].content).toBe('Hi there')
|
||||||
expect(component.messages[1].references).toEqual([
|
expect(component.messages()[1].references).toEqual([
|
||||||
{ id: 42, title: 'Bread Recipe' },
|
{ id: 42, title: 'Bread Recipe' },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render document reference links under assistant messages', () => {
|
it('should render document reference links under assistant messages', () => {
|
||||||
component.input = 'Hello'
|
component.input.set('Hello')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
|
|
||||||
mockStream$.next(
|
mockStream$.next(
|
||||||
@@ -123,12 +132,12 @@ describe('ChatComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should remove delimiter fragments that were already streamed', () => {
|
it('should remove delimiter fragments that were already streamed', () => {
|
||||||
component.input = 'Hello'
|
component.input.set('Hello')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
|
|
||||||
mockStream$.next(`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`)
|
mockStream$.next(`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`)
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
expect(component.messages[1].content).toBe(
|
expect(component.messages()[1].content).toBe(
|
||||||
`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`
|
`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,21 +146,21 @@ describe('ChatComponent', () => {
|
|||||||
)
|
)
|
||||||
jest.advanceTimersByTime(1000)
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
expect(component.messages[1].content).toBe('Hi there')
|
expect(component.messages()[1].content).toBe('Hi there')
|
||||||
expect(component.messages[1].references).toEqual([
|
expect(component.messages()[1].references).toEqual([
|
||||||
{ id: 42, title: 'Bread Recipe' },
|
{ id: 42, title: 'Bread Recipe' },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle errors during streaming', () => {
|
it('should handle errors during streaming', () => {
|
||||||
component.input = 'Hello'
|
component.input.set('Hello')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
|
|
||||||
mockStream$.error('Error')
|
mockStream$.error('Error')
|
||||||
expect(component.messages[1].content).toContain(
|
expect(component.messages()[1].content).toContain(
|
||||||
'⚠️ Error receiving response.'
|
'⚠️ Error receiving response.'
|
||||||
)
|
)
|
||||||
expect(component.loading).toBe(false)
|
expect(component.loading()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should enqueue typewriter chunks correctly', () => {
|
it('should enqueue typewriter chunks correctly', () => {
|
||||||
@@ -166,7 +175,7 @@ describe('ChatComponent', () => {
|
|||||||
ChatComponent.prototype as any,
|
ChatComponent.prototype as any,
|
||||||
'scrollToBottom'
|
'scrollToBottom'
|
||||||
)
|
)
|
||||||
component.input = 'Test'
|
component.input.set('Test')
|
||||||
component.sendMessage()
|
component.sendMessage()
|
||||||
expect(scrollSpy).toHaveBeenCalled()
|
expect(scrollSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
inject,
|
||||||
|
OnInit,
|
||||||
|
signal,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NavigationEnd, Router, RouterModule } from '@angular/router'
|
import { NavigationEnd, Router, RouterModule } from '@angular/router'
|
||||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@@ -23,10 +30,10 @@ import {
|
|||||||
styleUrl: './chat.component.scss',
|
styleUrl: './chat.component.scss',
|
||||||
})
|
})
|
||||||
export class ChatComponent implements OnInit {
|
export class ChatComponent implements OnInit {
|
||||||
public messages: ChatMessage[] = []
|
readonly messages = signal<ChatMessage[]>([])
|
||||||
public loading = false
|
readonly loading = signal(false)
|
||||||
public input: string = ''
|
readonly input = signal('')
|
||||||
public documentId!: number
|
readonly documentId = signal<number>(undefined)
|
||||||
|
|
||||||
private chatService: ChatService = inject(ChatService)
|
private chatService: ChatService = inject(ChatService)
|
||||||
private router: Router = inject(Router)
|
private router: Router = inject(Router)
|
||||||
@@ -38,7 +45,7 @@ export class ChatComponent implements OnInit {
|
|||||||
private typewriterActive = false
|
private typewriterActive = false
|
||||||
|
|
||||||
public get placeholder(): string {
|
public get placeholder(): string {
|
||||||
return this.documentId
|
return this.documentId()
|
||||||
? $localize`Ask a question about this document...`
|
? $localize`Ask a question about this document...`
|
||||||
: $localize`Ask a question about a document...`
|
: $localize`Ask a question about a document...`
|
||||||
}
|
}
|
||||||
@@ -57,14 +64,14 @@ export class ChatComponent implements OnInit {
|
|||||||
|
|
||||||
private updateDocumentId(url: string): void {
|
private updateDocumentId(url: string): void {
|
||||||
const docIdRe = url.match(/^\/documents\/(\d+)/)
|
const docIdRe = url.match(/^\/documents\/(\d+)/)
|
||||||
this.documentId = docIdRe ? +docIdRe[1] : undefined
|
this.documentId.set(docIdRe ? +docIdRe[1] : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(): void {
|
sendMessage(): void {
|
||||||
if (!this.input.trim()) return
|
if (!this.input().trim()) return
|
||||||
|
|
||||||
const userMessage: ChatMessage = { role: 'user', content: this.input }
|
const userMessage: ChatMessage = { role: 'user', content: this.input() }
|
||||||
this.messages.push(userMessage)
|
this.messages.update((messages) => [...messages, userMessage])
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
|
|
||||||
const assistantMessage: ChatMessage = {
|
const assistantMessage: ChatMessage = {
|
||||||
@@ -72,12 +79,12 @@ export class ChatComponent implements OnInit {
|
|||||||
content: '',
|
content: '',
|
||||||
isStreaming: true,
|
isStreaming: true,
|
||||||
}
|
}
|
||||||
this.messages.push(assistantMessage)
|
this.messages.update((messages) => [...messages, assistantMessage])
|
||||||
this.loading = true
|
this.loading.set(true)
|
||||||
|
|
||||||
let lastVisibleContent = ''
|
let lastVisibleContent = ''
|
||||||
|
|
||||||
this.chatService.streamChat(this.documentId, this.input).subscribe({
|
this.chatService.streamChat(this.documentId(), this.input()).subscribe({
|
||||||
next: (chunk) => {
|
next: (chunk) => {
|
||||||
const nextResponse = parseChatResponse(chunk)
|
const nextResponse = parseChatResponse(chunk)
|
||||||
|
|
||||||
@@ -93,26 +100,30 @@ export class ChatComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assistantMessage.references = nextResponse.references
|
assistantMessage.references = nextResponse.references
|
||||||
|
this.notifyMessagesChanged()
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
assistantMessage.content += '\n\n⚠️ Error receiving response.'
|
assistantMessage.content += '\n\n⚠️ Error receiving response.'
|
||||||
assistantMessage.isStreaming = false
|
assistantMessage.isStreaming = false
|
||||||
this.loading = false
|
this.notifyMessagesChanged()
|
||||||
|
this.loading.set(false)
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
assistantMessage.isStreaming = false
|
assistantMessage.isStreaming = false
|
||||||
this.loading = false
|
this.notifyMessagesChanged()
|
||||||
|
this.loading.set(false)
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
this.input = ''
|
this.input.set('')
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetTypewriter(message: ChatMessage, content: string): void {
|
private resetTypewriter(message: ChatMessage, content: string): void {
|
||||||
this.typewriterBuffer = []
|
this.typewriterBuffer = []
|
||||||
this.typewriterActive = false
|
this.typewriterActive = false
|
||||||
message.content = content
|
message.content = content
|
||||||
|
this.notifyMessagesChanged()
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +146,16 @@ export class ChatComponent implements OnInit {
|
|||||||
|
|
||||||
const nextChar = this.typewriterBuffer.shift()
|
const nextChar = this.typewriterBuffer.shift()
|
||||||
message.content += nextChar
|
message.content += nextChar
|
||||||
|
this.notifyMessagesChanged()
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
|
|
||||||
setTimeout(() => this.playTypewriter(message), 10) // 10ms per character
|
setTimeout(() => this.playTypewriter(message), 10) // 10ms per character
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notifyMessagesChanged(): void {
|
||||||
|
this.messages.update((messages) => [...messages])
|
||||||
|
}
|
||||||
|
|
||||||
private scrollToBottom(): void {
|
private scrollToBottom(): void {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' })
|
this.scrollAnchor?.nativeElement?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
|
|
||||||
<ng-template #popoverContent>
|
<ng-template #popoverContent>
|
||||||
<div>
|
<div>
|
||||||
{{confirmMessage}} <button class="btn btn-link btn-sm text-danger p-0 m-0 lh-1" type="button" (click)="onConfirm($event)">Yes</button>
|
{{confirmMessage}} <button class="btn btn-link btn-sm text-danger p-0 m-0 lh-1" type="button" (click)="onConfirm($event)" i18n>Yes</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
@if (messageBold) {
|
||||||
|
<p class="text-break"><b>{{messageBold}}</b></p>
|
||||||
|
}
|
||||||
|
@if (message) {
|
||||||
|
<p class="mb-0 text-break" [innerHTML]="message"></p>
|
||||||
|
}
|
||||||
|
@if (showRemoteOcr) {
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="reprocessRemoteOcr" [(ngModel)]="remoteOcr" />
|
||||||
|
<label class="form-check-label" for="reprocessRemoteOcr" i18n>Use remote OCR</label>
|
||||||
|
<div class="form-text" i18n>Sends the document to the configured remote OCR service, which may incur costs.</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||||
|
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||||
|
{{btnCaption}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { RemoteOCRModeConfig } from 'src/app/data/paperless-config'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ReprocessConfirmDialogComponent } from './reprocess-confirm-dialog.component'
|
||||||
|
|
||||||
|
describe('ReprocessConfirmDialogComponent', () => {
|
||||||
|
let component: ReprocessConfirmDialogComponent
|
||||||
|
let fixture: ComponentFixture<ReprocessConfirmDialogComponent>
|
||||||
|
let settingsService: SettingsService
|
||||||
|
|
||||||
|
const createComponent = (configured: boolean, mode: string) => {
|
||||||
|
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED, configured)
|
||||||
|
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_MODE, mode)
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ReprocessConfirmDialogComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
NgbActiveModal,
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
|
imports: [ReprocessConfirmDialogComponent],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
settingsService = TestBed.inject(SettingsService)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not request remote OCR by default', () => {
|
||||||
|
createComponent(true, RemoteOCRModeConfig.WORKFLOW_ONLY)
|
||||||
|
|
||||||
|
expect(component.remoteOcr).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not offer remote OCR when no engine is configured', () => {
|
||||||
|
createComponent(false, RemoteOCRModeConfig.WORKFLOW_ONLY)
|
||||||
|
|
||||||
|
expect(component.showRemoteOcr).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
fixture.nativeElement.querySelector('#reprocessRemoteOcr')
|
||||||
|
).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not offer remote OCR when it already handles every document', () => {
|
||||||
|
createComponent(true, RemoteOCRModeConfig.ALWAYS)
|
||||||
|
|
||||||
|
expect(component.showRemoteOcr).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
fixture.nativeElement.querySelector('#reprocessRemoteOcr')
|
||||||
|
).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should offer remote OCR when configured and selective', () => {
|
||||||
|
createComponent(true, RemoteOCRModeConfig.WORKFLOW_ONLY)
|
||||||
|
|
||||||
|
expect(component.showRemoteOcr).toBeTruthy()
|
||||||
|
const checkbox = fixture.nativeElement.querySelector('#reprocessRemoteOcr')
|
||||||
|
expect(checkbox).not.toBeNull()
|
||||||
|
|
||||||
|
checkbox.click()
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.remoteOcr).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { Component, inject } from '@angular/core'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-reprocess-confirm-dialog',
|
||||||
|
templateUrl: './reprocess-confirm-dialog.component.html',
|
||||||
|
imports: [FormsModule],
|
||||||
|
})
|
||||||
|
export class ReprocessConfirmDialogComponent extends ConfirmDialogComponent {
|
||||||
|
private settings = inject(SettingsService)
|
||||||
|
|
||||||
|
remoteOcr: boolean = false
|
||||||
|
|
||||||
|
public get showRemoteOcr(): boolean {
|
||||||
|
// Hidden when it is not configured, or when it already handles every document anyway.
|
||||||
|
return this.settings.remoteOCRIsSelectable
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-1
@@ -1,5 +1,12 @@
|
|||||||
import { CurrencyPipe, getLocaleCurrencyCode, SlicePipe } from '@angular/common'
|
import { CurrencyPipe, getLocaleCurrencyCode, SlicePipe } from '@angular/common'
|
||||||
import { Component, inject, Input, LOCALE_ID, OnInit } from '@angular/core'
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
Input,
|
||||||
|
LOCALE_ID,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core'
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { takeUntil } from 'rxjs'
|
import { takeUntil } from 'rxjs'
|
||||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
@@ -22,6 +29,7 @@ export class CustomFieldDisplayComponent
|
|||||||
{
|
{
|
||||||
private customFieldService = inject(CustomFieldsService)
|
private customFieldService = inject(CustomFieldsService)
|
||||||
private documentService = inject(DocumentService)
|
private documentService = inject(DocumentService)
|
||||||
|
private changeDetector = inject(ChangeDetectorRef)
|
||||||
|
|
||||||
CustomFieldDataType = CustomFieldDataType
|
CustomFieldDataType = CustomFieldDataType
|
||||||
|
|
||||||
@@ -74,6 +82,7 @@ export class CustomFieldDisplayComponent
|
|||||||
this.customFieldService.listAll().subscribe((r) => {
|
this.customFieldService.listAll().subscribe((r) => {
|
||||||
this.customFields = r.results
|
this.customFields = r.results
|
||||||
this.init()
|
this.init()
|
||||||
|
this.changeDetector.markForCheck()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +120,7 @@ export class CustomFieldDisplayComponent
|
|||||||
this.docLinkDocuments = this.value
|
this.docLinkDocuments = this.value
|
||||||
.map((id) => result.results.find((d) => d.id === id))
|
.map((id) => result.results.find((d) => d.id === id))
|
||||||
.filter((d) => d)
|
.filter((d) => d)
|
||||||
|
this.changeDetector.markForCheck()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -95,8 +95,8 @@ describe('CustomFieldsDropdownComponent', () => {
|
|||||||
it('should support update unused fields', () => {
|
it('should support update unused fields', () => {
|
||||||
component.existingFields = [{ field: fields[0].id } as any]
|
component.existingFields = [{ field: fields[0].id } as any]
|
||||||
component['updateUnusedFields']()
|
component['updateUnusedFields']()
|
||||||
expect(component['unusedFields'].length).toEqual(1)
|
expect(component['unusedFields']().length).toEqual(1)
|
||||||
expect(component['unusedFields'][0].name).toEqual('Field 2')
|
expect(component['unusedFields']()[0].name).toEqual('Field 2')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support getting data type label', () => {
|
it('should support getting data type label', () => {
|
||||||
|
|||||||
+7
-4
@@ -8,6 +8,7 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
inject,
|
inject,
|
||||||
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@@ -65,11 +66,11 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
|
|||||||
@ViewChildren('button') buttons: QueryList<ElementRef>
|
@ViewChildren('button') buttons: QueryList<ElementRef>
|
||||||
|
|
||||||
private customFields: CustomField[] = []
|
private customFields: CustomField[] = []
|
||||||
private unusedFields: CustomField[] = []
|
private readonly unusedFields = signal<CustomField[]>([])
|
||||||
private keyboardIndex: number
|
private keyboardIndex: number
|
||||||
|
|
||||||
public get filteredFields(): CustomField[] {
|
public get filteredFields(): CustomField[] {
|
||||||
return this.unusedFields.filter(
|
return this.unusedFields().filter(
|
||||||
(f) => !this.filterText || matchesSearchText(f.name, this.filterText)
|
(f) => !this.filterText || matchesSearchText(f.name, this.filterText)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -99,8 +100,10 @@ export class CustomFieldsDropdownComponent extends LoadingComponentWithPermissio
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateUnusedFields() {
|
private updateUnusedFields() {
|
||||||
this.unusedFields = this.customFields.filter(
|
this.unusedFields.set(
|
||||||
(f) => !this.existingFields?.find((e) => e.field === f.id)
|
this.customFields.filter(
|
||||||
|
(f) => !this.existingFields?.find((e) => e.field === f.id)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -68,6 +68,11 @@
|
|||||||
></ng-select>
|
></ng-select>
|
||||||
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.DocumentLink) {
|
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.DocumentLink) {
|
||||||
<pngx-input-document-link [(ngModel)]="atom.value" class="w-25 form-select doc-link-select p-0" placeholder="Search docs..." i18n-placeholder [minimal]="true"></pngx-input-document-link>
|
<pngx-input-document-link [(ngModel)]="atom.value" class="w-25 form-select doc-link-select p-0" placeholder="Search docs..." i18n-placeholder [minimal]="true"></pngx-input-document-link>
|
||||||
|
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Monetary) {
|
||||||
|
<input class="w-25 form-control rounded-end" type="text" inputmode="decimal"
|
||||||
|
[ngModel]="atom.value"
|
||||||
|
(ngModelChange)="setMonetaryValue(atom, $event)"
|
||||||
|
[disabled]="disabled">
|
||||||
} @else {
|
} @else {
|
||||||
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="atom.value" [disabled]="disabled">
|
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="atom.value" [disabled]="disabled">
|
||||||
}
|
}
|
||||||
@@ -77,7 +82,7 @@
|
|||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<ng-select #fieldSelects
|
<ng-select #fieldSelects
|
||||||
class="paperless-input-select"
|
class="paperless-input-select"
|
||||||
[items]="customFields"
|
[items]="customFields()"
|
||||||
[(ngModel)]="atom.field"
|
[(ngModel)]="atom.field"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
bindLabel="name"
|
bindLabel="name"
|
||||||
|
|||||||
+27
-3
@@ -1,5 +1,6 @@
|
|||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
|
import { LOCALE_ID } from '@angular/core'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@@ -41,6 +42,12 @@ const customFields = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Test Monetary Field',
|
||||||
|
data_type: CustomFieldDataType.Monetary,
|
||||||
|
extra_data: { default_currency: 'EUR' },
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
describe('CustomFieldsQueryDropdownComponent', () => {
|
describe('CustomFieldsQueryDropdownComponent', () => {
|
||||||
@@ -61,6 +68,7 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
{ provide: LOCALE_ID, useValue: 'de' },
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@@ -79,7 +87,7 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should initialize custom fields on creation', () => {
|
it('should initialize custom fields on creation', () => {
|
||||||
expect(component.customFields).toEqual(customFields)
|
expect(component.customFields()).toEqual(customFields)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should add an expression when opened if queries are empty', () => {
|
it('should add an expression when opened if queries are empty', () => {
|
||||||
@@ -101,7 +109,7 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
data_type: CustomFieldDataType.String,
|
data_type: CustomFieldDataType.String,
|
||||||
extra_data: {},
|
extra_data: {},
|
||||||
}
|
}
|
||||||
component.customFields = [field]
|
component.customFields.set([field])
|
||||||
const operators = component.getOperatorsForField(1)
|
const operators = component.getOperatorsForField(1)
|
||||||
expect(operators.length).toEqual(
|
expect(operators.length).toEqual(
|
||||||
[
|
[
|
||||||
@@ -138,7 +146,7 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
component.customFields = [field]
|
component.customFields.set([field])
|
||||||
const options = component.getSelectOptionsForField(1)
|
const options = component.getSelectOptionsForField(1)
|
||||||
expect(options).toEqual([
|
expect(options).toEqual([
|
||||||
{ label: 'Option 1', id: 'abc-123' },
|
{ label: 'Option 1', id: 'abc-123' },
|
||||||
@@ -150,6 +158,22 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
expect(options2).toEqual([])
|
expect(options2).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should normalize localized monetary comparison values', () => {
|
||||||
|
const atom = new CustomFieldQueryAtom([3, 'exact', null])
|
||||||
|
|
||||||
|
component.setMonetaryValue(atom, '1.234,56')
|
||||||
|
|
||||||
|
expect(atom.value).toEqual('1234.56')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve API-formatted monetary comparison values', () => {
|
||||||
|
const atom = new CustomFieldQueryAtom([3, 'exact', null])
|
||||||
|
|
||||||
|
component.setMonetaryValue(atom, '1234.56')
|
||||||
|
|
||||||
|
expect(atom.value).toEqual('1234.56')
|
||||||
|
})
|
||||||
|
|
||||||
it('should remove an element from the selection model', () => {
|
it('should remove an element from the selection model', () => {
|
||||||
const expression = new CustomFieldQueryExpression()
|
const expression = new CustomFieldQueryExpression()
|
||||||
const atom = new CustomFieldQueryAtom()
|
const atom = new CustomFieldQueryAtom()
|
||||||
|
|||||||
+27
-6
@@ -1,11 +1,17 @@
|
|||||||
import { NgTemplateOutlet } from '@angular/common'
|
import {
|
||||||
|
getLocaleNumberSymbol,
|
||||||
|
NgTemplateOutlet,
|
||||||
|
NumberSymbol,
|
||||||
|
} from '@angular/common'
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
|
LOCALE_ID,
|
||||||
Output,
|
Output,
|
||||||
QueryList,
|
QueryList,
|
||||||
|
signal,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ViewChildren,
|
ViewChildren,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
@@ -211,6 +217,7 @@ export class CustomFieldQueriesModel {
|
|||||||
})
|
})
|
||||||
export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions {
|
export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPermissions {
|
||||||
protected customFieldsService = inject(CustomFieldsService)
|
protected customFieldsService = inject(CustomFieldsService)
|
||||||
|
private readonly locale = inject(LOCALE_ID)
|
||||||
|
|
||||||
public CustomFieldQueryComponentType = CustomFieldQueryElementType
|
public CustomFieldQueryComponentType = CustomFieldQueryElementType
|
||||||
public CustomFieldQueryOperator = CustomFieldQueryOperator
|
public CustomFieldQueryOperator = CustomFieldQueryOperator
|
||||||
@@ -278,7 +285,7 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
|||||||
@Output()
|
@Output()
|
||||||
selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
|
selectionModelChange = new EventEmitter<CustomFieldQueriesModel>()
|
||||||
|
|
||||||
customFields: CustomField[] = []
|
readonly customFields = signal<CustomField[]>([])
|
||||||
|
|
||||||
public readonly today: string = new Date().toLocaleDateString('en-CA')
|
public readonly today: string = new Date().toLocaleDateString('en-CA')
|
||||||
|
|
||||||
@@ -325,12 +332,12 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
|||||||
.listAll()
|
.listAll()
|
||||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((result) => {
|
.subscribe((result) => {
|
||||||
this.customFields = result.results
|
this.customFields.set(result.results)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCustomFieldByID(id: number): CustomField {
|
public getCustomFieldByID(id: number): CustomField {
|
||||||
return this.customFields.find((field) => field.id === id)
|
return this.customFields().find((field) => field.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
public addAtom(expression: CustomFieldQueryExpression) {
|
public addAtom(expression: CustomFieldQueryExpression) {
|
||||||
@@ -353,7 +360,7 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
|||||||
getOperatorsForField(
|
getOperatorsForField(
|
||||||
fieldID: number
|
fieldID: number
|
||||||
): Array<{ value: string; label: string }> {
|
): Array<{ value: string; label: string }> {
|
||||||
const field = this.customFields.find((field) => field.id === fieldID)
|
const field = this.customFields().find((field) => field.id === fieldID)
|
||||||
const groups: CustomFieldQueryOperatorGroups[] = field
|
const groups: CustomFieldQueryOperatorGroups[] = field
|
||||||
? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type]
|
? CUSTOM_FIELD_QUERY_OPERATOR_GROUPS_BY_TYPE[field.data_type]
|
||||||
: [CustomFieldQueryOperatorGroups.Basic]
|
: [CustomFieldQueryOperatorGroups.Basic]
|
||||||
@@ -369,10 +376,24 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
|||||||
getSelectOptionsForField(
|
getSelectOptionsForField(
|
||||||
fieldID: number
|
fieldID: number
|
||||||
): Array<{ label: string; id: string }> {
|
): Array<{ label: string; id: string }> {
|
||||||
const field = this.customFields.find((field) => field.id === fieldID)
|
const field = this.customFields().find((field) => field.id === fieldID)
|
||||||
if (field) {
|
if (field) {
|
||||||
return field.extra_data['select_options']
|
return field.extra_data['select_options']
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMonetaryValue(atom: CustomFieldQueryAtom, value: string) {
|
||||||
|
// Normalize the decimal symbol e.g. . vs , by locale
|
||||||
|
const decimalSymbol = getLocaleNumberSymbol(
|
||||||
|
this.locale,
|
||||||
|
NumberSymbol.Decimal
|
||||||
|
)
|
||||||
|
if (decimalSymbol !== '.' && value.includes(decimalSymbol)) {
|
||||||
|
const groupSymbol = getLocaleNumberSymbol(this.locale, NumberSymbol.Group)
|
||||||
|
value = value.split(groupSymbol).join('').split(decimalSymbol).join('.')
|
||||||
|
}
|
||||||
|
|
||||||
|
atom.value = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
|
|||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{ getTitle() }}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{ getTitle() }}</h4>
|
||||||
|
<span class="error">{{ error?.name }}</span>
|
||||||
|
<button [disabled]="networkActive" (click)="save()">Save</button>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
imports: [FormsModule, ReactiveFormsModule],
|
imports: [FormsModule, ReactiveFormsModule],
|
||||||
@@ -276,4 +278,22 @@ describe('EditDialogComponent', () => {
|
|||||||
expect(failedSpy).toHaveBeenCalled()
|
expect(failedSpy).toHaveBeenCalled()
|
||||||
expect(component.error).toEqual('error')
|
expect(component.error).toEqual('error')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should update the view after a failed save', async () => {
|
||||||
|
const button: HTMLButtonElement =
|
||||||
|
fixture.nativeElement.querySelector('button')
|
||||||
|
button.click()
|
||||||
|
await fixture.whenStable()
|
||||||
|
expect(button.disabled).toBe(true)
|
||||||
|
|
||||||
|
httpTestingController
|
||||||
|
.expectOne(`${environment.apiBaseUrl}tags/`)
|
||||||
|
.flush({ name: ['Name is required.'] }, { status: 400, statusText: '' })
|
||||||
|
await fixture.whenStable()
|
||||||
|
|
||||||
|
expect(button.disabled).toBe(false)
|
||||||
|
expect(fixture.nativeElement.querySelector('.error').textContent).toContain(
|
||||||
|
'Name is required.'
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
Directive,
|
Directive,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
@@ -45,6 +46,7 @@ export abstract class EditDialogComponent<
|
|||||||
protected userService = inject(UserService)
|
protected userService = inject(UserService)
|
||||||
protected settingsService = inject(SettingsService)
|
protected settingsService = inject(SettingsService)
|
||||||
protected permissionsService = inject(PermissionsService)
|
protected permissionsService = inject(PermissionsService)
|
||||||
|
protected changeDetector = inject(ChangeDetectorRef)
|
||||||
|
|
||||||
dialogMode = model(EditDialogMode.CREATE)
|
dialogMode = model(EditDialogMode.CREATE)
|
||||||
|
|
||||||
@@ -108,10 +110,12 @@ export abstract class EditDialogComponent<
|
|||||||
// wait to enable close button so it doesn't 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(() => {
|
setTimeout(() => {
|
||||||
this.closeEnabled = true
|
this.closeEnabled = true
|
||||||
|
this.changeDetector.markForCheck()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.userService.listAll().subscribe((r) => {
|
this.userService.listAll().subscribe((r) => {
|
||||||
this.users = r.results
|
this.users = r.results
|
||||||
|
this.changeDetector.markForCheck()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +195,7 @@ export abstract class EditDialogComponent<
|
|||||||
this.error = error.error
|
this.error = error.error
|
||||||
this.networkActive = false
|
this.networkActive = false
|
||||||
this.failed.next(error)
|
this.failed.next(error)
|
||||||
|
this.changeDetector.markForCheck()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -22,11 +22,11 @@
|
|||||||
<ng-template>
|
<ng-template>
|
||||||
<div class="card mb-2">
|
<div class="card mb-2">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
@if (testLoading) {
|
@if (testLoading()) {
|
||||||
<ng-container [ngTemplateOutlet]="loadingTemplate"></ng-container>
|
<ng-container [ngTemplateOutlet]="loadingTemplate"></ng-container>
|
||||||
} @else if (testResult) {
|
} @else if (testResult()) {
|
||||||
<code>{{testResult}}</code>
|
<code>{{testResult()}}</code>
|
||||||
} @else if (testFailed) {
|
} @else if (testFailed()) {
|
||||||
<div class="text-danger" i18n>Path test failed</div>
|
<div class="text-danger" i18n>Path test failed</div>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="text-muted small" i18n>No document selected</div>
|
<div class="text-muted small" i18n>No document selected</div>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
[compareWith]="compareDocuments"
|
[compareWith]="compareDocuments"
|
||||||
[trackByFn]="trackByFn"
|
[trackByFn]="trackByFn"
|
||||||
[minTermLength]="2"
|
[minTermLength]="2"
|
||||||
[loading]="loading"
|
[loading]="loading()"
|
||||||
[typeahead]="documentsInput$"
|
[typeahead]="documentsInput$"
|
||||||
(change)="testPath($event)">
|
(change)="testPath($event)">
|
||||||
<ng-template #loadingTemplate ng-loadingspinner-tmp>
|
<ng-template #loadingTemplate ng-loadingspinner-tmp>
|
||||||
|
|||||||
+5
-5
@@ -58,17 +58,17 @@ describe('StoragePathEditDialogComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
component.testPath({ id: 1 })
|
component.testPath({ id: 1 })
|
||||||
expect(testSpy).toHaveBeenCalledWith('test/{{title}}', 1)
|
expect(testSpy).toHaveBeenCalledWith('test/{{title}}', 1)
|
||||||
expect(component.testResult).toBe('test/abc123')
|
expect(component.testResult()).toBe('test/abc123')
|
||||||
expect(component.testFailed).toBeFalsy()
|
expect(component.testFailed()).toBeFalsy()
|
||||||
|
|
||||||
// test failed
|
// test failed
|
||||||
testSpy.mockReturnValueOnce(of(''))
|
testSpy.mockReturnValueOnce(of(''))
|
||||||
component.testPath({ id: 1 })
|
component.testPath({ id: 1 })
|
||||||
expect(component.testResult).toBeNull()
|
expect(component.testResult()).toBeNull()
|
||||||
expect(component.testFailed).toBeTruthy()
|
expect(component.testFailed()).toBeTruthy()
|
||||||
|
|
||||||
component.testPath(null)
|
component.testPath(null)
|
||||||
expect(component.testResult).toBeNull()
|
expect(component.testResult()).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should compare two documents by id', () => {
|
it('should compare two documents by id', () => {
|
||||||
|
|||||||
+11
-11
@@ -1,5 +1,5 @@
|
|||||||
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
|
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
|
||||||
import { Component, OnDestroy, inject } from '@angular/core'
|
import { Component, OnDestroy, inject, signal } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
@@ -65,9 +65,9 @@ export class StoragePathEditDialogComponent
|
|||||||
public documentsInput$ = new Subject<string>()
|
public documentsInput$ = new Subject<string>()
|
||||||
public foundDocuments$: Observable<Document[]>
|
public foundDocuments$: Observable<Document[]>
|
||||||
private testDocument: Document
|
private testDocument: Document
|
||||||
public testResult: string
|
readonly testResult = signal<string>(undefined)
|
||||||
public testFailed: boolean = false
|
readonly testFailed = signal(false)
|
||||||
public testLoading = false
|
readonly testLoading = signal(false)
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
@@ -99,22 +99,22 @@ export class StoragePathEditDialogComponent
|
|||||||
|
|
||||||
public testPath(document: Document) {
|
public testPath(document: Document) {
|
||||||
if (!document) {
|
if (!document) {
|
||||||
this.testResult = null
|
this.testResult.set(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.testDocument = document
|
this.testDocument = document
|
||||||
this.testLoading = true
|
this.testLoading.set(true)
|
||||||
;(this.service as StoragePathService)
|
;(this.service as StoragePathService)
|
||||||
.testPath(this.objectForm.get('path').value, document.id)
|
.testPath(this.objectForm.get('path').value, document.id)
|
||||||
.subscribe((result) => {
|
.subscribe((result) => {
|
||||||
if (result?.length) {
|
if (result?.length) {
|
||||||
this.testResult = result
|
this.testResult.set(result)
|
||||||
this.testFailed = false
|
this.testFailed.set(false)
|
||||||
} else {
|
} else {
|
||||||
this.testResult = null
|
this.testResult.set(null)
|
||||||
this.testFailed = true
|
this.testFailed.set(true)
|
||||||
}
|
}
|
||||||
this.testLoading = false
|
this.testLoading.set(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>
|
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>
|
||||||
|
|
||||||
<pngx-input-select i18n-title title="Parent" formControlName="parent" [items]="tags" [allowNull]="true" [error]="error?.parent"></pngx-input-select>
|
<pngx-input-select i18n-title title="Parent" formControlName="parent" [items]="tags()" [allowNull]="true" [error]="error?.parent"></pngx-input-select>
|
||||||
|
|
||||||
<pngx-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></pngx-input-check>
|
<pngx-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></pngx-input-check>
|
||||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject, signal } from '@angular/core'
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
@@ -35,7 +35,7 @@ import { TextComponent } from '../../input/text/text.component'
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TagEditDialogComponent extends EditDialogComponent<Tag> {
|
export class TagEditDialogComponent extends EditDialogComponent<Tag> {
|
||||||
tags: Tag[]
|
readonly tags = signal<Tag[]>([])
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
@@ -43,7 +43,7 @@ export class TagEditDialogComponent extends EditDialogComponent<Tag> {
|
|||||||
this.userService = inject(UserService)
|
this.userService = inject(UserService)
|
||||||
this.settingsService = inject(SettingsService)
|
this.settingsService = inject(SettingsService)
|
||||||
this.service.listAll().subscribe((result) => {
|
this.service.listAll().subscribe((result) => {
|
||||||
this.tags = result.results
|
this.tags.set(result.results)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
@@ -1019,4 +1019,28 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
'pass3',
|
'pass3',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should parse passwords again when retrying a failed save', () => {
|
||||||
|
component.object = {
|
||||||
|
name: 'Workflow with blank Passwords',
|
||||||
|
id: 1,
|
||||||
|
order: null,
|
||||||
|
enabled: true,
|
||||||
|
triggers: [],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: WorkflowActionType.PasswordRemoval,
|
||||||
|
passwords: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
component.ngOnInit()
|
||||||
|
|
||||||
|
component.save()
|
||||||
|
component.objectForm.get('order').setValue(1)
|
||||||
|
|
||||||
|
expect(() => component.save()).not.toThrow()
|
||||||
|
expect(component.objectForm.get('actions').value[0].passwords).toEqual([])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-3
@@ -1207,9 +1207,8 @@ export class WorkflowEditDialogComponent
|
|||||||
return passwords.join('\n')
|
return passwords.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
private parsePasswords(value: string = ''): string[] {
|
private parsePasswords(value: string | string[] = ''): string[] {
|
||||||
return value
|
return (Array.isArray(value) ? value : value.split(/[\n,]+/))
|
||||||
.split(/[\n,]+/)
|
|
||||||
.map((entry) => entry.trim())
|
.map((entry) => entry.trim())
|
||||||
.filter((entry) => entry.length > 0)
|
.filter((entry) => entry.length > 0)
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-17
@@ -34,32 +34,30 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (selectionModel.items) {
|
@if (selectionModel.items) {
|
||||||
<cdk-virtual-scroll-viewport class="items" [itemSize]="FILTERABLE_BUTTON_HEIGHT_PX" #buttonsViewport [style.height.px]="scrollViewportHeight">
|
<cdk-virtual-scroll-viewport class="items" [itemSize]="FILTERABLE_BUTTON_HEIGHT_PX" #buttonsViewport [style.height.px]="scrollViewportHeight">
|
||||||
<div *cdkVirtualFor="let item of selectionModel.items | filter: filterText:'name'; trackBy: trackByItem; let i = index">
|
<div *cdkVirtualFor="let item of filteredItems; trackBy: trackByItem; let i = index">
|
||||||
@if (allowSelectNone || item.id) {
|
<pngx-toggleable-dropdown-button
|
||||||
<pngx-toggleable-dropdown-button
|
[item]="item"
|
||||||
[item]="item"
|
[hideCount]="hideCount(item)"
|
||||||
[hideCount]="hideCount(item)"
|
[opacifyCount]="!editing"
|
||||||
[opacifyCount]="!editing"
|
[state]="selectionModel.get(item.id)"
|
||||||
[state]="selectionModel.get(item.id)"
|
[count]="getUpdatedDocumentCount(item.id)"
|
||||||
[count]="getUpdatedDocumentCount(item.id)"
|
(toggled)="selectionModel.toggle(item.id)"
|
||||||
(toggled)="selectionModel.toggle(item.id)"
|
(exclude)="excludeClicked(item.id)"
|
||||||
(exclude)="excludeClicked(item.id)"
|
[disabled]="disabled">
|
||||||
[disabled]="disabled">
|
</pngx-toggleable-dropdown-button>
|
||||||
</pngx-toggleable-dropdown-button>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</cdk-virtual-scroll-viewport>
|
</cdk-virtual-scroll-viewport>
|
||||||
}
|
}
|
||||||
@if (editing) {
|
@if (editing) {
|
||||||
@if ((selectionModel.items | filter: filterText:'name').length === 0 && createRef !== undefined) {
|
@if (filteredItems.length === 0 && createRef !== undefined) {
|
||||||
<button class="list-group-item list-group-item-action bg-light" (click)="createClicked()" [disabled]="disabled">
|
<button class="list-group-item list-group-item-action bg-light" (click)="createClicked()" [disabled]="disabled">
|
||||||
<small class="ms-2"><ng-container i18n>Create</ng-container> "{{filterText}}"</small>
|
<small class="ms-2"><ng-container i18n>Create</ng-container> "{{filterText}}"</small>
|
||||||
<i-bs width="1.5em" height="1em" name="plus"></i-bs>
|
<i-bs width="1.5em" height="1em" name="plus"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if ((selectionModel.items | filter: filterText:'name').length > 0) {
|
@if (filteredItems.length > 0) {
|
||||||
<button class="list-group-item list-group-item-action bg-light d-flex align-items-center" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
|
<button class="list-group-item list-group-item-action bg-light d-flex align-items-center" (click)="applyClicked()" [disabled]="!modelIsDirty() || disabled">
|
||||||
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty}" i18n>Apply</small>
|
<small class="ms-2" [ngClass]="{'fw-bold': modelIsDirty()}" i18n>Apply</small>
|
||||||
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
|
<i-bs width="1.5em" height="1em" name="arrow-right"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-3
@@ -221,7 +221,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
.dispatchEvent(new MouseEvent('click')) // open
|
.dispatchEvent(new MouseEvent('click')) // open
|
||||||
selectionModel.toggle(items[0].id)
|
selectionModel.toggle(items[0].id)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.modelIsDirty).toBeTruthy()
|
expect(component.modelIsDirty()).toBeTruthy()
|
||||||
let applyResult: ChangedItems
|
let applyResult: ChangedItems
|
||||||
const closeSpy = jest.spyOn(component.dropdown, 'close')
|
const closeSpy = jest.spyOn(component.dropdown, 'close')
|
||||||
component.apply.subscribe((result) => (applyResult = result))
|
component.apply.subscribe((result) => (applyResult = result))
|
||||||
@@ -244,7 +244,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
.dispatchEvent(new MouseEvent('click')) // open
|
.dispatchEvent(new MouseEvent('click')) // open
|
||||||
selectionModel.toggle(items[0].id)
|
selectionModel.toggle(items[0].id)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(component.modelIsDirty).toBeTruthy()
|
expect(component.modelIsDirty()).toBeTruthy()
|
||||||
let applyResult: ChangedItems
|
let applyResult: ChangedItems
|
||||||
component.apply.subscribe((result) => (applyResult = result))
|
component.apply.subscribe((result) => (applyResult = result))
|
||||||
component.dropdown.close()
|
component.dropdown.close()
|
||||||
@@ -265,7 +265,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
expect(document.activeElement).toEqual(
|
expect(document.activeElement).toEqual(
|
||||||
component.listFilterTextInput.nativeElement
|
component.listFilterTextInput.nativeElement
|
||||||
)
|
)
|
||||||
expect(component.buttonsViewport.getRenderedRange().end).toEqual(3) // all items shown
|
expect(component.buttonsViewport.getRenderedRange().end).toEqual(
|
||||||
|
items.length
|
||||||
|
) // all selectable items shown
|
||||||
|
|
||||||
component.filterText = 'Tag2'
|
component.filterText = 'Tag2'
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
@@ -278,6 +280,29 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
expect(component.filterText).toHaveLength(0)
|
expect(component.filterText).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should omit disallowed null items from the virtual scroll viewport', async () => {
|
||||||
|
component.selectionModel.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
|
fixture.nativeElement
|
||||||
|
.querySelector('button')
|
||||||
|
.dispatchEvent(new MouseEvent('click')) // open
|
||||||
|
fixture.detectChanges()
|
||||||
|
await wait(100)
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.buttonsViewport?.checkViewportSize()
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.filteredItems).toEqual(items)
|
||||||
|
expect(component.scrollViewportHeight).toEqual(
|
||||||
|
items.length * component.FILTERABLE_BUTTON_HEIGHT_PX
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
component.buttonsViewport.elementRef.nativeElement.querySelectorAll(
|
||||||
|
'.cdk-virtual-scroll-content-wrapper > div'
|
||||||
|
)
|
||||||
|
).toHaveLength(items.length)
|
||||||
|
})
|
||||||
|
|
||||||
it('should toggle & close on enter inside filter field if 1 item remains', async () => {
|
it('should toggle & close on enter inside filter field if 1 item remains', async () => {
|
||||||
component.selectionModel.items = items
|
component.selectionModel.items = items
|
||||||
component.icon = 'tag-fill'
|
component.icon = 'tag-fill'
|
||||||
|
|||||||
+15
-10
@@ -12,6 +12,7 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
inject,
|
inject,
|
||||||
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
@@ -660,7 +661,6 @@ export class FilterableDropdownSelectionModel {
|
|||||||
imports: [
|
imports: [
|
||||||
ClearableBadgeComponent,
|
ClearableBadgeComponent,
|
||||||
ToggleableDropdownButtonComponent,
|
ToggleableDropdownButtonComponent,
|
||||||
FilterPipe,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
@@ -796,22 +796,27 @@ export class FilterableDropdownComponent
|
|||||||
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
modelIsDirty: boolean = false
|
readonly modelIsDirty = signal(false)
|
||||||
|
|
||||||
private keyboardIndex: number
|
private keyboardIndex: number
|
||||||
|
|
||||||
|
public get filteredItems(): MatchingModel[] {
|
||||||
|
return this.filterPipe
|
||||||
|
.transform(this.items, this.filterText, 'name')
|
||||||
|
.filter((item) => this.allowSelectNone || Boolean(item.id))
|
||||||
|
}
|
||||||
|
|
||||||
public get scrollViewportHeight(): number {
|
public get scrollViewportHeight(): number {
|
||||||
const filteredLength = this.filterPipe.transform(
|
return Math.min(
|
||||||
this.items,
|
this.filteredItems.length * this.FILTERABLE_BUTTON_HEIGHT_PX,
|
||||||
this.filterText
|
400
|
||||||
).length
|
)
|
||||||
return Math.min(filteredLength * this.FILTERABLE_BUTTON_HEIGHT_PX, 400)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.selectionModelChange.subscribe((updatedModel) => {
|
this.selectionModelChange.subscribe((updatedModel) => {
|
||||||
this.modelIsDirty = updatedModel.isDirty()
|
this.modelIsDirty.set(updatedModel.isDirty())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,7 +863,7 @@ export class FilterableDropdownComponent
|
|||||||
}, 0)
|
}, 0)
|
||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
this.selectionModel.reset()
|
this.selectionModel.reset()
|
||||||
this.modelIsDirty = false
|
this.modelIsDirty.set(false)
|
||||||
}
|
}
|
||||||
this.selectionModel.singleSelect =
|
this.selectionModel.singleSelect =
|
||||||
this.editing && !this.selectionModel.manyToOne
|
this.editing && !this.selectionModel.manyToOne
|
||||||
@@ -877,7 +882,7 @@ export class FilterableDropdownComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
listFilterEnter(): void {
|
listFilterEnter(): void {
|
||||||
let filtered = this.filterPipe.transform(this.items, this.filterText)
|
const filtered = this.filteredItems
|
||||||
if (filtered.length == 1) {
|
if (filtered.length == 1) {
|
||||||
this.selectionModel.toggle(filtered[0].id)
|
this.selectionModel.toggle(filtered[0].id)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
+4
-3
@@ -5,6 +5,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -63,11 +64,11 @@ export class CustomFieldsValuesComponent extends AbstractInputComponent<Object>
|
|||||||
|
|
||||||
super()
|
super()
|
||||||
customFieldsService.listAll().subscribe((items) => {
|
customFieldsService.listAll().subscribe((items) => {
|
||||||
this.fields = items.results
|
this.fields.set(items.results)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fields: CustomField[]
|
private readonly fields = signal<CustomField[]>([])
|
||||||
|
|
||||||
private _selectedFields: number[]
|
private _selectedFields: number[]
|
||||||
|
|
||||||
@@ -90,6 +91,6 @@ export class CustomFieldsValuesComponent extends AbstractInputComponent<Object>
|
|||||||
public removeSelectedField: EventEmitter<number> = new EventEmitter<number>()
|
public removeSelectedField: EventEmitter<number> = new EventEmitter<number>()
|
||||||
|
|
||||||
public getCustomField(id: number): CustomField {
|
public getCustomField(id: number): CustomField {
|
||||||
return this.fields.find((field) => field.id === id)
|
return this.fields().find((field) => field.id === id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
[compareWith]="compareDocuments"
|
[compareWith]="compareDocuments"
|
||||||
[trackByFn]="trackByFn"
|
[trackByFn]="trackByFn"
|
||||||
[minTermLength]="2"
|
[minTermLength]="2"
|
||||||
[loading]="loading"
|
[loading]="loading()"
|
||||||
[typeahead]="documentsInput$"
|
[typeahead]="documentsInput$"
|
||||||
(mousedown)="$event.stopImmediatePropagation()"
|
(mousedown)="$event.stopImmediatePropagation()"
|
||||||
(change)="onChange(selectedDocumentIDs)">
|
(change)="onChange(selectedDocumentIDs)">
|
||||||
|
|||||||
+26
-2
@@ -2,7 +2,11 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
import { of, throwError } from 'rxjs'
|
import { By } from '@angular/platform-browser'
|
||||||
|
import { provideRouter } from '@angular/router'
|
||||||
|
import { NgSelectComponent } from '@ng-select/ng-select'
|
||||||
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { of, Subject, throwError } from 'rxjs'
|
||||||
import { FILTER_SIMPLE_TITLE } from 'src/app/data/filter-rule-type'
|
import { FILTER_SIMPLE_TITLE } from 'src/app/data/filter-rule-type'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { DocumentLinkComponent } from './document-link.component'
|
import { DocumentLinkComponent } from './document-link.component'
|
||||||
@@ -33,10 +37,11 @@ describe('DocumentLinkComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [DocumentLinkComponent],
|
imports: [DocumentLinkComponent, NgxBootstrapIconsModule.pick(allIcons)],
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
|
provideRouter([]),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
documentService = TestBed.inject(DocumentService)
|
documentService = TestBed.inject(DocumentService)
|
||||||
@@ -60,6 +65,25 @@ describe('DocumentLinkComponent', () => {
|
|||||||
expect(getSpy).toHaveBeenCalled()
|
expect(getSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should render loading and selected documents after async updates', async () => {
|
||||||
|
const result$ = new Subject<any>()
|
||||||
|
jest.spyOn(documentService, 'getFew').mockReturnValue(result$)
|
||||||
|
|
||||||
|
component.writeValue([1])
|
||||||
|
await fixture.whenStable()
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.directive(NgSelectComponent))
|
||||||
|
.componentInstance as NgSelectComponent
|
||||||
|
expect(select.loading()).toBe(true)
|
||||||
|
|
||||||
|
result$.next({ count: 1, all: [1], results: [documents[0]] })
|
||||||
|
result$.complete()
|
||||||
|
await fixture.whenStable()
|
||||||
|
|
||||||
|
expect(select.loading()).toBe(false)
|
||||||
|
expect(fixture.nativeElement.textContent).toContain(documents[0].title)
|
||||||
|
})
|
||||||
|
|
||||||
it('shoud maintain ordering of selected documents', () => {
|
it('shoud maintain ordering of selected documents', () => {
|
||||||
const getSpy = jest.spyOn(documentService, 'getFew')
|
const getSpy = jest.spyOn(documentService, 'getFew')
|
||||||
getSpy.mockImplementation((ids) => {
|
getSpy.mockImplementation((ids) => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
signal,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -63,7 +64,7 @@ export class DocumentLinkComponent
|
|||||||
|
|
||||||
documentsInput$ = new Subject<string>()
|
documentsInput$ = new Subject<string>()
|
||||||
foundDocuments$: Observable<Document[]>
|
foundDocuments$: Observable<Document[]>
|
||||||
loading = false
|
readonly loading = signal(false)
|
||||||
selectedDocuments: Document[] = []
|
selectedDocuments: Document[] = []
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
@@ -93,12 +94,12 @@ export class DocumentLinkComponent
|
|||||||
this.selectedDocuments = []
|
this.selectedDocuments = []
|
||||||
super.writeValue([])
|
super.writeValue([])
|
||||||
} else {
|
} else {
|
||||||
this.loading = true
|
this.loading.set(true)
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.getFew(documentIDs, { fields: 'id,title' })
|
.getFew(documentIDs, { fields: 'id,title' })
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((documentResults) => {
|
.subscribe((documentResults) => {
|
||||||
this.loading = false
|
this.loading.set(false)
|
||||||
this.selectedDocuments = documentIDs.map(
|
this.selectedDocuments = documentIDs.map(
|
||||||
(id) => documentResults.results.find((d) => d.id === id) ?? {}
|
(id) => documentResults.results.find((d) => d.id === id) ?? {}
|
||||||
)
|
)
|
||||||
@@ -113,7 +114,7 @@ export class DocumentLinkComponent
|
|||||||
this.documentsInput$.pipe(
|
this.documentsInput$.pipe(
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
takeUntil(this.unsubscribeNotifier),
|
takeUntil(this.unsubscribeNotifier),
|
||||||
tap(() => (this.loading = true)),
|
tap(() => this.loading.set(true)),
|
||||||
switchMap((title) =>
|
switchMap((title) =>
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.listFiltered(
|
.listFiltered(
|
||||||
@@ -133,7 +134,7 @@ export class DocumentLinkComponent
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
catchError(() => of([])), // empty on error
|
catchError(() => of([])), // empty on error
|
||||||
tap(() => (this.loading = false))
|
tap(() => this.loading.set(false))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export class NumberComponent extends AbstractInputComponent<number> {
|
|||||||
this.documentService.getNextAsn().subscribe((nextAsn) => {
|
this.documentService.getNextAsn().subscribe((nextAsn) => {
|
||||||
this.value = nextAsn
|
this.value = nextAsn
|
||||||
this.onChange(this.value)
|
this.onChange(this.value)
|
||||||
|
this.changeDetector.markForCheck()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,13 @@
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
& section {
|
||||||
|
position: absolute;
|
||||||
|
text-align: initial;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
& .annotationTextContent {
|
& .annotationTextContent {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ describe('PngxPdfViewerComponent', () => {
|
|||||||
}
|
}
|
||||||
expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
|
expect(viewer).toBeInstanceOf(PDFSinglePageViewer)
|
||||||
expect(viewer.options.textLayerMode).toBe(0)
|
expect(viewer.options.textLayerMode).toBe(0)
|
||||||
|
expect(viewer.options.enableSelectionRendering).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('applies zoom, rotation, and page changes', async () => {
|
it('applies zoom, rotation, and page changes', async () => {
|
||||||
@@ -129,13 +130,37 @@ describe('PngxPdfViewerComponent', () => {
|
|||||||
;(component as any).applyScale()
|
;(component as any).applyScale()
|
||||||
expect(viewer.currentScaleValue).toBe(PdfZoomScale.PageFit)
|
expect(viewer.currentScaleValue).toBe(PdfZoomScale.PageFit)
|
||||||
expect(viewer.currentScale).toBe(2)
|
expect(viewer.currentScale).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not reapply scale for page-only changes', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const pdf = (component as any).pdf as { numPages: number }
|
||||||
|
pdf.numPages = 3
|
||||||
|
const viewer = (component as any).pdfViewer as PDFViewer
|
||||||
|
viewer.setDocument(pdf)
|
||||||
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
|
const applyScaleSpy = jest.spyOn(component as any, 'applyScale')
|
||||||
component.page = 2
|
component.page = 2
|
||||||
;(component as any).lastViewerPage = 2
|
|
||||||
;(component as any).applyViewerState()
|
component.ngOnChanges({
|
||||||
|
page: new SimpleChange(1, 2, false),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(viewer.currentPageNumber).toBe(2)
|
||||||
expect((component as any).lastViewerPage).toBeUndefined()
|
expect((component as any).lastViewerPage).toBeUndefined()
|
||||||
expect(applyScaleSpy).toHaveBeenCalled()
|
expect(applyScaleSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not reset the viewer when it is already on the requested page', async () => {
|
||||||
|
await initComponent()
|
||||||
|
|
||||||
|
const viewer = (component as any).pdfViewer as PDFViewer
|
||||||
|
const currentPageSpy = jest.spyOn(viewer, 'currentPageNumber', 'set')
|
||||||
|
component.page = viewer.currentPageNumber
|
||||||
|
|
||||||
|
;(component as any).applyViewerState()
|
||||||
|
|
||||||
|
expect(currentPageSpy).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('dispatches find when search query changes after render', async () => {
|
it('dispatches find when search query changes after render', async () => {
|
||||||
@@ -271,6 +296,22 @@ describe('PngxPdfViewerComponent', () => {
|
|||||||
expect(mockViewer.currentPageNumber).toBe(1)
|
expect(mockViewer.currentPageNumber).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('reloads when the source revision changes', () => {
|
||||||
|
const resetSpy = jest.spyOn(component as any, 'resetViewerState')
|
||||||
|
const loadSpy = jest
|
||||||
|
.spyOn(component as any, 'loadDocument')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
component.src = 'test.pdf'
|
||||||
|
component.sourceRevision = 1
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
sourceRevision: new SimpleChange(0, 1, false),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(resetSpy).toHaveBeenCalled()
|
||||||
|
expect(loadSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('applies viewer state after view init when already loaded', () => {
|
it('applies viewer state after view init when already loaded', () => {
|
||||||
const applySpy = jest.spyOn(component as any, 'applyViewerState')
|
const applySpy = jest.spyOn(component as any, 'applyViewerState')
|
||||||
;(component as any).hasLoaded = true
|
;(component as any).hasLoaded = true
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import {
|
import {
|
||||||
|
AnnotationMode,
|
||||||
getDocument,
|
getDocument,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
PDFDocumentLoadingTask,
|
PDFDocumentLoadingTask,
|
||||||
@@ -43,6 +44,7 @@ export class PngxPdfViewerComponent
|
|||||||
private readonly document = inject<Document>(DOCUMENT)
|
private readonly document = inject<Document>(DOCUMENT)
|
||||||
|
|
||||||
@Input() src!: string
|
@Input() src!: string
|
||||||
|
@Input() sourceRevision = 0
|
||||||
@Input() password?: string
|
@Input() password?: string
|
||||||
@Input() page?: number
|
@Input() page?: number
|
||||||
@Output() pageChange = new EventEmitter<number>()
|
@Output() pageChange = new EventEmitter<number>()
|
||||||
@@ -93,7 +95,7 @@ export class PngxPdfViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes['src'] || changes['password']) {
|
if (changes['src'] || changes['sourceRevision'] || changes['password']) {
|
||||||
this.resetViewerState()
|
this.resetViewerState()
|
||||||
if (this.src) {
|
if (this.src) {
|
||||||
this.loadDocument()
|
this.loadDocument()
|
||||||
@@ -115,7 +117,10 @@ export class PngxPdfViewerComponent
|
|||||||
changes['zoomScale'] ||
|
changes['zoomScale'] ||
|
||||||
changes['rotation']
|
changes['rotation']
|
||||||
) {
|
) {
|
||||||
this.applyViewerState()
|
// Prevent loop with page / scale application see https://github.com/paperless-ngx/paperless-ngx/issues/13404
|
||||||
|
this.applyViewerState(
|
||||||
|
!!(changes['zoom'] || changes['zoomScale'] || changes['rotation'])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changes['searchQuery']) {
|
if (changes['searchQuery']) {
|
||||||
@@ -217,6 +222,8 @@ export class PngxPdfViewerComponent
|
|||||||
linkService: this.linkService,
|
linkService: this.linkService,
|
||||||
findController: this.findController,
|
findController: this.findController,
|
||||||
textLayerMode,
|
textLayerMode,
|
||||||
|
annotationMode: AnnotationMode.ENABLE,
|
||||||
|
enableSelectionRendering: false,
|
||||||
removePageBorders: true,
|
removePageBorders: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +246,7 @@ export class PngxPdfViewerComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyViewerState(): void {
|
private applyViewerState(applyScale = true): void {
|
||||||
if (!this.pdfViewer) {
|
if (!this.pdfViewer) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -256,12 +263,14 @@ export class PngxPdfViewerComponent
|
|||||||
Math.max(Math.trunc(this.page), 1),
|
Math.max(Math.trunc(this.page), 1),
|
||||||
this.pdfViewer.pagesCount
|
this.pdfViewer.pagesCount
|
||||||
)
|
)
|
||||||
this.pdfViewer.currentPageNumber = nextPage
|
if (nextPage !== this.pdfViewer.currentPageNumber) {
|
||||||
|
this.pdfViewer.currentPageNumber = nextPage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.page === this.lastViewerPage) {
|
if (this.page === this.lastViewerPage) {
|
||||||
this.lastViewerPage = undefined
|
this.lastViewerPage = undefined
|
||||||
}
|
}
|
||||||
if (hasPages) {
|
if (hasPages && applyScale) {
|
||||||
this.applyScale()
|
this.applyScale()
|
||||||
}
|
}
|
||||||
this.dispatchFindIfReady()
|
this.dispatchFindIfReady()
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@
|
|||||||
[(ngModel)]="selectionModel.includeUsers"
|
[(ngModel)]="selectionModel.includeUsers"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
[items]="users"
|
[items]="users()"
|
||||||
bindLabel="username"
|
bindLabel="username"
|
||||||
multiple="true"
|
multiple="true"
|
||||||
bindValue="id"
|
bindValue="id"
|
||||||
|
|||||||
+10
-3
@@ -1,5 +1,12 @@
|
|||||||
import { NgClass } from '@angular/common'
|
import { NgClass } from '@angular/common'
|
||||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectComponent } from '@ng-select/ng-select'
|
import { NgSelectComponent } from '@ng-select/ng-select'
|
||||||
@@ -75,7 +82,7 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
@Output()
|
@Output()
|
||||||
ownerFilterSet = new EventEmitter<PermissionsSelectionModel>()
|
ownerFilterSet = new EventEmitter<PermissionsSelectionModel>()
|
||||||
|
|
||||||
users: User[]
|
readonly users = signal<User[]>([])
|
||||||
|
|
||||||
hideUnowned: boolean
|
hideUnowned: boolean
|
||||||
|
|
||||||
@@ -102,7 +109,7 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
|
|||||||
.listAll()
|
.listAll()
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => (this.users = result.results),
|
next: (result) => this.users.set(result.results),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle"
|
<a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle"
|
||||||
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body"
|
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body"
|
||||||
autoClose="true" [popoverClass]="popoverClass()" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
|
autoClose="true" [popoverClass]="popoverClass()" [popperOptions]="popperOptions"
|
||||||
|
(mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</a>
|
</a>
|
||||||
<ng-template #previewContent>
|
<ng-template #previewContent>
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ describe('PreviewPopupComponent', () => {
|
|||||||
expect(component.useNativePdfViewer).toBeTruthy()
|
expect(component.useNativePdfViewer).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should use fixed positioning for the preview popover', () => {
|
||||||
|
expect(component.popperOptions({}).strategy).toEqual('fixed')
|
||||||
|
})
|
||||||
|
|
||||||
it('should render object if native PDF viewer enabled', () => {
|
it('should render object if native PDF viewer enabled', () => {
|
||||||
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true)
|
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true)
|
||||||
component.popover.open()
|
component.popover.open()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { Options } from '@popperjs/core'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { first, Subject, takeUntil } from 'rxjs'
|
import { first, Subject, takeUntil } from 'rxjs'
|
||||||
import { Document } from 'src/app/data/document'
|
import { Document } from 'src/app/data/document'
|
||||||
@@ -74,6 +75,11 @@ export class PreviewPopupComponent implements OnDestroy {
|
|||||||
|
|
||||||
readonly popoverClass = signal('shadow popover-preview')
|
readonly popoverClass = signal('shadow popover-preview')
|
||||||
|
|
||||||
|
readonly popperOptions = (options: Partial<Options>): Partial<Options> => ({
|
||||||
|
...options,
|
||||||
|
strategy: 'fixed',
|
||||||
|
})
|
||||||
|
|
||||||
get renderAsObject(): boolean {
|
get renderAsObject(): boolean {
|
||||||
return (this.isPdf && this.useNativePdfViewer) || !this.isPdf
|
return (this.isPdf && this.useNativePdfViewer) || !this.isPdf
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -57,15 +57,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
@if (socialAccounts?.length > 0) {
|
@if (socialAccounts().length > 0) {
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p i18n>Connected social accounts</p>
|
<p i18n>Connected social accounts</p>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
@for (account of socialAccounts; track account.id) {
|
@for (account of socialAccounts(); track account.id) {
|
||||||
<li class="list-group-item"
|
<li class="list-group-item"
|
||||||
ngbPopover="Set a password before disconnecting social account."
|
ngbPopover="Set a password before disconnecting social account."
|
||||||
i18n-ngbPopover
|
i18n-ngbPopover
|
||||||
[disablePopover]="hasUsablePassword"
|
[disablePopover]="hasUsablePassword()"
|
||||||
triggers="mouseenter:mouseleave">
|
triggers="mouseenter:mouseleave">
|
||||||
{{account.name}} ({{account.provider}})
|
{{account.name}} ({{account.provider}})
|
||||||
<pngx-confirm-button
|
<pngx-confirm-button
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
i18n-title
|
i18n-title
|
||||||
buttonClasses="btn-outline-danger btn-sm ms-2 align-baseline"
|
buttonClasses="btn-outline-danger btn-sm ms-2 align-baseline"
|
||||||
iconName="trash"
|
iconName="trash"
|
||||||
[disabled]="!hasUsablePassword"
|
[disabled]="!hasUsablePassword()"
|
||||||
(confirm)="disconnectSocialAccount(account.id)">
|
(confirm)="disconnectSocialAccount(account.id)">
|
||||||
</pngx-confirm-button>
|
</pngx-confirm-button>
|
||||||
</li>
|
</li>
|
||||||
@@ -84,11 +84,11 @@
|
|||||||
<div class="form-text text-muted text-end fst-italic" i18n>Warning: disconnecting social accounts cannot be undone</div>
|
<div class="form-text text-muted text-end fst-italic" i18n>Warning: disconnecting social accounts cannot be undone</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (socialAccountProviders?.length > 0) {
|
@if (socialAccountProviders().length > 0) {
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p i18n>Connect new social account</p>
|
<p i18n>Connect new social account</p>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
@for (provider of socialAccountProviders; track provider.name) {
|
@for (provider of socialAccountProviders(); track provider.name) {
|
||||||
<a class="list-group-item list-group-item-action text-primary d-flex align-items-center" href="{{ provider.login_url }}" rel="noopener noreferrer">
|
<a class="list-group-item list-group-item-action text-primary d-flex align-items-center" href="{{ provider.login_url }}" rel="noopener noreferrer">
|
||||||
{{provider.name}}<i-bs class="pb-1 ms-2" name="box-arrow-up-right"></i-bs>
|
{{provider.name}}<i-bs class="pb-1 ms-2" name="box-arrow-up-right"></i-bs>
|
||||||
</a>
|
</a>
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (!isTotpEnabled) {
|
@if (!isTotpEnabled()) {
|
||||||
<div ngbAccordion>
|
<div ngbAccordion>
|
||||||
<div ngbAccordionItem>
|
<div ngbAccordionItem>
|
||||||
<h2 ngbAccordionHeader>
|
<h2 ngbAccordionHeader>
|
||||||
@@ -105,10 +105,10 @@
|
|||||||
<div ngbAccordionCollapse>
|
<div ngbAccordionCollapse>
|
||||||
<div ngbAccordionBody>
|
<div ngbAccordionBody>
|
||||||
<ng-template>
|
<ng-template>
|
||||||
@if (totpSettingsLoading) {
|
@if (totpSettingsLoading()) {
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
} @else if (totpSettings) {
|
} @else if (totpSettings()) {
|
||||||
<figure class="figure">
|
<figure class="figure">
|
||||||
@if (qrSvgDataUrl) {
|
@if (qrSvgDataUrl) {
|
||||||
<img class="bg-white d-inline-block" [src]="qrSvgDataUrl" alt="Authenticator QR code">
|
<img class="bg-white d-inline-block" [src]="qrSvgDataUrl" alt="Authenticator QR code">
|
||||||
@@ -116,14 +116,14 @@
|
|||||||
<figcaption class="figure-caption text-end mt-2" i18n>Scan the QR code with your authenticator app and then enter the code below</figcaption>
|
<figcaption class="figure-caption text-end mt-2" i18n>Scan the QR code with your authenticator app and then enter the code below</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<p>
|
<p>
|
||||||
<ng-container i18n>Authenticator secret</ng-container>: <code>{{totpSettings.secret}}</code>.
|
<ng-container i18n>Authenticator secret</ng-container>: <code>{{totpSettings().secret}}</code>.
|
||||||
<ng-container i18n>You can store this secret and use it to reinstall your authenticator app at a later time.</ng-container>
|
<ng-container i18n>You can store this secret and use it to reinstall your authenticator app at a later time.</ng-container>
|
||||||
</p>
|
</p>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input type="text" class="form-control" formControlName="totp_code" placeholder="Code" i18n-placeholder>
|
<input type="text" class="form-control" formControlName="totp_code" placeholder="Code" i18n-placeholder>
|
||||||
<button type="button" class="btn btn-primary ml-auto" (click)="activateTotp()" [disabled]="totpLoading">
|
<button type="button" class="btn btn-primary ml-auto" (click)="activateTotp()" [disabled]="totpLoading()">
|
||||||
<ng-container i18n>Enable</ng-container>
|
<ng-container i18n>Enable</ng-container>
|
||||||
@if (totpLoading) {
|
@if (totpLoading()) {
|
||||||
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||||
<div class="visually-hidden" i18n>Loading...</div>
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
}
|
}
|
||||||
@@ -137,18 +137,18 @@
|
|||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<label class="d-block mb-2" i18n>Two-factor Authentication</label>
|
<label class="d-block mb-2" i18n>Two-factor Authentication</label>
|
||||||
@if (recoveryCodes) {
|
@if (recoveryCodes()) {
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">
|
||||||
<i-bs name="exclamation-triangle" class="me-1"></i-bs><ng-container i18n>Recovery codes will not be shown again, make sure to save them.</ng-container>
|
<i-bs name="exclamation-triangle" class="me-1"></i-bs><ng-container i18n>Recovery codes will not be shown again, make sure to save them.</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-row align-items-start mb-3">
|
<div class="d-flex flex-row align-items-start mb-3">
|
||||||
<ul class="list-group w-50">
|
<ul class="list-group w-50">
|
||||||
@for (code of recoveryCodes; track code; let i = $index) {
|
@for (code of recoveryCodes(); track code; let i = $index) {
|
||||||
@if (i % 2 === 0) {
|
@if (i % 2 === 0) {
|
||||||
<li class="list-group-item d-flex justify-content-around align-items-center">
|
<li class="list-group-item d-flex justify-content-around align-items-center">
|
||||||
<code>{{code}}</code>
|
<code>{{code}}</code>
|
||||||
@if (recoveryCodes[i + 1]) {
|
@if (recoveryCodes()[i + 1]) {
|
||||||
<code>{{recoveryCodes[i + 1]}}</code>
|
<code>{{recoveryCodes()[i + 1]}}</code>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
i18n-title
|
i18n-title
|
||||||
buttonClasses="btn-outline-danger btn-sm"
|
buttonClasses="btn-outline-danger btn-sm"
|
||||||
iconName="trash"
|
iconName="trash"
|
||||||
[disabled]="totpLoading"
|
[disabled]="totpLoading()"
|
||||||
(confirm)="deactivateTotp()">
|
(confirm)="deactivateTotp()">
|
||||||
</pngx-confirm-button>
|
</pngx-confirm-button>
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-12
@@ -10,7 +10,7 @@ import {
|
|||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { of, throwError } from 'rxjs'
|
import { NEVER, of, Subject, throwError } from 'rxjs'
|
||||||
import { ProfileService } from 'src/app/services/profile.service'
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import * as navUtils from 'src/app/utils/navigation'
|
import * as navUtils from 'src/app/utils/navigation'
|
||||||
@@ -60,6 +60,10 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())],
|
providers: [NgbActiveModal, provideHttpClient(withInterceptorsFromDi())],
|
||||||
})
|
})
|
||||||
profileService = TestBed.inject(ProfileService)
|
profileService = TestBed.inject(ProfileService)
|
||||||
|
jest.spyOn(profileService, 'get').mockReturnValue(NEVER)
|
||||||
|
jest
|
||||||
|
.spyOn(profileService, 'getSocialAccountProviders')
|
||||||
|
.mockReturnValue(of([]))
|
||||||
toastService = TestBed.inject(ToastService)
|
toastService = TestBed.inject(ToastService)
|
||||||
clipboard = TestBed.inject(Clipboard)
|
clipboard = TestBed.inject(Clipboard)
|
||||||
fixture = TestBed.createComponent(ProfileEditDialogComponent)
|
fixture = TestBed.createComponent(ProfileEditDialogComponent)
|
||||||
@@ -155,7 +159,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
'getSocialAccountProviders'
|
'getSocialAccountProviders'
|
||||||
)
|
)
|
||||||
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
getProvidersSpy.mockReturnValue(of(socialAccountProviders))
|
||||||
component.hasUsablePassword = true
|
component.hasUsablePassword.set(true)
|
||||||
component.ngOnInit()
|
component.ngOnInit()
|
||||||
component.form.get('password').patchValue('new*pass')
|
component.form.get('password').patchValue('new*pass')
|
||||||
component.onPasswordKeyUp({
|
component.onPasswordKeyUp({
|
||||||
@@ -268,6 +272,27 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
expect(getProvidersSpy).toHaveBeenCalled()
|
expect(getProvidersSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should render social account providers after an async update', async () => {
|
||||||
|
const providers$ = new Subject<typeof socialAccountProviders>()
|
||||||
|
jest.spyOn(profileService, 'get').mockReturnValue(of(profile))
|
||||||
|
jest
|
||||||
|
.spyOn(profileService, 'getSocialAccountProviders')
|
||||||
|
.mockReturnValue(providers$)
|
||||||
|
|
||||||
|
component.ngOnInit()
|
||||||
|
await fixture.whenStable()
|
||||||
|
expect(
|
||||||
|
fixture.nativeElement.querySelector('a[href="https://example.com"]')
|
||||||
|
).toBeNull()
|
||||||
|
|
||||||
|
providers$.next(socialAccountProviders)
|
||||||
|
await fixture.whenStable()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
fixture.nativeElement.querySelector('a[href="https://example.com"]')
|
||||||
|
).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('should remove disconnected social account from component, show error if needed', () => {
|
it('should remove disconnected social account from component, show error if needed', () => {
|
||||||
const disconnectSpy = jest.spyOn(profileService, 'disconnectSocialAccount')
|
const disconnectSpy = jest.spyOn(profileService, 'disconnectSocialAccount')
|
||||||
const getSpy = jest.spyOn(profileService, 'get')
|
const getSpy = jest.spyOn(profileService, 'get')
|
||||||
@@ -276,7 +301,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
|
|
||||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
|
||||||
expect(component.socialAccounts).toContainEqual(socialAccount)
|
expect(component.socialAccounts()).toContainEqual(socialAccount)
|
||||||
|
|
||||||
// fail first
|
// fail first
|
||||||
disconnectSpy.mockReturnValueOnce(
|
disconnectSpy.mockReturnValueOnce(
|
||||||
@@ -289,7 +314,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
disconnectSpy.mockReturnValue(of(socialAccount.id))
|
disconnectSpy.mockReturnValue(of(socialAccount.id))
|
||||||
component.disconnectSocialAccount(socialAccount.id)
|
component.disconnectSocialAccount(socialAccount.id)
|
||||||
expect(disconnectSpy).toHaveBeenCalled()
|
expect(disconnectSpy).toHaveBeenCalled()
|
||||||
expect(component.socialAccounts).not.toContainEqual(socialAccount)
|
expect(component.socialAccounts()).not.toContainEqual(socialAccount)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get totp settings', () => {
|
it('should get totp settings', () => {
|
||||||
@@ -310,7 +335,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
getSpy.mockReturnValue(of(settings))
|
getSpy.mockReturnValue(of(settings))
|
||||||
component.gettotpSettings()
|
component.gettotpSettings()
|
||||||
expect(getSpy).toHaveBeenCalled()
|
expect(getSpy).toHaveBeenCalled()
|
||||||
expect(component.totpSettings).toEqual(settings)
|
expect(component.totpSettings()).toEqual(settings)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should activate totp', () => {
|
it('should activate totp', () => {
|
||||||
@@ -319,15 +344,15 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const error = new Error('failed to activate totp')
|
const error = new Error('failed to activate totp')
|
||||||
activateSpy.mockReturnValueOnce(throwError(() => error))
|
activateSpy.mockReturnValueOnce(throwError(() => error))
|
||||||
component.totpSettings = {
|
component.totpSettings.set({
|
||||||
url: 'http://localhost/',
|
url: 'http://localhost/',
|
||||||
qr_svg: 'svg',
|
qr_svg: 'svg',
|
||||||
secret: 'secret',
|
secret: 'secret',
|
||||||
}
|
})
|
||||||
component.form.get('totp_code').patchValue('123456')
|
component.form.get('totp_code').patchValue('123456')
|
||||||
component.activateTotp()
|
component.activateTotp()
|
||||||
expect(activateSpy).toHaveBeenCalledWith(
|
expect(activateSpy).toHaveBeenCalledWith(
|
||||||
component.totpSettings.secret,
|
component.totpSettings().secret,
|
||||||
component.form.get('totp_code').value
|
component.form.get('totp_code').value
|
||||||
)
|
)
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toHaveBeenCalled()
|
||||||
@@ -341,8 +366,8 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
)
|
)
|
||||||
component.activateTotp()
|
component.activateTotp()
|
||||||
expect(toastInfoSpy).toHaveBeenCalled()
|
expect(toastInfoSpy).toHaveBeenCalled()
|
||||||
expect(component.isTotpEnabled).toBeTruthy()
|
expect(component.isTotpEnabled()).toBeTruthy()
|
||||||
expect(component.recoveryCodes).toEqual(['1', '2', '3'])
|
expect(component.recoveryCodes()).toEqual(['1', '2', '3'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should deactivate totp', () => {
|
it('should deactivate totp', () => {
|
||||||
@@ -362,13 +387,13 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
deactivateSpy.mockReturnValueOnce(of(true))
|
deactivateSpy.mockReturnValueOnce(of(true))
|
||||||
component.deactivateTotp()
|
component.deactivateTotp()
|
||||||
expect(toastInfoSpy).toHaveBeenCalled()
|
expect(toastInfoSpy).toHaveBeenCalled()
|
||||||
expect(component.isTotpEnabled).toBeFalsy()
|
expect(component.isTotpEnabled()).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should copy recovery codes', () => {
|
it('should copy recovery codes', () => {
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
const copySpy = jest.spyOn(clipboard, 'copy')
|
const copySpy = jest.spyOn(clipboard, 'copy')
|
||||||
component.recoveryCodes = ['1', '2', '3']
|
component.recoveryCodes.set(['1', '2', '3'])
|
||||||
component.copyRecoveryCodes()
|
component.copyRecoveryCodes()
|
||||||
expect(copySpy).toHaveBeenCalledWith('1\n2\n3')
|
expect(copySpy).toHaveBeenCalledWith('1\n2\n3')
|
||||||
jest.advanceTimersByTime(3000)
|
jest.advanceTimersByTime(3000)
|
||||||
|
|||||||
+36
-33
@@ -56,6 +56,14 @@ export class ProfileEditDialogComponent
|
|||||||
readonly showEmailConfirm = signal(false)
|
readonly showEmailConfirm = signal(false)
|
||||||
readonly copied = signal(false)
|
readonly copied = signal(false)
|
||||||
readonly codesCopied = signal(false)
|
readonly codesCopied = signal(false)
|
||||||
|
readonly hasUsablePassword = signal(false)
|
||||||
|
readonly isTotpEnabled = signal(false)
|
||||||
|
readonly totpSettings = signal<TotpSettings>(undefined)
|
||||||
|
readonly totpSettingsLoading = signal(false)
|
||||||
|
readonly totpLoading = signal(false)
|
||||||
|
readonly recoveryCodes = signal<string[]>(undefined)
|
||||||
|
readonly socialAccounts = signal<SocialAccount[]>([])
|
||||||
|
readonly socialAccountProviders = signal<SocialAccountProvider[]>([])
|
||||||
|
|
||||||
public form = new FormGroup({
|
public form = new FormGroup({
|
||||||
email: new FormControl(''),
|
email: new FormControl(''),
|
||||||
@@ -72,25 +80,15 @@ export class ProfileEditDialogComponent
|
|||||||
private newPassword: string
|
private newPassword: string
|
||||||
private passwordConfirm: string
|
private passwordConfirm: string
|
||||||
|
|
||||||
public hasUsablePassword: boolean = false
|
|
||||||
|
|
||||||
private currentEmail: string
|
private currentEmail: string
|
||||||
private newEmail: string
|
private newEmail: string
|
||||||
private emailConfirm: string
|
private emailConfirm: string
|
||||||
|
|
||||||
public isTotpEnabled: boolean = false
|
|
||||||
public totpSettings: TotpSettings
|
|
||||||
public totpSettingsLoading: boolean = false
|
|
||||||
public totpLoading: boolean = false
|
|
||||||
public recoveryCodes: string[]
|
|
||||||
public socialAccounts: SocialAccount[] = []
|
|
||||||
public socialAccountProviders: SocialAccountProvider[] = []
|
|
||||||
|
|
||||||
get qrSvgDataUrl(): string | null {
|
get qrSvgDataUrl(): string | null {
|
||||||
if (!this.totpSettings?.qr_svg) {
|
if (!this.totpSettings()?.qr_svg) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return `data:image/svg+xml;utf8,${encodeURIComponent(this.totpSettings.qr_svg)}`
|
return `data:image/svg+xml;utf8,${encodeURIComponent(this.totpSettings().qr_svg)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -107,20 +105,20 @@ export class ProfileEditDialogComponent
|
|||||||
this.onEmailChange()
|
this.onEmailChange()
|
||||||
})
|
})
|
||||||
this.currentPassword = profile.password
|
this.currentPassword = profile.password
|
||||||
this.hasUsablePassword = profile.has_usable_password
|
this.hasUsablePassword.set(profile.has_usable_password)
|
||||||
this.form.get('password').valueChanges.subscribe((newPassword) => {
|
this.form.get('password').valueChanges.subscribe((newPassword) => {
|
||||||
this.newPassword = newPassword
|
this.newPassword = newPassword
|
||||||
this.onPasswordChange()
|
this.onPasswordChange()
|
||||||
})
|
})
|
||||||
this.socialAccounts = profile.social_accounts
|
this.socialAccounts.set(profile.social_accounts ?? [])
|
||||||
this.isTotpEnabled = profile.is_mfa_enabled
|
this.isTotpEnabled.set(profile.is_mfa_enabled)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.profileService
|
this.profileService
|
||||||
.getSocialAccountProviders()
|
.getSocialAccountProviders()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((providers) => {
|
.subscribe((providers) => {
|
||||||
this.socialAccountProviders = providers
|
this.socialAccountProviders.set(providers ?? [])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +257,9 @@ export class ProfileEditDialogComponent
|
|||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (id: number) => {
|
next: (id: number) => {
|
||||||
this.socialAccounts = this.socialAccounts.filter((a) => a.id != id)
|
this.socialAccounts.update((accounts) =>
|
||||||
|
accounts.filter((account) => account.id != id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
@@ -271,36 +271,39 @@ export class ProfileEditDialogComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public gettotpSettings(): void {
|
public gettotpSettings(): void {
|
||||||
this.totpSettingsLoading = true
|
this.totpSettingsLoading.set(true)
|
||||||
this.profileService
|
this.profileService
|
||||||
.getTotpSettings()
|
.getTotpSettings()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (totpSettings) => {
|
next: (totpSettings) => {
|
||||||
this.totpSettingsLoading = false
|
this.totpSettingsLoading.set(false)
|
||||||
this.totpSettings = totpSettings
|
this.totpSettings.set(totpSettings)
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error fetching TOTP settings`,
|
$localize`Error fetching TOTP settings`,
|
||||||
error
|
error
|
||||||
)
|
)
|
||||||
this.totpSettingsLoading = false
|
this.totpSettingsLoading.set(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public activateTotp(): void {
|
public activateTotp(): void {
|
||||||
this.totpLoading = true
|
this.totpLoading.set(true)
|
||||||
this.form.get('totp_code').disable()
|
this.form.get('totp_code').disable()
|
||||||
this.profileService
|
this.profileService
|
||||||
.activateTotp(this.totpSettings.secret, this.form.get('totp_code').value)
|
.activateTotp(
|
||||||
|
this.totpSettings().secret,
|
||||||
|
this.form.get('totp_code').value
|
||||||
|
)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (activationResponse) => {
|
next: (activationResponse) => {
|
||||||
this.totpLoading = false
|
this.totpLoading.set(false)
|
||||||
this.isTotpEnabled = activationResponse.success
|
this.isTotpEnabled.set(activationResponse.success)
|
||||||
this.recoveryCodes = activationResponse.recovery_codes
|
this.recoveryCodes.set(activationResponse.recovery_codes)
|
||||||
this.form.get('totp_code').enable()
|
this.form.get('totp_code').enable()
|
||||||
if (activationResponse.success) {
|
if (activationResponse.success) {
|
||||||
this.toastService.showInfo($localize`TOTP activated successfully`)
|
this.toastService.showInfo($localize`TOTP activated successfully`)
|
||||||
@@ -309,7 +312,7 @@ export class ProfileEditDialogComponent
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.totpLoading = false
|
this.totpLoading.set(false)
|
||||||
this.form.get('totp_code').enable()
|
this.form.get('totp_code').enable()
|
||||||
this.toastService.showError($localize`Error activating TOTP`, error)
|
this.toastService.showError($localize`Error activating TOTP`, error)
|
||||||
},
|
},
|
||||||
@@ -317,15 +320,15 @@ export class ProfileEditDialogComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public deactivateTotp(): void {
|
public deactivateTotp(): void {
|
||||||
this.totpLoading = true
|
this.totpLoading.set(true)
|
||||||
this.profileService
|
this.profileService
|
||||||
.deactivateTotp()
|
.deactivateTotp()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (success) => {
|
next: (success) => {
|
||||||
this.totpLoading = false
|
this.totpLoading.set(false)
|
||||||
this.isTotpEnabled = !success
|
this.isTotpEnabled.set(!success)
|
||||||
this.recoveryCodes = null
|
this.recoveryCodes.set(null)
|
||||||
if (success) {
|
if (success) {
|
||||||
this.toastService.showInfo($localize`TOTP deactivated successfully`)
|
this.toastService.showInfo($localize`TOTP deactivated successfully`)
|
||||||
} else {
|
} else {
|
||||||
@@ -333,14 +336,14 @@ export class ProfileEditDialogComponent
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.totpLoading = false
|
this.totpLoading.set(false)
|
||||||
this.toastService.showError($localize`Error deactivating TOTP`, error)
|
this.toastService.showError($localize`Error deactivating TOTP`, error)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public copyRecoveryCodes(): void {
|
public copyRecoveryCodes(): void {
|
||||||
this.clipboard.copy(this.recoveryCodes.join('\n'))
|
this.clipboard.copy(this.recoveryCodes().join('\n'))
|
||||||
this.codesCopied.set(true)
|
this.codesCopied.set(true)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.codesCopied.set(false)
|
this.codesCopied.set(false)
|
||||||
|
|||||||
+8
-2
@@ -2,10 +2,16 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())">
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
|
} @else if (noSuggestions) {
|
||||||
|
<i-bs width="1.2em" height="1.2em" name="check-circle"></i-bs>
|
||||||
} @else {
|
} @else {
|
||||||
<i-bs width="1.2em" height="1.2em" name="stars"></i-bs>
|
<i-bs width="1.2em" height="1.2em" name="stars"></i-bs>
|
||||||
}
|
}
|
||||||
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
|
@if (noSuggestions) {
|
||||||
|
<span class="d-none d-lg-inline ps-1" i18n>No suggestions</span>
|
||||||
|
} @else {
|
||||||
|
<span class="d-none d-lg-inline ps-1" i18n>Suggest</span>
|
||||||
|
}
|
||||||
@if (totalSuggestions > 0) {
|
@if (totalSuggestions > 0) {
|
||||||
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
|
<span class="badge bg-primary ms-2">{{ totalSuggestions }}</span>
|
||||||
}
|
}
|
||||||
@@ -19,7 +25,7 @@
|
|||||||
|
|
||||||
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
|
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
|
||||||
<div class="list-group list-group-flush small pb-0">
|
<div class="list-group list-group-flush small pb-0">
|
||||||
@if (!suggestions()?.suggested_tags && !suggestions()?.suggested_document_types && !suggestions()?.suggested_correspondents) {
|
@if (totalSuggestions === 0) {
|
||||||
<div class="list-group-item text-muted fst-italic">
|
<div class="list-group-item text-muted fst-italic">
|
||||||
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
|
<small class="text-muted small fst-italic" i18n>No novel suggestions</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+29
@@ -30,6 +30,34 @@ describe('SuggestionsDropdownComponent', () => {
|
|||||||
expect(component.totalSuggestions).toBe(4)
|
expect(component.totalSuggestions).toBe(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show when a completed request returned no suggestions', () => {
|
||||||
|
fixture.componentRef.setInput('suggestions', {
|
||||||
|
correspondents: [],
|
||||||
|
tags: [],
|
||||||
|
document_types: [],
|
||||||
|
storage_paths: [],
|
||||||
|
dates: [],
|
||||||
|
})
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.noSuggestions).toBeTruthy()
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('No suggestions')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not show the empty state before a request or with suggestions', () => {
|
||||||
|
expect(component.noSuggestions).toBeFalsy()
|
||||||
|
|
||||||
|
fixture.componentRef.setInput('suggestions', {
|
||||||
|
correspondents: [],
|
||||||
|
tags: [42],
|
||||||
|
document_types: [],
|
||||||
|
storage_paths: [],
|
||||||
|
dates: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(component.noSuggestions).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('should emit getSuggestions when clickSuggest is called and suggestions are null', () => {
|
it('should emit getSuggestions when clickSuggest is called and suggestions are null', () => {
|
||||||
jest.spyOn(component.getSuggestions, 'emit')
|
jest.spyOn(component.getSuggestions, 'emit')
|
||||||
fixture.componentRef.setInput('suggestions', null)
|
fixture.componentRef.setInput('suggestions', null)
|
||||||
@@ -59,5 +87,6 @@ describe('SuggestionsDropdownComponent', () => {
|
|||||||
})
|
})
|
||||||
component.clickSuggest()
|
component.clickSuggest()
|
||||||
expect(component.dropdown.open).toBeTruthy()
|
expect(component.dropdown.open).toBeTruthy()
|
||||||
|
expect(fixture.nativeElement.textContent).toContain('No novel suggestions')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+17
@@ -61,4 +61,21 @@ export class SuggestionsDropdownComponent {
|
|||||||
this.suggestions()?.suggested_document_types?.length || 0
|
this.suggestions()?.suggested_document_types?.length || 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get noSuggestions(): boolean {
|
||||||
|
const suggestions = this.suggestions()
|
||||||
|
return (
|
||||||
|
suggestions != null &&
|
||||||
|
!suggestions.title &&
|
||||||
|
!suggestions.tags?.length &&
|
||||||
|
!suggestions.suggested_tags?.length &&
|
||||||
|
!suggestions.correspondents?.length &&
|
||||||
|
!suggestions.suggested_correspondents?.length &&
|
||||||
|
!suggestions.document_types?.length &&
|
||||||
|
!suggestions.suggested_document_types?.length &&
|
||||||
|
!suggestions.storage_paths?.length &&
|
||||||
|
!suggestions.suggested_storage_paths?.length &&
|
||||||
|
!suggestions.dates?.length
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -14,7 +14,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div content class="wrapper fade" [class.show]="show()">
|
<div content class="wrapper fade" [class.show]="show()">
|
||||||
@if (displayMode() === DisplayMode.TABLE) {
|
@if (error()) {
|
||||||
|
<div class="alert alert-danger mb-0" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{error()}}</div>
|
||||||
|
} @else if (displayMode() === DisplayMode.TABLE) {
|
||||||
<table class="table table-hover mb-0 mt-n2 align-middle">
|
<table class="table table-hover mb-0 mt-n2 align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
+27
-2
@@ -1,6 +1,10 @@
|
|||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { DatePipe } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import {
|
||||||
|
HttpErrorResponse,
|
||||||
|
provideHttpClient,
|
||||||
|
withInterceptorsFromDi,
|
||||||
|
} from '@angular/common/http'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
@@ -8,7 +12,7 @@ import { Router } from '@angular/router'
|
|||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { Subject, of } from 'rxjs'
|
import { Subject, of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
|
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
|
||||||
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
||||||
@@ -230,6 +234,27 @@ describe('SavedViewWidgetComponent', () => {
|
|||||||
expect(component.documents()).toEqual(documentResults)
|
expect(component.documents()).toEqual(documentResults)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show an error if documents fail to load', () => {
|
||||||
|
jest.spyOn(documentService, 'listFiltered').mockReturnValue(
|
||||||
|
throwError(
|
||||||
|
() =>
|
||||||
|
new HttpErrorResponse({
|
||||||
|
error: { added__date__lte: ['Enter a valid date.'] },
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
component.reload()
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.loading()).toBe(false)
|
||||||
|
expect(component.error()).toEqual('Added: Enter a valid date.')
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||||
|
'Error while loading documents: Added: Enter a valid date.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should reload on document consumption finished', () => {
|
it('should reload on document consumption finished', () => {
|
||||||
const fileStatusSubject = new Subject<FileStatus>()
|
const fileStatusSubject = new Subject<FileStatus>()
|
||||||
jest
|
jest
|
||||||
|
|||||||
+37
-6
@@ -125,6 +125,8 @@ export class SavedViewWidgetComponent
|
|||||||
|
|
||||||
readonly count = signal<number>(null)
|
readonly count = signal<number>(null)
|
||||||
|
|
||||||
|
readonly error = signal<string | null>(null)
|
||||||
|
|
||||||
placeholderRows: number[] = []
|
placeholderRows: number[] = []
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -180,6 +182,7 @@ export class SavedViewWidgetComponent
|
|||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
this.loading.set(this.documents().length == 0)
|
this.loading.set(this.documents().length == 0)
|
||||||
|
this.error.set(null)
|
||||||
this.show.set(true)
|
this.show.set(true)
|
||||||
this.documentService
|
this.documentService
|
||||||
.listFiltered(
|
.listFiltered(
|
||||||
@@ -191,12 +194,40 @@ export class SavedViewWidgetComponent
|
|||||||
{ truncate_content: true }
|
{ truncate_content: true }
|
||||||
)
|
)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((result) => {
|
.subscribe({
|
||||||
this.documents.set(result.results)
|
next: (result) => {
|
||||||
this.count.set(result.count)
|
this.documents.set(result.results)
|
||||||
this.savedViewService.setDocumentCount(this.savedView, result.count)
|
this.count.set(result.count)
|
||||||
this.loading.set(false)
|
this.savedViewService.setDocumentCount(this.savedView, result.count)
|
||||||
this.show.set(true)
|
this.loading.set(false)
|
||||||
|
this.show.set(true)
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.documents.set([])
|
||||||
|
this.count.set(null)
|
||||||
|
let errorMessage
|
||||||
|
if (
|
||||||
|
typeof error.error === 'object' &&
|
||||||
|
Object.keys(error.error).length > 0
|
||||||
|
) {
|
||||||
|
errorMessage = Object.keys(error.error)
|
||||||
|
.map((fieldName) => {
|
||||||
|
const fieldNameBase = fieldName.split('__')[0]
|
||||||
|
const fieldError: Array<string> = error.error[fieldName]
|
||||||
|
return `${
|
||||||
|
this.documentService.sortFields.find(
|
||||||
|
(f) => f.field?.split('__')[0] == fieldNameBase
|
||||||
|
)?.name ?? fieldNameBase
|
||||||
|
}: ${fieldError[0]}`
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
} else {
|
||||||
|
errorMessage = error.error
|
||||||
|
}
|
||||||
|
this.error.set(errorMessage)
|
||||||
|
this.loading.set(false)
|
||||||
|
this.show.set(true)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<p i18n>Searching document with asn {{asn}}</p>
|
<p i18n>Searching document with asn {{asn()}}</p>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnInit, inject, signal } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { FILTER_ASN } from '../../data/filter-rule-type'
|
import { FILTER_ASN } from '../../data/filter-rule-type'
|
||||||
import { DocumentService } from '../../services/rest/document.service'
|
import { DocumentService } from '../../services/rest/document.service'
|
||||||
@@ -13,13 +13,13 @@ export class DocumentAsnComponent implements OnInit {
|
|||||||
private route = inject(ActivatedRoute)
|
private route = inject(ActivatedRoute)
|
||||||
private router = inject(Router)
|
private router = inject(Router)
|
||||||
|
|
||||||
asn: string
|
readonly asn = signal<string>(undefined)
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.paramMap.subscribe((paramMap) => {
|
this.route.paramMap.subscribe((paramMap) => {
|
||||||
this.asn = paramMap.get('id')
|
this.asn.set(paramMap.get('id'))
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.listAllFilteredIds([{ rule_type: FILTER_ASN, value: this.asn }])
|
.listAllFilteredIds([{ rule_type: FILTER_ASN, value: this.asn() }])
|
||||||
.subscribe((documentId) => {
|
.subscribe((documentId) => {
|
||||||
if (documentId.length == 1) {
|
if (documentId.length == 1) {
|
||||||
this.router.navigate(['documents', documentId[0]])
|
this.router.navigate(['documents', documentId[0]])
|
||||||
|
|||||||
@@ -113,8 +113,8 @@
|
|||||||
|
|
||||||
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
||||||
|
|
||||||
<div class="btn-toolbar mb-1 border-bottom">
|
<div class="btn-toolbar justify-content-end mb-1 pb-3 gap-2 row-gap-2 border-bottom">
|
||||||
<div class="btn-group pb-3">
|
<div class="btn-group me-auto">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()">
|
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()">
|
||||||
<i-bs width="1.2em" height="1.2em" name="x"></i-bs>
|
<i-bs width="1.2em" height="1.2em" name="x"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
@@ -127,32 +127,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||||
<div class="btn-group pb-3 ms-auto">
|
<div class="d-flex gap-2">
|
||||||
<pngx-suggestions-dropdown *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"
|
<div class="btn-group">
|
||||||
[disabled]="!userCanEdit || suggestionsLoading()"
|
<pngx-suggestions-dropdown *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"
|
||||||
[loading]="suggestionsLoading()"
|
[disabled]="!userCanEdit || suggestionsLoading()"
|
||||||
[suggestions]="suggestions()"
|
[loading]="suggestionsLoading()"
|
||||||
[aiEnabled]="aiEnabled"
|
[suggestions]="suggestions()"
|
||||||
(getSuggestions)="getSuggestions()"
|
[aiEnabled]="aiEnabled"
|
||||||
(addTag)="createTag($event)"
|
(getSuggestions)="getSuggestions()"
|
||||||
(addDocumentType)="createDocumentType($event)"
|
(addTag)="createTag($event)"
|
||||||
(addCorrespondent)="createCorrespondent($event)">
|
(addDocumentType)="createDocumentType($event)"
|
||||||
</pngx-suggestions-dropdown>
|
(addCorrespondent)="createCorrespondent($event)">
|
||||||
|
</pngx-suggestions-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<pngx-custom-fields-dropdown
|
||||||
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"
|
||||||
|
[documentId]="documentId()"
|
||||||
|
[disabled]="!userCanEdit"
|
||||||
|
[existingFields]="document()?.custom_fields"
|
||||||
|
(created)="refreshCustomFields()"
|
||||||
|
(added)="addField($event)">
|
||||||
|
</pngx-custom-fields-dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group pb-3 ms-2">
|
<div class="ps-3">
|
||||||
<pngx-custom-fields-dropdown
|
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"
|
|
||||||
[documentId]="documentId()"
|
|
||||||
[disabled]="!userCanEdit"
|
|
||||||
[existingFields]="document()?.custom_fields"
|
|
||||||
(created)="refreshCustomFields()"
|
|
||||||
(added)="addField($event)">
|
|
||||||
</pngx-custom-fields-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [activeId]="activeNavID()" (activeIdChange)="activeNavID.set($event)">
|
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [activeId]="activeNavID()" (activeIdChange)="activeNavID.set($event)">
|
||||||
@@ -276,7 +280,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex border-top pt-3">
|
<div class="d-flex justify-content-end border-top pt-3">
|
||||||
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
|
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -440,7 +444,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #saveButtons>
|
<ng-template #saveButtons>
|
||||||
<div class="btn-group pb-3 ms-4">
|
<div class="btn-group">
|
||||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||||
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">Save</button>
|
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">Save</button>
|
||||||
@if (hasNext()) {
|
@if (hasNext()) {
|
||||||
@@ -472,6 +476,7 @@
|
|||||||
<div class="preview-sticky pdf-viewer-container">
|
<div class="preview-sticky pdf-viewer-container">
|
||||||
<pngx-pdf-viewer
|
<pngx-pdf-viewer
|
||||||
[src]="pdfSource()"
|
[src]="pdfSource()"
|
||||||
|
[sourceRevision]="previewRevision()"
|
||||||
[password]="pdfPassword()"
|
[password]="pdfPassword()"
|
||||||
[renderMode]="PdfRenderMode.All"
|
[renderMode]="PdfRenderMode.All"
|
||||||
[page]="previewCurrentPage()" (pageChange)="previewCurrentPage.set($event)"
|
[page]="previewCurrentPage()" (pageChange)="previewCurrentPage.set($event)"
|
||||||
@@ -482,9 +487,7 @@
|
|||||||
</pngx-pdf-viewer>
|
</pngx-pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%">
|
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%"></object>
|
||||||
<span>Preview is unavailable.</span>
|
|
||||||
</object>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@case (ContentRenderType.Text) {
|
@case (ContentRenderType.Text) {
|
||||||
@@ -505,9 +508,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@case (ContentRenderType.Other) {
|
@case (ContentRenderType.Other) {
|
||||||
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%">
|
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%"></object>
|
||||||
<span>Preview is unavailable.</span>
|
|
||||||
</object>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (requiresPassword) {
|
@if (requiresPassword) {
|
||||||
|
|||||||
@@ -963,12 +963,24 @@ describe('DocumentDetailComponent', () => {
|
|||||||
component.reprocess()
|
component.reprocess()
|
||||||
const modalCloseSpy = jest.spyOn(openModal, 'close')
|
const modalCloseSpy = jest.spyOn(openModal, 'close')
|
||||||
openModal.componentInstance.confirmClicked.next()
|
openModal.componentInstance.confirmClicked.next()
|
||||||
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] })
|
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] }, false)
|
||||||
expect(modalSpy).toHaveBeenCalled()
|
expect(modalSpy).toHaveBeenCalled()
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
expect(modalCloseSpy).toHaveBeenCalled()
|
expect(modalCloseSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should pass remote OCR choice when reprocessing', () => {
|
||||||
|
initNormally()
|
||||||
|
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
|
||||||
|
reprocessSpy.mockReturnValue(of(true))
|
||||||
|
let openModal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||||
|
component.reprocess()
|
||||||
|
openModal.componentInstance.remoteOcr = true
|
||||||
|
openModal.componentInstance.confirmClicked.next()
|
||||||
|
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] }, true)
|
||||||
|
})
|
||||||
|
|
||||||
it('should show error if redo ocr call fails', () => {
|
it('should show error if redo ocr call fails', () => {
|
||||||
initNormally()
|
initNormally()
|
||||||
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
|
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
|
||||||
@@ -1602,6 +1614,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(openDoc.__changedFields).toEqual([])
|
expect(openDoc.__changedFields).toEqual([])
|
||||||
expect(setDirtySpy).toHaveBeenCalledWith(openDoc, false)
|
expect(setDirtySpy).toHaveBeenCalledWith(openDoc, false)
|
||||||
expect(saveSpy).toHaveBeenCalled()
|
expect(saveSpy).toHaveBeenCalled()
|
||||||
|
expect(component.previewRevision()).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore incoming update for a different document id', () => {
|
it('should ignore incoming update for a different document id', () => {
|
||||||
@@ -2160,8 +2173,14 @@ describe('DocumentDetailComponent', () => {
|
|||||||
it('should support open share links and email modals', () => {
|
it('should support open share links and email modals', () => {
|
||||||
const modalSpy = jest.spyOn(modalService, 'open')
|
const modalSpy = jest.spyOn(modalService, 'open')
|
||||||
initNormally()
|
initNormally()
|
||||||
|
component.selectedVersionId.set(10)
|
||||||
component.openShareLinks()
|
component.openShareLinks()
|
||||||
expect(modalSpy).toHaveBeenCalled()
|
expect(modalSpy).toHaveBeenCalled()
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
modalSpy.mock.results[0].value as NgbModalRef
|
||||||
|
).componentInstance.documentId()
|
||||||
|
).toBe(10)
|
||||||
component.openEmailDocument()
|
component.openEmailDocument()
|
||||||
expect(modalSpy).toHaveBeenCalled()
|
expect(modalSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -2190,9 +2209,6 @@ describe('DocumentDetailComponent', () => {
|
|||||||
const appendChildSpy = jest
|
const appendChildSpy = jest
|
||||||
.spyOn(document.body, 'appendChild')
|
.spyOn(document.body, 'appendChild')
|
||||||
.mockImplementation((node: Node) => node)
|
.mockImplementation((node: Node) => node)
|
||||||
const removeChildSpy = jest
|
|
||||||
.spyOn(document.body, 'removeChild')
|
|
||||||
.mockImplementation((node: Node) => node)
|
|
||||||
const createObjectURLSpy = jest
|
const createObjectURLSpy = jest
|
||||||
.spyOn(URL, 'createObjectURL')
|
.spyOn(URL, 'createObjectURL')
|
||||||
.mockReturnValue('blob:mock-url')
|
.mockReturnValue('blob:mock-url')
|
||||||
@@ -2211,6 +2227,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
src: '',
|
src: '',
|
||||||
onload: null,
|
onload: null,
|
||||||
contentWindow: mockContentWindow,
|
contentWindow: mockContentWindow,
|
||||||
|
remove: jest.fn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const createElementSpy = jest
|
const createElementSpy = jest
|
||||||
@@ -2254,12 +2271,11 @@ describe('DocumentDetailComponent', () => {
|
|||||||
mockContentWindow.onafterprint(new Event('afterprint'))
|
mockContentWindow.onafterprint(new Event('afterprint'))
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
expect(mockIframe.remove).toHaveBeenCalled()
|
||||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
|
|
||||||
createElementSpy.mockRestore()
|
createElementSpy.mockRestore()
|
||||||
appendChildSpy.mockRestore()
|
appendChildSpy.mockRestore()
|
||||||
removeChildSpy.mockRestore()
|
|
||||||
createObjectURLSpy.mockRestore()
|
createObjectURLSpy.mockRestore()
|
||||||
revokeObjectURLSpy.mockRestore()
|
revokeObjectURLSpy.mockRestore()
|
||||||
})
|
})
|
||||||
@@ -2306,9 +2322,6 @@ describe('DocumentDetailComponent', () => {
|
|||||||
const appendChildSpy = jest
|
const appendChildSpy = jest
|
||||||
.spyOn(document.body, 'appendChild')
|
.spyOn(document.body, 'appendChild')
|
||||||
.mockImplementation((node: Node) => node)
|
.mockImplementation((node: Node) => node)
|
||||||
const removeChildSpy = jest
|
|
||||||
.spyOn(document.body, 'removeChild')
|
|
||||||
.mockImplementation((node: Node) => node)
|
|
||||||
const createObjectURLSpy = jest
|
const createObjectURLSpy = jest
|
||||||
.spyOn(URL, 'createObjectURL')
|
.spyOn(URL, 'createObjectURL')
|
||||||
.mockReturnValue('blob:mock-url')
|
.mockReturnValue('blob:mock-url')
|
||||||
@@ -2331,6 +2344,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
src: '',
|
src: '',
|
||||||
onload: null,
|
onload: null,
|
||||||
contentWindow: mockContentWindow,
|
contentWindow: mockContentWindow,
|
||||||
|
remove: jest.fn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const createElementSpy = jest
|
const createElementSpy = jest
|
||||||
@@ -2353,15 +2367,21 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
if (expectToast) {
|
if (expectToast) {
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
|
expect(mockIframe.remove).toHaveBeenCalled()
|
||||||
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
} else {
|
} else {
|
||||||
expect(toastSpy).not.toHaveBeenCalled()
|
expect(toastSpy).not.toHaveBeenCalled()
|
||||||
|
expect(mockIframe.remove).not.toHaveBeenCalled()
|
||||||
|
expect(revokeObjectURLSpy).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
component.ngOnDestroy()
|
||||||
|
|
||||||
|
expect(mockIframe.remove).toHaveBeenCalled()
|
||||||
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
}
|
}
|
||||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
|
||||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
|
||||||
|
|
||||||
createElementSpy.mockRestore()
|
createElementSpy.mockRestore()
|
||||||
appendChildSpy.mockRestore()
|
appendChildSpy.mockRestore()
|
||||||
removeChildSpy.mockRestore()
|
|
||||||
createObjectURLSpy.mockRestore()
|
createObjectURLSpy.mockRestore()
|
||||||
revokeObjectURLSpy.mockRestore()
|
revokeObjectURLSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user